Improve Spanish Anki vocabulary tooling

This commit is contained in:
2026-06-13 19:26:45 -04:00
parent 0c8561d25e
commit 67594835d2
6 changed files with 2027 additions and 16 deletions
+141 -13
View File
@@ -15,7 +15,7 @@ from .importer import (
supported_tts_backends,
synthesize_tts_sample,
)
from .words import compare_word_files, extract_words
from .words import compare_word_files, compare_word_lists, extract_words, extract_words_from_file, lint_anki_cards_main
from .youtube import run_youtube
@@ -85,22 +85,48 @@ def build_parser(config: Config | None = None) -> argparse.ArgumentParser:
audio.add_argument("--media-dir")
audio.add_argument("--copy-only-new", action="store_true")
words = sub.add_parser("words", help="Extract frequent words from Anki.")
words.add_argument("lang", choices=choices)
words = sub.add_parser("words", help="Extract frequent words from Anki or from a TSV export file.")
words.add_argument("lang", nargs="?", choices=choices)
words.add_argument("--lang", dest="lang_option", choices=choices, help="Language code.")
words.add_argument("--input", help="Anki TSV export file (instead of AnkiConnect).")
group = words.add_mutually_exclusive_group()
group.add_argument("--query")
group.add_argument("--deck", action="append")
words.add_argument("--field")
words.add_argument("--field", help="Anki field name (AnkiConnect) or 1-based column index (TSV file).")
words.add_argument("--min-freq", type=int, default=2)
words.add_argument("--outdir")
words.add_argument("--out")
words.add_argument("--out", "--output", dest="out")
words.add_argument("--full-field", action="store_true")
words.add_argument("--spacy-model")
words.add_argument("--include-proper-nouns", action="store_true", help="Include proper nouns (PROPN).")
words.add_argument("--function-words", action="store_true", help="Extract function words separately.")
words.add_argument("--debug", help="Path for debug TSV output.")
words.add_argument("--lemma-corrections", help="Path to bad<TAB>good TSV of lemma corrections.")
words.add_argument("--bad-lemma-file", help="Path to file listing lemmas to skip.")
words.add_argument("--include-tags", action="store_true", help="Include tags column in parsing.")
words.add_argument("--debug-min-freq", type=int, default=0,
help="Minimum frequency for debug output (default: all).")
words.add_argument("--no-clean", action="store_true",
help="Skip Anki field text cleaning before NLP.")
compare = sub.add_parser("compare-words", help="Print words in source that are not in known.")
compare.add_argument("source")
compare.add_argument("known")
compare_new = sub.add_parser("compare", help="Compare deck vocabulary against a target list.")
compare_new.add_argument("--deck-words", required=True, help="Path to deck word list.")
compare_new.add_argument("--target-words", required=True, help="Path to target vocabulary list.")
compare_new.add_argument("--output", help="Write missing words to this file.")
compare_new.add_argument("--seen-output", help="Write seen words to this file.")
compare_new.add_argument("--min-frequency", type=int, default=0,
help="Minimum frequency for deck words to count.")
lint_es = sub.add_parser("lint-anki-es", help="Check Spanish Anki cards for suspicious patterns.")
lint_es.add_argument("--input", required=True, help="Anki TSV export file.")
lint_es.add_argument("--field", type=int, default=2, help="1-based column index (default 2).")
lint_es.add_argument("--output", default="suspicious_cards_es.tsv", help="Output TSV path.")
lint_es.add_argument("--verbose", action="store_true", help="Output all cards, not just suspicious ones.")
youtube = sub.add_parser("youtube", help="Mine a YouTube transcript.")
youtube.add_argument("lang", choices=choices)
youtube.add_argument("video")
@@ -144,6 +170,19 @@ def main(argv: list[str] | None = None) -> int:
parser = build_parser(config)
args = parser.parse_args(argv)
if args.command == "words":
lang_option = getattr(args, "lang_option", None)
if args.lang and lang_option and args.lang != lang_option:
print(
f"Error: conflicting language values: {args.lang!r} and {lang_option!r}",
file=sys.stderr,
)
return 2
args.lang = lang_option or args.lang
if not args.lang:
print("Error: words requires a language, e.g. 'saiki words es' or 'saiki words --lang es'", file=sys.stderr)
return 2
if args.command == "audio":
result = extract_audio(config, args.lang, args.outdir, args.media_dir, args.copy_only_new, args.concat)
print(f"Copied {result['copied']} files")
@@ -154,14 +193,62 @@ def main(argv: list[str] | None = None) -> int:
return 0
if args.command == "words":
result = extract_words(
config, args.lang, args.query, args.deck, args.field, args.min_freq,
args.outdir, args.out, args.full_field, args.spacy_model,
)
print(f"Query: {result['query']}")
print(f"Found {result['notes']} notes")
print(f"Extracted {result['unique']} unique entries")
print(f"Wrote {result['written']} entries to: {result['out']}")
if args.input:
# File-based extraction
if config.language_name(args.lang) != "spanish":
print(
"Error: TSV file-based word extraction is currently Spanish-only.",
file=sys.stderr,
)
return 1
if args.field and not args.field.isdigit():
print(
f"Error: --field must be a 1-based column index (integer) "
f"when --input is used, got: {args.field!r}",
file=sys.stderr,
)
return 1
field_index = int(args.field) if args.field else 2
if field_index < 1:
print(
f"Error: --field must be a 1-based column index >= 1, got: {field_index}",
file=sys.stderr,
)
return 1
result = extract_words_from_file(
config, args.lang, args.input,
field_index=field_index,
min_freq=args.min_freq,
outdir=args.outdir,
out=args.out,
include_proper_nouns=args.include_proper_nouns,
function_words=args.function_words,
debug=args.debug,
lemma_corrections=args.lemma_corrections,
bad_lemma_file=args.bad_lemma_file,
spacy_model=args.spacy_model,
include_tags=args.include_tags,
debug_min_freq=args.debug_min_freq,
no_clean=args.no_clean,
)
print(f"Parsed {result['records']} records")
print(f"Wrote {result['written']} content entries to: {result['out']}")
if result.get("debug"):
print(f"Debug output: {result['debug']}")
if result.get("proper_nouns"):
print(f"Proper nouns: {result['proper_nouns']}")
if result.get("function_words"):
print(f"Function words: {result['function_words']}")
print(f"Suspicious tokens: {result.get('suspicious', '')}")
else:
result = extract_words(
config, args.lang, args.query, args.deck, args.field, args.min_freq,
args.outdir, args.out, args.full_field, args.spacy_model,
)
print(f"Query: {result['query']}")
print(f"Found {result['notes']} notes")
print(f"Extracted {result['unique']} unique entries")
print(f"Wrote {result['written']} entries to: {result['out']}")
return 0
if args.command == "compare-words":
@@ -169,6 +256,47 @@ def main(argv: list[str] | None = None) -> int:
print(line)
return 0
if args.command == "compare":
result = compare_word_lists(
args.deck_words,
args.target_words,
output_path=args.output,
min_frequency=args.min_frequency,
seen_output_path=args.seen_output,
)
print(f"Total missing from deck: {result['total_missing']}")
seen_count = len(result["seen"])
print(f"Total seen in deck: {seen_count}")
if result["missing"]:
print("Missing words:")
for w in result["missing"][:20]:
print(f" {w}")
if len(result["missing"]) > 20:
print(f" ... and {len(result['missing']) - 20} more")
if args.output:
print(f"Missing words written to: {args.output}")
if args.seen_output:
print(f"Seen words written to: {args.seen_output}")
return 0
if args.command == "lint-anki-es":
if args.field < 1:
print(
f"Error: --field must be a 1-based column index >= 1, got: {args.field}",
file=sys.stderr,
)
return 1
result = lint_anki_cards_main(
args.input,
field_index=args.field,
output_path=args.output,
verbose=args.verbose,
)
print(f"Checked {result['records']} cards")
print(f"Found {result['issues']} suspicious card(s)")
print(f"Report written to: {result['path']}")
return 0
if args.command == "youtube":
result = run_youtube(
config, args.lang, args.video, args.mode, args.top, args.no_stopwords,