Commit Graph

86 Commits

Author SHA1 Message Date
Trey T 5c0fc8ee2d Vocab Practice — proper SRS session queue with in-session learning steps
Vocab Flashcards / Multiple Choice were a flat linear walk through a
shuffled list — rating a card "Hard" or "Again" did nothing to bring it
back. Replaced with a real two-layer SRS, matching how Anki separates
in-session learning steps from the long-term schedule.

VocabSessionQueue (new) — the in-session layer. Position-based learning
steps:
  - Again → card reappears 5–8 cards later (state: learning)
  - Hard  → reappears 7–10 cards later (state: learning)
  - Good  → first time: → review state, reappears 16–24 cards later
            already in review: graduates, leaves the session
  - Easy  → graduates immediately
A card you keep failing keeps cycling until you mark it Good twice or
Easy once. answer() returns a ReviewQuality only on graduation — that's
the single rating handed to the long-term VerbReviewStore, so
intermediate Again/Hard presses no longer thrash the cross-session
SM-2 schedule.

VocabVerbPool.sessionVerbs (rewritten) — due-first ordering + a 20-card
session cap. Overdue verbs (per VerbReviewCard.dueDate) come first,
most-overdue leading; then never-reviewed verbs by frequency rank.
Not-yet-due verbs are intentionally skipped — that's the SRS schedule
doing its job. A single sitting is now bounded instead of a 100+ card
slog.

Study Again — the completion screen gets a "Study Again" button that
rebuilds the queue from the same verb set (re-shuffled), so you can run
the whole set again after finishing.

Progress display switched from "1 of 110" to "N learned · M to go",
which reflects the live queue as cards requeue and graduate.

Both vocab views now share VocabSessionQueue + VocabVerbPool; the queue
struct is pure value-type logic, easy to reason about and test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:44:43 -05:00
Trey T d61f9e50b1 Vocab Practice — speaker button to hear the Spanish word
Both vocab session reveal panes now show a speaker button next to the
Spanish infinitive. Tapping it speaks the word via the existing
SpeechService (Spanish voice), same TTS the Course flashcards use.

Flashcard mode: button beside the infinitive in the reveal pane.
Multiple choice: button beside the infinitive in the answer feedback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:15:12 -05:00
Trey T 900a927f95 Fixes #34 — drop AI-generated images from verb vocab practice
Image Playground's .illustration style can't depict abstract verbs —
"dar" (to give) generated a Shiba Inu in a meadow, "saber" a cake.
Verbs are actions, not objects; a static illustration rarely helps and
the irrelevant output was pure noise.

Removed:
  - VerbIllustration view (was in VocabFlashcardPracticeView).
  - VocabImageService.swift entirely — VerbIllustration was its only
    consumer.
  - vocabImageService from the app environment in ConjugaApp.
  - The illustration row from both vocab session reveal panes.

Vocab card reveal is now: Spanish infinitive + example sentence +
rating buttons. The example sentence already carries the learning
context.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:38:01 -05:00
Trey T f0eb75a28a Fixes #33 — verb examples must actually use the verb
The example sentences in VerbDetailView (and the new vocab practice
modes) frequently used the wrong verb — a "tener" set would show
"Él estaba leyendo un libro" (estar), "Nosotros vamos a viajar" (ir),
"Tú debes estudiar" (deber). The model drifted off the target verb
partway through generating the 6-example batch, and nothing checked
the output.

Two defenses:

  Prompt grounding — VerbExampleGenerator.generate now takes a
  formsByTense map (tenseId → conjugated forms, from the new
  ReferenceStore.conjugatedForms). Each tense line in the prompt
  lists the verb's exact conjugated forms and instructs the model to
  use one of them. The model echoes a real form instead of recalling
  (and mis-recalling) the conjugation.

  Output validation — every generated sentence is checked against the
  conjugation table via accent/case-folded whole-word matching. Any
  sentence that doesn't contain a real conjugated form of the verb is
  rejected. Failures trigger one regeneration pass; anything still
  wrong is dropped rather than displayed. Better to show 4 correct
  examples than 6 with 2 wrong.

  Cache invalidation — VerbExampleCache now persists a versioned
  wrapper (version 2). Pre-fix cached example sets — which may contain
  wrong-verb sentences — fail the version check and are discarded, so
  they regenerate cleanly under the new path.

Callers updated: VerbDetailView, VocabFlashcardPracticeView,
VocabMultipleChoicePracticeView all build formsByTense from
ReferenceStore and pass it through.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:57:57 -05:00
Trey T f4c139aed0 Fixes #35 — vocab practice no longer doubles the verb's "to"
VocabFlashcardPracticeView and VocabMultipleChoicePracticeView rendered
the English prompt as "to \(verb.english)". Every verb in conjuga_data
already stores english with a leading "to " (all 1750 of them), so the
prompt came out as "to to be", "to to run", etc.

Dropped the prefix — verb.english is shown verbatim. Same fix applied
to the Image Playground concept string so generated illustrations are
prompted with "to be" rather than "to to be".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:43:34 -05:00
Trey T 0af8e648fe Vocab Practice crash — defensive Dictionary init + correct tense count
Crash: Swift/NativeDictionary.swift:792 Fatal error: Duplicate values
for key: 'imp_tú', triggered the first time a user rated a vocab card
and the in-flight example generation tried to materialise.

Root cause: VerbExampleGenerator.generate() builds a [tenseId: example]
dictionary from the model's output via Dictionary(uniqueKeysWithValues:),
which traps on duplicates. The generator's @Generable schema declares
@Guide(.count(6)) on the examples array, so the LLM is forced to return
exactly 6. The new Vocab Flashcards / Multiple Choice views called
generate(... tenseIds: ["ind_presente"]) — only one tense — which left
the model to invent the other 5 tenseIds; it duplicated 'imp_tú' and
the dictionary init trapped.

Three fixes:

  Services/VerbExampleGenerator.swift — use Dictionary(_:uniquingKeysWith:)
  with first-wins so the generator can never crash regardless of caller
  shape.

  Views/Practice/Vocab/VocabFlashcardPracticeView.swift and
  Views/Practice/Vocab/VocabMultipleChoicePracticeView.swift — pass
  the canonical 6-tense set (VocabExampleTenseIds.canonical, same as
  VerbDetailView uses), then pick the ind_presente example for the
  card. Caches all six in VerbExampleCache as a side effect.

  Views/Verbs/VerbListView.swift — replace empty-string systemImage
  on the Level menu Labels with "circle" so the device console isn't
  spammed with "No symbol named '' found in system symbol set" every
  time the user opens the filter menu.

The crash analysis I gave earlier (CloudKit schema migration of
VerbReviewCard) was wrong — device console shows the real culprit. No
CloudKit-side changes needed; the new model stays in the cloud
container.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 10:02:34 -05:00
Trey T c890095610 Vocab Practice — verb pool, Settings level filter, per-verb SRS
Vocab Practice used a deck picker over VocabCard rows. That meant it
ignored the Settings level toggles entirely and operated on a totally
separate vocabulary universe than the conjugation modes. Rewired
end-to-end:

Pool source
  Replaced VocabCard with the Verb table. The pool is now
  ReferenceStore.fetchVerbs(selectedLevels: UserProgress.selectedVerbLevels)
  — the same call PracticeSessionService uses for conjugation. Changes
  to level toggles in Settings (or the Verbs tab, which also writes to
  this field) immediately affect Vocab Practice.

Entry flow
  Deleted VocabPracticeEntryView. Practice → Vocabulary now has two
  direct entries:
    • Vocab Flashcards — verb.english → tap → verb.infinitive
    • Vocab Multiple Choice — verb.english → pick from 4 infinitives
  Both pull from the same level-filtered pool, shuffled.

Per-verb SRS
  New VerbReviewCard @Model (cloud-synced, mirrors CourseReviewCard's
  SM-2 fields but keyed by verbId). VerbReviewStore.rate(verbId:quality:)
  applies the existing SRSEngine. Registered in cloud container schema
  in ConjugaApp.swift.

Example sentences
  Lazy-generated via VerbExampleGenerator on first reveal, cached
  through VerbExampleCache (same path VerbDetailView uses). Empty until
  the example arrives — block hides itself if Apple Intelligence isn't
  available.

AI illustration
  VerbIllustration replaces VocabIllustration; same Image Playground
  pipeline. Cache key uses ("verb", infinitive, english) so verbs and
  course-deck vocab never collide.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 23:31:12 -05:00
Trey T 164a0a1bb7 Verbs — share level filter with Practice via UserProgress
VerbListView used a local @State selectedLevel that reset whenever
the user left the tab and was disconnected from Practice's verb
sourcing. PracticeSessionService reads UserProgress.selectedVerbLevels;
the Verbs tab now does the same.

  Level menu becomes multi-select (toggle each level in/out, mirrors
  SettingsView's pattern). "All Levels" enables every level; tapping
  again on a level removes it from the active set. Picking "All"
  while everything is already on is a no-op.

  Filter logic loops over the active level set and admits a verb if
  any active level matches via VerbLevelGroup.matches.

  Active-filter chip shows "Basic", "Elementary", etc. when one level
  is on, or "N levels" when multiple are on. Tapping the chip resets
  to all-levels.

  Empty set is treated as "no filter applied" on the Verbs list, but
  setAllLevels(enabled: false) falls back to [.basic] rather than an
  empty set — Practice treats empty as "no verbs", so we guard against
  leaving the user with an empty pool.

Practice already reads UserProgress.selectedVerbLevels, so changes
made in the Verbs tab now flow into Practice automatically. No
Practice-side code changes needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 23:18:23 -05:00
Trey T d49eb38a6d Books — bump bookDataVersion to 4 to force re-seed
A device install was showing the Books row but no books inside. The
seed code is intact (bundle resource present, schema registered,
explicit-slug fallback in place). Bumping the version flips
versionCurrent to false on every existing install, forcing
refreshBooksDataIfNeeded to wipe + re-seed on next launch. The
diagnostic prints added earlier ([DataLoader] bundledBookJSONURLs
found N files / Book seeding complete) will tell us if it still
fails this time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 23:09:00 -05:00
Trey T 0b7d4a73ad Add Vocab Practice — English-first flashcards + multiple choice, with AI illustrations
Practice tab restructured into three sections:
- Conjugation: the 6 conjugation modes + the Common Tenses / Weak
  Verbs / Irregularity Drills focus buttons.
- Vocabulary: new "Vocab Practice" entry (flashcard or multiple
  choice, deck-picker on entry) + the existing Vocab Review.
- Reading: Stories, Books, Lyrics, Conversation, Listening, Cloze
  (moved here from the flat list).

VocabPracticeEntryView lets the user pick any course/textbook deck
(or "All decks") and a mode (Flashcard / Multiple Choice). Last-used
choice is remembered via @AppStorage.

VocabFlashcardPracticeView:
  Front shows the English meaning. Tap to reveal the Spanish word,
  example sentences from the card, an AI-generated illustration of
  the concept, and Again/Hard/Good/Easy rating buttons. SRS updates
  via the existing CourseReviewStore.rate() path.

VocabMultipleChoicePracticeView:
  English prompt, 4 Spanish options. Distractors come from the same
  deck and prefer matching part-of-speech (via
  DictionaryService.lookup) — falls back to random when POS is
  unknown or the deck has fewer than 4 cards. After answer: reveal
  correct/incorrect, the Spanish word, examples, illustration, and
  the same rating buttons.

VocabImageService wraps Apple Intelligence's ImageCreator
(iOS 18.2+) for on-device illustration generation. Caches PNG
results to disk under Caches/VocabImages keyed by
SHA256(deck+ES+EN). In-flight dedup keeps concurrent requests for
the same key sharing one task. Falls back to a placeholder UI when
Apple Intelligence isn't available (older devices / disabled in
Settings) — detected lazily on the first failed ImageCreator init.

EN-first direction is enforced regardless of the underlying deck's
isReversed flag, so the user sees the English-to-Spanish recall
direction they asked for even when practising a reversed course
deck.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 23:02:02 -05:00
Trey T 9aa4d0836d Guide — bidirectional cross-link chips between tense guides & grammar notes
After the full enrichment pass, both the Tenses and Grammar surfaces of
the Guide tab cover overlapping material — WEIRDO appears in both
"Subjuntivo Presente" and "Subjunctive Triggers", preterite↔imperfect
contrast in three places, etc. Instead of trimming either body and
losing content, add a small chip row at the top of each detail view
linking directly across.

GuideCrossLinks.swift (new) — curated tense→[noteId] map covering 18 of
the 20 tenses. The two without aligned notes (ind_pluscuamperfecto,
ind_preterito_anterior) don't show chips. The reverse map (noteId→
[tenseId]) is derived once at static init and sorted by canonical tense
order so chips appear in conjugation-table order.

GuideDetailView — "Related grammar" indigo chip row directly under the
header. Tap a chip → switch to the Grammar segment with that note
selected.

GrammarNoteDetailView — "Used in tenses" orange chip row directly under
the title. Tap a chip → switch to the Tenses segment with that tense
selected.

The GuideView segment-change handler now only clears the *other* tab's
selection so programmatic jumps keep their destination intact; manual
segment swipes still feel "fresh" like before.

No content is removed. Users get a deeper-dive path one tap away in
either direction, and the redundancy becomes a feature instead of a
maintenance hazard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 10:59:46 -05:00
Trey T 5db4b014a9 Guide — full enrichment pass: 19 tense guides + 36 grammar notes
Brings every tense guide and grammar note in the app up to teacher-
handout depth via parallel research subagents drafting against a
shared "thorough" checklist (TL;DR, usages, conjugation table, common
irregulars, mnemonic, top pitfalls, contrast with neighbour topic,
real-world dialogue example).

Tense guides — Conjuga/conjuga_data.json (tenseGuides[].body)
  All 19 remaining guides rewritten. The 20th (subj_presente) was
  enriched in the prior commit. Each body now ~4-5.5K chars (vs the
  500-1500 chars of the pre-pass reference cards), covering:
  - All five indicative tenses, both conditionals, both imperatives.
  - Full subjunctive set including the archaic futuro / futuro
    perfecto, framed honestly with "recognise, don't produce" guidance.
  - Per-tense conjugation patterns and the top 5-15 irregular verbs.
  - Tense-vs-tense contrasts (preterite↔imperfect, future↔ir-a,
    -ra↔-se past subjunctive, etc.).
  - Pitfalls that English speakers actually make.

Grammar notes — Conjuga/Conjuga/Models/GrammarNote.swift
  All 36 notes audited and rewritten where the existing body was
  missing one of: explicit mnemonic, contrast pair, pitfalls section,
  or coverage of a key sub-topic. None copied verbatim — every note
  got at least one of those slotted in. Notable additions:
  - DOCTOR/PLACE, WEIRDO, ESCAPA, RID, PRODDS, BANGS, RRPIA mnemonics
    where missing.
  - commands-imperative: nosotros + vosotros forms were entirely
    absent; both added with the -d/-os and present-subjunctive rules.
  - relative-pronouns: el que/el cual distinction, cuyo, lo que/lo
    cual, donde/adonde.
  - se-constructions: all 6 uses including the le→se substitution.
  - irregular-yo-verbs: impact on subjunctive and negative tú command.
  - Plus 5-item pitfalls sections on every note that lacked one.

Tooling — Conjuga/Scripts/guide-enrichment/
  - PLAN.md (prior commit) — the audit, checklist, and priority order
    that drove this pass.
  - apply_drafts.py (new) — reads drafts/out/*.md, swaps tense guides
    into the JSON and grammar notes into the Swift source via regex on
    the GrammarNote(...) declarations. Handles multi-block `#` comment
    headers some agents emitted. drafts/in/ and drafts/out/ are
    gitignored — regeneratable from current state.

DataLoader.swift — courseDataVersion 8 → 9 so existing installs re-
seed all guides on next launch.

Verification:
  - `swift -frontend -parse` on GrammarNote.swift succeeds (exit 0).
  - JSON validates (python3 json.load round-trip).
  - Triple-quote count is even (72 = 36 pairs, matching 36 notes).
  - Full xcodebuild verify deferred — local SDK install was disrupted
    by an Xcode update; will retest as part of the next ad-hoc deploy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 09:50:35 -05:00
Trey T de446b2301 Guide — enrich present-subjunctive entry with WEIRDO + ESCAPA + plan
The present-subjunctive guide was surface-level: two numbered usages
and a handful of examples, no mnemonic and no structural trigger cue.
That's the recurring problem with the tense guides — they're reference
cards, not teaching materials.

This commit fixes the immediate gap and lays out a plan to fix the
rest:

  Conjuga/conjuga_data.json — subj_presente body expanded from 794 to
  3670 chars. Adds the WEIRDO mnemonic with per-letter triggers and
  examples (Wishes, Emotions, Impersonal, Recommendations, Doubt,
  Ojalá), the ESCAPA adverbial-conjunction set, the "que + change of
  subject" structural rule, adjectival clauses with unknown
  antecedents, and the future-time-clause rule (cuando / hasta que /
  en cuanto).

  Scripts/guide-enrichment/PLAN.md (new) — audit of all 20 tense
  guides and 36 grammar notes, tier-1/2/3 prioritisation, "thorough"
  checklist (TL;DR, usages, conjugation, irregulars, mnemonic,
  pitfalls, contrast, dialogue example), research sources, per-topic
  workflow, effort estimate.

  DataLoader.swift — courseDataVersion 7 → 8 so existing installs
  re-seed the new body on next launch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:33:09 -05:00
Trey T a416233a2d Books — read-aloud mode with active-word highlight and tap-to-define
Adds a TTS read-along to the book reader. Tap the play button in the
toolbar; AVSpeechSynthesizer reads the chapter paragraph-by-paragraph
with the current word highlighted in yellow, auto-scrolling the active
paragraph to centre. Tap any word during read-along to pause and open
the definition sheet; reading resumes when the sheet dismisses.

Behavior per spec:
- Tap-to-define interrupts the synth (pauseSpeaking at: .immediate) and
  resumes on sheet dismiss.
- Voice picker sheet (waveform.circle toolbar button) lists installed
  Spanish voices grouped by Premium / Enhanced / Default quality, with
  a "Download more voices…" row that opens iOS Settings (no public
  deep-link to Accessibility → Spoken Content exists; the footer spells
  out the path).
- Speed picker (Slow / Normal / Fast) drives AVSpeechUtterance.rate.
- Stops at chapter end, no auto-advance to the next chapter.
- Vocabulary lines shaped `palabra = meaning` are skipped — the synth
  would otherwise say "palabra equals meaning" and they're reference
  material, not prose.

Audio session uses .playback + .spokenAudio mode and is properly
deactivated with .notifyOthersOnDeactivation on stop() so music apps
resume cleanly after reading ends.

Voice/rate persisted via @AppStorage; controller picks them up
onAppear and writes back through Bindings the picker mutates.

Word-index space in BookSpeechController.wordRanges(in:) matches
BookReaderView's split(separator: " ") rendering exactly — both split
on ASCII U+0020 only, so willSpeakRange callbacks resolve to the right
visible word.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 21:42:09 -05:00
Trey T 70d8299df8 Books — robust bundle lookup so ad-hoc device installs always seed
bundledBookJSONURLs() was relying solely on
Bundle.urls(forResourcesWithExtension:subdirectory:), the directory-
enumeration API. That API is observed to return empty in some on-device
configurations even when the resource is present, which would cause
seedBooks() to silently no-op and leave BookLibraryView showing "No
Books" despite the JSON being bundled correctly.

Switched to the same pattern textbook seeding uses: explicit per-slug
Bundle.url(forResource:withExtension:) with a bundleURL.appending-
PathComponent fallback. Directory enumeration is kept as a secondary so
future books bundled without code changes still get picked up.

Also added diagnostic prints to refreshBooksDataIfNeeded and the URL
discovery step so device logs reveal what happened if seeding still
falls through.

Bumped bookDataVersion to 3 so existing installs re-trigger
refreshBooksDataIfNeeded → seedBooks on next launch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 10:36:16 -05:00
Trey T 51067e23fd Fix Books navigation — tapping a book no longer re-pushes the library
BookLibraryView is itself pushed from PracticeView's NavigationStack,
so the .navigationDestination(for: Book.self) it declared was a
non-root registration. Combined with NavigationLink(value: book), that
resolved the push to *both* the destination handler and the closure
that produced BookLibraryView originally — pushing the chapter list
underneath, then re-pushing the library on top. Hitting back popped
the library and revealed the chapter list, in the wrong order.

Switched both Library→ChapterList and ChapterList→Reader to closure-
based NavigationLinks. Destinations attach directly to the link, no
type-keyed registry involved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 10:18:04 -05:00
Trey T 05a367fdbe Books — capture <li> vocab bullets the extractor was silently dropping
extract_epub.py was walking <p> only, but every "Vocabulario" section in
the Olly Richards EPUB lives inside <ul><li>...</li></ul>. That meant
the heading made it through but the entries didn't — 680 vocab lines
across 24 sections in this book were missing from the bundled JSON.

Audit (text-node owner by closest block ancestor) confirmed <li> is the
only silent drop: 5,260 nodes in <p>, 1,960 in <li>, 0 anywhere else.
No <h1>-<h6>, tables, or blockquotes in this EPUB at all.

Fix: walk find_all(["p", "li"]) in document order so bullet entries
slot in right after their "Vocabulario" / list heading. Re-extracted
(2,646 → 3,326 paragraphs), re-translated all 118 jobs in parallel
Claude Code subagents. translate_chapters.py prompt template now tells
subagents to keep bilingual `palabra = meaning` lines verbatim — both
sides already coexist on the line.

Bumped bookDataVersion to 2 so refreshBooksDataIfNeeded re-seeds.
Verified in simulator: all 13 chapter row sizes grew (e.g. ch6
18,295→20,951 chars).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 10:10:34 -05:00
Trey T 09e49bda2c Add Books — read EPUB-imported books in Practice with tap-to-define
New "Books" row in the Practice tab opens a library of bundled bilingual
books. Each chapter renders Spanish paragraph-by-paragraph; tap any
word for a definition sheet (DictionaryService with on-device AI
fallback), or toggle the toolbar button to swap to the pre-computed
English translation inline.

Local-only Book + BookChapter SwiftData models added to the local
container schema (reset version bumped to 5). DataLoader.seedBooks
walks the bundle for `book_*.json` resources, so future books drop in
without touching app code — just bundle a new JSON and bump
bookDataVersion.

First book: Olly Richards' "Spanish Short Stories For Beginners
Vol 2" — 13 chapters, 2,646 paragraphs, bilingual.

Scripts/books/ is the repeatable pipeline for future EPUBs:
extract_epub.py → translate_chapters.py (per-chapter resumable jobs) →
bundle_book.py. Translation is done by parallel Claude Code subagents
reading per-job input files and writing output files — no API key
required, matching the pattern used for the textbook vocab vision
pass. See Scripts/books/README.md for the full how-to.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 09:21:44 -05:00
Trey T ade091f108 Add Extra Study — star cards during review, study just the marked ones
Per-week "Extra Study (N)" row appears at the end of each LanGo week
section when at least one card is marked. Cards are marked from inside
VocabFlashcardView via a star next to the speaker on reveal. Marks are
keyed by the same SHA256 hash CourseReviewCard uses, so a mark and its
SRS state describe the same logical card.

ExtraStudyMark is CloudKit-synced (private DB), with uniqueness enforced
by fetch-or-create on id since CloudKit forbids @Attribute(.unique).
Skipped for textbook courses: DeckStudyView nils out the mark context
when the deck's courseName matches a TextbookChapter, and CourseView
hides the row when the active course is a textbook — so there are no
orphan marks the user can't reach.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 22:38:39 -05:00
Trey T 05a0cc0d17 Issue #32 cleanup — drop the last 5 mis-oriented vocab pairs
Two small fixes after the LLM-vision pass:

1. merge_pdf_into_book.py — when the LLM classifies an image as 'hybrid'
   but extracts zero pairs (e.g., a conjugation table whose only English
   text is on the section header that was excluded by the prompt rules),
   respect that decision instead of falling through to the bbox/heuristic
   pipeline. Previously: 1 chapter-2 estar conjugation table generated
   4 bad pairs from the heuristic fallback.

2. fix_vocab.py language_score — recognize Spanish present-perfect
   ('he tenido', 'He andado por este pueblo') as Spanish. The classifier
   was treating the auxiliary 'he'/'has'/'ha' as English subject pronouns,
   producing false-positive mis-orientation flags on 4 chapter-15/20/23
   present-perfect example tables.

Result: mis-oriented vocab pairs across the book go from 5 → 0.
textbookDataVersion bumped to 14.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:52:53 -05:00
Trey T f368c24ad6 Fixes #32 — LLM vision pass for vocab pairs, fixes scrambled English/Spanish
The bbox-OCR pipeline mis-paired ~114 vocab tables across the book — the
chapter 7 "Other Idioms" image (issue #32) being the most visible.
Three failure modes were collapsing the data:
  1) classifier blind to subject pronouns ("yo", "I", etc.)
  2) right-then-left OCR reads on 2-col tables
  3) Y-cluster drift on multi-line cells in 4-col layouts

Replaced the entire vocab-extraction tier with a Claude vision pass over
all 931 vocab images. Output is keyed by image with three classifications:
  - pair_table       (extract all Spanish↔English pairs)
  - reference_only   (Spanish-only conjugation tables — no pairs, UI shows
                      the flat OCR lines as a reference list instead)
  - hybrid           (some header pairs + reference content beneath; only
                      the genuine pairs become cards)

merge_pdf_into_book.py now picks pair source by priority:
  llm-vision → bounding-box OCR → block-alternation heuristic.

Numbers (across the whole book):
  - mis-oriented tables: 114 → 5
  - quarantined cards:   250 → 2
  - extracted pairs:     2832 → 4569

textbookDataVersion bumped to 13. Per-batch agent outputs gitignored
under Conjuga/Scripts/textbook/paired_vocab_llm/ — only the merged
paired_vocab_llm.json (also gitignored) is needed to rebuild.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:48:04 -05:00
Trey T 90aea92fba Fix Full Table eligibility — accept ordinary verbs, reject orto
The eligibility filter required every form's regularity tag to equal
"regular", but the data uses four labels:
  - regular   (179 forms — curated paradigm verbs)
  - ordinary  (50,992 forms — pattern-following verbs like hablar, comer)
  - irregular (8,653)
  - orto      (176 — orthographic spelling changes like busqué)

Result was a 27-combo eligible pool, ~26 of which were -ir verbs in
present tense — every Full Table prompt landed on the same handful of
verbs.

Pulled the rule into a SharedModels function (FullTableEligibility) so
it's testable in isolation. Accepts "regular" + "ordinary" (both mean
"follows the pattern"); rejects "irregular" and "orto". 9 unit tests
cover the matrix including edge cases (incomplete forms, mixed labels,
unknown values).

PracticeSessionService.makePromptIfFullyRegular now delegates to
FullTableEligibility, sorting forms by personIndex so the regularity
array lines up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 11:26:34 -05:00
Trey t dce2cc1f51 Make Full Table level-agnostic, fix the streak system end-to-end
Full Table (issue from chat): drop the level filter — Full Table tests
regular conjugation patterns, not vocabulary recognition, so restricting
to Basic-level verbs collapsed the eligible pool to two combos
(vivir present, ir future). Pool now draws from all 1,750 verbs. Random
sampling first; if 40 attempts fail we fall through to a deterministic
shuffled scan that guarantees finding any eligible (verb, tense) combo
when one exists. Returning nil now happens only when the user's filters
genuinely produce zero eligible prompts. The view replaces its silent
blank screen with a ContentUnavailableView pointing at the settings
that need adjusting. FeatureReferenceView documents the level exception.

Streak (issue #31 follow-up): activity recording was scoped to flashcard
and Full Table reviews only, so spending an hour on textbook work,
guides, videos, or AI chat could break a "streak" that the dashboard
kept displaying as if it were intact. Three fixes:

  1. Extract ReviewStore.recordActivity(context:) — a streak-only entry
     point that any user-initiated learning action can call.
  2. Add UserProgress.validateStreakIfStale(today:context:) — resets a
     broken currentStreak to 0 immediately, called from app launch and
     dashboard appear so the displayed number is never a lie.
  3. DailyLog formatter pins POSIX locale + current timezone so the
     yyyy-MM-dd strings can't drift across locales.

Wired recordActivity into every previously-silent learning action: chat
send, story-quiz completion, textbook exercise submit, grammar exercise
completion, course-deck study finish, week test / checkpoint save,
listening + pronunciation check, cloze quiz completion, lyrics word
lookup, video stream / play / download success, sentence-builder check,
and course-vocab SRS rate (which was bypassing ReviewStore entirely).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 01:24:27 -05:00
Trey T 06b47d37cf Fix video action button wrapping + bundle textbook seed JSONs
- VideoActionsView: stack icon over title via a custom label style so
  the Stream/Download/Play row fits iPhone width without wrapping
  mid-word ("Stre am", "Down load").
- project.yml: add textbook_data.json and textbook_vocab.json to the
  Conjuga target's Copy Bundle Resources. They were missing, so
  DataLoader.seedTextbookData early-returned every launch and the
  textbook never appeared in the Book tab. Regenerated Conjuga.xcodeproj.
2026-04-24 17:32:36 -05:00
Trey t fcb907718a Re-curate videos toward preferred channels
Swap 24 tense-guide / grammar-note videos to The Language Tutor's
numbered lesson series where a matching lesson exists, filling the two
remaining gaps (ind_preterito_anterior → Lesson 65, estar-gerund-
progressive → Lesson 113). All 32 TLT picks preserved on this pass.

For the non-TLT slots, prefer BaseLang's beginner lesson series where a
topic-specific video exists: ser-vs-estar, preterite-vs-imperfect,
subjunctive-triggers, object-pronouns, conditional-if-clauses,
tener-expressions, future-vs-ir-a, possessive-adjectives,
irregular-yo-verbs, and stem-changing-verbs.

Retire both Tell Me In Spanish videos (personal-a → castellano4U,
types-of-irregular-verbs → Master IRREGULAR VERBS Complete Lesson).

Generator header note clarifies that "not available on this app" rows
are a transient yt-dlp extraction limit — videos still play when tapped
in the app via the Stream button, which opens youtube.com externally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 07:24:53 -05:00
Trey t 9c7033d1b4 Add curated-videos markdown report + generator script
youtube_videos.md lists every entry in youtube_videos.json with its
tense-guide / grammar-note id, title, channel, upload date, duration,
views, and likes (where public). Also flags the two topics with no
curated video so the gap is auditable in one place.

generate_videos_markdown.py queries yt-dlp in parallel for each unique
videoId and writes the markdown. Rerun when curation changes. One
current entry (saber-vs-conocer → j87i7MVCvIE) is now marked Private
Video — needs re-curation as a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 07:07:41 -05:00
Trey t 0a099c3fc9 Fixes #30 — Downloaded videos include audio via adaptive-stream muxing
YouTubeKit returns stream URLs only; its own README punts on the
progressive-vs-adaptive problem. Most modern YouTube videos above 360p
serve video and audio as separate DASH tracks, so the previous "any MP4"
fallback silently grabbed a video-only track and the downloaded file had
no audio.

VideoDownloadService now resolves a DownloadPlan up front. Progressive
MP4 (when one exists) still streams straight to the final destination.
Otherwise the service downloads the best MP4 video track + best M4A
audio track to temp files, then combines them into a single MP4 via
AVMutableComposition + AVAssetExportSession. Passthrough preset first
(lossless container rewrite) with a fallback to highestQuality if the
codec combination requires re-encoding. Temp files are cleaned up on
both success and failure.

DownloadStatus replaces the bare Double progress so the UI can show
per-phase labels (Video %, Audio %, Finalizing…). Muxing progress is
rendered as an indeterminate spinner since AVAssetExportSession's
progress property doesn't cross actor boundaries cleanly.

Also retires the deprecated Stream.subtype comparison in favor of
Stream.fileExtension, matching YouTubeKit's current API.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 07:00:16 -05:00
Trey t 57f945a4d3 Fixes #29 — Delete downloaded video inline on the guide detail view
The middle button in the Stream/Download/Play row now cycles through the
full download lifecycle instead of ending at a disabled "Downloaded"
checkmark. Once a video is on disk the button becomes a red, destructive
"Delete" with a trash icon; tapping presents a confirmation dialog, and
confirming removes the file + SwiftData row, flipping the button back to
"Download" and disabling Play.

Settings → Downloaded Videos retains the swipe-delete and "Delete all"
affordances for bulk management.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 06:46:51 -05:00
Trey t 5777a210cd Fixes #21 — Curated YouTube videos per guide + grammar item
Each of the 56 tense guides and grammar notes gets a curated YouTube video
attached (54 with picks, 2 silent nulls on rare / hard-to-find topics).
Users can stream in YouTube/Safari, download via YouTubeKit for offline
viewing, or play the local MP4 full-screen via AVPlayer.

YouTubeVideoStore loads the bundled youtube_videos.json at launch and serves
lookups by tense id or grammar note id. VideoDownloadService resolves the
best progressive MP4 stream off the main actor (YouTubeKit isn't Sendable),
writes to documents/videos/<videoId>.mp4, and records a DownloadedVideo row
in the local SwiftData container so the app knows what's on disk across
launches.

VideoActionsButtonRow is the unified UI for both detail views: three large
buttons — Stream (red, always enabled), Download (blue, disabled while in
flight and after completion, shows progress), Play (green, enabled only
when downloaded). Full-screen cover on tap. Settings gains a Downloaded
Videos list with swipe-delete, total-size summary, and a 500 MB warning.

Local store reset version bumped to 4 for the new DownloadedVideo schema.

Known fragility: YouTubeKit scrapes YouTube's private stream API and will
break when YouTube changes their internal format. Streaming keeps working
regardless.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 18:51:19 -05:00
Trey t 98badc98ad Fixes #28 — Curated reflexive verb list on detail + practice filter
Bundles the 100 most common reflexive verbs from spanishwithdaniel.com as a
canonical list and wires it through the UI. Compound list entries (recibirse
/ graduarse, equivocarse / confundirse) are split. Trailing prepositions and
set-phrase completions are captured as usageHint (e.g. acordarse "de",
ponerse "de acuerdo").

ReflexiveVerbStore loads the JSON at launch and exposes lookups by base
infinitive, both via @Environment for SwiftUI and a static shared instance
for services. Verbs whose bare infinitive isn't in the list skip the UI
treatment silently.

VerbDetailView shows a new Reflexive section with the reflexive infinitive,
usage hint, and English meaning when there is a match. VerbListView gains a
"Reflexive verbs only" filter alongside the existing Level and Irregularity
filters. Settings adds the same flag so it also constrains the practice
pool; PracticeSessionService applies the reflexive filter in all six pick
paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 10:56:04 -05:00
Trey t 4093b5a7f3 Fixes #27 — AI-generated example sentences on verb detail
Tapping a verb now shows six example sentences beneath the conjugation table,
one per core tense (Present, Preterite, Imperfect, Future, Present Subjunctive,
Imperative). Each example renders the tense label, Spanish sentence, and
English translation in the DeckStudyView style.

Generation uses Foundation Models with a @Generable schema that pins each
response to the requested tenseId and forces tú/nosotros subjects for
imperatives. Results are cached as JSON in the Caches directory keyed by
verb id (DictionaryService pattern); cache misses regenerate on demand.

Devices without Apple Intelligence see an inline notice instead of the
loading state. No network dependency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 09:57:32 -05:00
Trey t 5d3accb2c0 Fixes #26 — Multi-select levels and irregular-type practice filter
UserProgress gains selectedLevelsBlob and enabledIrregularCategoriesBlob
(mirrors the existing tense-blob pattern). The multi-level setter keeps the
legacy selectedLevel String in sync with the highest-ranked selection, so
widget sync, AI scenarios, and achievement checks keep working unchanged.
Legacy single-level users are migrated on first read.

Settings replaces the level Picker with per-level toggles and adds an
Irregular Types section with three toggles. Practice pool is the literal
intersection: empty levels means zero results, empty irregular categories
means no irregularity constraint.

Pure filter logic lives in SharedModels (PracticeFilter, VerbLevel.highest)
and is covered by 20 Swift Testing cases. ReferenceStore delegates so the
intersection behavior is unit-tested without a ModelContainer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 09:36:25 -05:00
Trey t 3d8cbccc4e Fixes #25 — Long-press lyric words for definition and tense
Tokenize Spanish lyric lines into a flow layout of underlined, long-pressable
words. Long-press (0.35s) opens a sheet with base form, English, part of
speech, and a Tense · person row for verbs. Unknown words silently no-op.
English gloss lines remain untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 09:18:15 -05:00
Trey t 8e1c9b6bf1 Make textbook data self-heal after widget schema wipes
Root cause of the repeatedly-disappearing textbook: both widget timeline
providers were opening the shared local SwiftData store with a schema
that omitted TextbookChapter. On each widget refresh SwiftData
destructively migrated the store to match the widget's narrower schema,
dropping the ZTEXTBOOKCHAPTER rows (and sometimes the table itself).
The app then re-created an empty table on next open, but
refreshTextbookDataIfNeeded skipped re-seeding because the UserDefaults
version flag was already current — leaving the store empty indefinitely.

Three changes:

1. Widgets (CombinedWidget, WordOfDayWidget): added TextbookChapter to
   both schema arrays so they match the main app. Widget refreshes will
   no longer drop the entity.

2. DataLoader.refreshTextbookDataIfNeeded: trigger now considers BOTH
   the version flag and the actual on-disk row count. If rows are
   missing for any reason (past wipes, future subset-schema openers,
   corruption), the next launch re-seeds. Eliminates the class of bug
   where a version flag lies about what's really in the store.

3. StoreInspector: reports ZTEXTBOOKCHAPTER row count alongside the
   other entities so we can confirm state from logs.

Bumped textbookDataVersion to 12 so devices that were stuck in the
silent-failure state re-seed on next launch regardless of prior flag
value.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 23:31:14 -05:00
Trey t d9ddaa4902 Fixes #23 — Add irregularity filter to the Verbs list
The toolbar Filter menu now has two sections:
- Level (existing)
- Irregularity: Any Irregular / Spelling Change / Stem Change /
  Unique Irregular

Filters combine, so "Basic" + "Unique Irregular" narrows to the
foundational ser/ir/haber-class verbs. Categories are derived at load
time from existing IrregularSpan rows using the same spanType ranges
already used by PracticeSessionService (1xx spelling, 2xx stem, 3xx
unique), so no schema or data changes are required.

UI additions:
- Per-row icons (star / arrows / I-beam) show each verb's
  irregularity categories at a glance, tinted by type.
- When any filter is active, a chip bar appears under the search
  field showing the active filters (tap to clear) and the resulting
  verb count.
- Filter toolbar icon fills when any filter is applied.

Data coverage: 614 / 1750 verbs are flagged irregular — 411 spelling,
275 stem-change, 67 unique — consistent with canonical lists from
SpanishDict, Lawless Spanish, and Wikipedia.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 23:29:46 -05:00
Trey t cdf1e05c4c Fixes #22 — Skip irregular verbs in Full Table practice
randomFullTablePrompt now rejects any (verb, tense) combo where any
form has regularity != "regular" and keeps retrying (40 attempts) until
a fully-regular combo is picked. Full Table practice is meant for
drilling regular conjugation patterns — irregular tables belong in the
Irregularity Drills mode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 22:49:46 -05:00
Trey t 455df18dad Stop silent save failures from locking out textbook re-seeds
Two bugs were causing chapters to disappear on every relaunch:

1. seedTextbookData used `try? context.save()` (swallowing errors) and
   returned `inserted > 0`, so a failed save still reported success.
   Callers then bumped UserDefaults textbookDataVersion and subsequent
   launches skipped the re-seed entirely — with no rows on disk.

2. refreshTextbookDataIfNeeded wiped chapters via the batch-delete API
   `context.delete(model: TextbookChapter.self)`, which hits the store
   directly without clearing the context's .unique-id index. Re-inserting
   chapters with the same ids could then throw a unique-constraint error
   on save — also silently eaten by `try?`.

Fixes:
- seedTextbookData now uses do/catch around save(), returns false on
  error, and verifies persistence via fetchCount before returning true.
- refreshTextbookDataIfNeeded fetches and deletes chapters individually
  so the context tracks the deletion cleanly; wipe save is also now
  checked and bails early on failure.
- Bumped textbookDataVersion to 11 so devices poisoned by the previous
  silent-failure path retry on next launch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 22:33:23 -05:00
Trey t 3c5600f562 Fix textbook section missing on installs that predate bundled JSON
Earlier launches (before cd491bd bundled textbook_data.json) ran
seedTextbookData, got "not bundled — skipping", and still bumped
UserDefaults textbookDataVersion to 9. Subsequent launches then
short-circuited in refreshTextbookDataIfNeeded and never re-seeded,
so ZTEXTBOOKCHAPTER stayed empty and the Course tab hid the section.

- seedTextbookData now returns Bool (true only when chapters inserted).
- Both call sites only write the version key on success, so a missing
  or unparseable bundle no longer locks us out of future retries.
- Bumped textbookDataVersion to 10 to force existing installs to
  re-seed on next launch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 09:39:47 -05:00
Trey T 5f90a01314 Render textbook vocab as paired Spanish→English grid
Previously the chapter reader showed vocab tables as a flat list of OCR
lines — because Vision reads columns top-to-bottom, the Spanish column
appeared as one block followed by the English column, making pairings
illegible.

Now every vocab table renders as a 2-column grid with Spanish on the
left and English on the right. Supporting changes:

- New ocr_all_vocab.swift: bounding-box OCR over all 931 vocab images,
  cluster lines into rows by Y-coordinate, split rows by largest X-gap,
  detect 2- / 3- / 4-column layouts automatically. ~2800 pairs extracted
  this pass vs ~1100 from the old block-alternation heuristic.
- merge_pdf_into_book.py now prefers bounding-box pairs when present,
  falls back to the heuristic, embeds the resulting pairs as
  vocab_table.cards in book.json.
- DataLoader passes cards through to TextbookBlock on seed.
- TextbookChapterView renders cards via SwiftUI Grid (2 cols).
- fix_vocab.py quarantine rule relaxed — only mis-pairs where both
  sides are clearly the same language are removed. "unknown" sides
  stay (bbox pipeline already oriented them correctly).

Textbook card count jumps from 1044 → 3118 active pairs.
textbookDataVersion bumped to 9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 15:58:41 -05:00
Trey T cd491bd695 Bundle textbook JSON so fresh clones build without re-running pipeline
The pbxproj references textbook_data.json and textbook_vocab.json as Copy
Bundle Resources, so xcodebuild fails if they're missing. Committing the
generated output keeps the repo self-sufficient — regenerate via
Conjuga/Scripts/textbook/run_pipeline.sh when content changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 15:28:45 -05:00
Trey t df96a9e540 Merge branch 'main' of gitea.treytartt.com:admin/Spanish 2026-04-19 15:23:01 -05:00
Trey t c73762ab9f Add tappable word lookup to chat bubbles
Assistant messages now render each word as a button. Tap shows a sheet
with base form, English translation, and part of speech. Dictionary
lookup first; falls back to Foundation Models (@Generable ChatWordInfo)
for words not in the local dictionary. Results cached per-session.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 15:22:59 -05:00
Trey t f809bc2a1d Fix speech recognition crash from audio format mismatch
Switch audio session to .record-only, use nil tap format so the system
picks a compatible format, and route through AVAudioEngine with a 4096
buffer. Avoids the mDataByteSize(0) assertion seen on some devices.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 15:22:45 -05:00
Trey T 63dfc5e41a Add textbook reader, exercise grading, stem-change toggle, extraction pipeline
Major changes:
- Textbook UI: chapter list, reader, and interactive exercise view (keyboard
  + Apple Pencil) surfaced under the Course tab. 30 chapters, 251 exercises.
- Stem-change conjugation toggle on Week 4 flashcard decks (E-IE, E-I, O-UE).
  Uses existing VerbForm + IrregularSpan data to render highlighted present
  tense conjugations inline.
- Deterministic on-device answer grader with partial credit (correct / close
  for accent-stripped or single-char-typo / wrong). 11 unit tests cover it.
- SharedModels: TextbookChapter (local), TextbookExerciseAttempt (cloud-
  synced), AnswerGrader helpers. Bumped schema.
- DataLoader: textbook seeder (version 8) + refresh helpers that preserve
  LanGo course decks when textbook data is re-seeded.
- Local extraction pipeline in Conjuga/Scripts/textbook/ — XHTML chapter
  parser, answer-key parser, macOS Vision image OCR + PDF page OCR, merger,
  NSSpellChecker validator, language-aware auto-fixer, and repair pass that
  re-pairs quarantined vocab rows using bounding-box coordinates.
- UI test target (ConjugaUITests) with three tests: end-to-end textbook
  flow, all-chapters screenshot audit, and stem-change toggle verification.

Generated textbook content (textbook_data.json, textbook_vocab.json) and
third-party source files are gitignored — re-run Scripts/textbook/run_pipeline.sh
locally to regenerate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 15:12:55 -05:00
Trey t 5ba76a947b Bump courseDataVersion to 7 after merging Gitea course data updates
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 08:41:55 -05:00
Trey t bb596b19bd Merge branch 'main' of gitea.treytartt.com:admin/Spanish 2026-04-16 08:40:17 -05:00
Trey t 47a7871c38 Add 13 new grammar notes with 1010 exercises from video extraction
Scraped a 4h Spanish fundamentals YouTube video (transcript + OCR on
14810 frames), extracted structured content across 52 chapters, and
generated fill-in-the-blank quizzes for every grammar topic.

- 13 new GrammarNote entries (articles, possessives, demonstratives,
  greetings, poder, al/del, prepositional pronouns, irregular yo,
  stem-changing, stressed possessives, present/future perfect, present
  indicative conjugation)
- 1010 generated exercises across all 36 grammar notes (new + existing)
- Fix tense guide parser to handle unnumbered *Usages* blocks
- Rewrite 6 broken tense guide bodies (imperative, subj pluperfect,
  subj future) with numbered usage format
- Bump courseDataVersion 5→6 with TenseGuide refresh on upgrade
- Add docs/spanish-fundamentals/ with raw transcripts, polished notes,
  structured JSON, and exercise data

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 08:40:05 -05:00
Trey T 5b69f3b630 Fixes #19 — Add English translations to exceptional yo form flashcards
Cards now show "tengo — I have" instead of just "tengo", so learners
see the English meaning alongside the Spanish yo form. Bumps course
data version to 6 to trigger re-seed on next launch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 08:02:40 -05:00
Trey t ff4f906128 Fix crash from zero-length audio buffers in speech recognition
Guard against empty audio buffers before appending to speech
recognition request — AVAudioBuffer asserts non-zero data size.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 19:00:51 -05:00
Trey t b48e935231 Expand grammar exercises to 100 sentences each, pick 10 random per session
- Ser vs Estar: 100 sentences
- Por vs Para: 100 sentences
- Preterite vs Imperfect: 100 sentences
- Subjunctive Triggers: 100 sentences
- Personal A: 100 sentences

Each session randomly selects 10 from the pool for variety.

Closes #15

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:55:36 -05:00