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>
This commit is contained in:
Trey T
2026-04-19 15:58:41 -05:00
parent cd491bd695
commit 5f90a01314
9 changed files with 17619 additions and 1148 deletions

View File

@@ -0,0 +1,53 @@
import XCTest
final class VocabGridTests: XCTestCase {
override func setUpWithError() throws {
continueAfterFailure = false
}
/// Verifies the chapter reader renders vocab tables as a paired SpanishEnglish grid.
func testChapter4VocabGrid() throws {
let app = XCUIApplication()
app.launchArguments += ["-onboardingComplete", "YES"]
app.launch()
app.tabBars.buttons["Course"].tap()
let textbookRow = app.buttons.containing(NSPredicate(
format: "label CONTAINS[c] 'Complete Spanish'"
)).firstMatch
XCTAssertTrue(textbookRow.waitForExistence(timeout: 5))
textbookRow.tap()
let ch4 = app.buttons["textbook-chapter-row-4"]
XCTAssertTrue(ch4.waitForExistence(timeout: 3))
ch4.tap()
attach(app, name: "01-ch4-top")
// Tap the first vocab disclosure "Vocabulary (N items)"
let vocabButton = app.buttons.matching(NSPredicate(
format: "label BEGINSWITH 'Vocabulary ('"
)).firstMatch
XCTAssertTrue(vocabButton.waitForExistence(timeout: 3))
vocabButton.tap()
Thread.sleep(forTimeInterval: 0.4)
attach(app, name: "02-ch4-vocab-open")
// Scroll a little and screenshot a deeper vocab numbers table is
// typically a few screens down in chapter 4.
app.swipeUp(velocity: .fast)
app.swipeUp(velocity: .fast)
attach(app, name: "03-ch4-deeper")
}
private func attach(_ app: XCUIApplication, name: String) {
let s = app.screenshot()
let a = XCTAttachment(screenshot: s)
a.name = name
a.lifetime = .keepAlways
add(a)
}
}