Filter phonetic glosses from Complete the Sentence quiz
Examples shorter than 4 words (like pronunciation guides "discutir(dees-koo-teer)") are now rejected by both isBlankResolvable and buildQuestion. The engine only picks real multi-word sentences for the quiz prompt. Every card already has at least one real sentence alongside its phonetic entries, so no data regeneration is needed — the filter alone fixes the issue. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -60,6 +60,7 @@ public struct SentenceQuizEngine {
|
|||||||
public static func buildQuestion(for card: VocabCard, exampleIndex: Int) -> Question? {
|
public static func buildQuestion(for card: VocabCard, exampleIndex: Int) -> Question? {
|
||||||
guard exampleIndex >= 0, exampleIndex < card.examplesES.count else { return nil }
|
guard exampleIndex >= 0, exampleIndex < card.examplesES.count else { return nil }
|
||||||
let sentence = card.examplesES[exampleIndex]
|
let sentence = card.examplesES[exampleIndex]
|
||||||
|
guard sentence.split(separator: " ").count >= minimumWordCount else { return nil }
|
||||||
let sentenceEN = exampleIndex < card.examplesEN.count ? card.examplesEN[exampleIndex] : ""
|
let sentenceEN = exampleIndex < card.examplesEN.count ? card.examplesEN[exampleIndex] : ""
|
||||||
|
|
||||||
let storedBlank = exampleIndex < card.examplesBlanks.count ? card.examplesBlanks[exampleIndex] : ""
|
let storedBlank = exampleIndex < card.examplesBlanks.count ? card.examplesBlanks[exampleIndex] : ""
|
||||||
@@ -79,8 +80,13 @@ public struct SentenceQuizEngine {
|
|||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
|
/// Minimum number of whitespace-separated tokens for an example to count as
|
||||||
|
/// a real sentence (filters out phonetic glosses like "discutir(dees-koo-teer)").
|
||||||
|
public static let minimumWordCount = 4
|
||||||
|
|
||||||
private static func isBlankResolvable(card: VocabCard, exampleIndex: Int) -> Bool {
|
private static func isBlankResolvable(card: VocabCard, exampleIndex: Int) -> Bool {
|
||||||
let sentence = card.examplesES[exampleIndex]
|
let sentence = card.examplesES[exampleIndex]
|
||||||
|
guard sentence.split(separator: " ").count >= minimumWordCount else { return false }
|
||||||
let storedBlank = exampleIndex < card.examplesBlanks.count ? card.examplesBlanks[exampleIndex] : ""
|
let storedBlank = exampleIndex < card.examplesBlanks.count ? card.examplesBlanks[exampleIndex] : ""
|
||||||
if !storedBlank.isEmpty, sentence.range(of: storedBlank, options: .caseInsensitive) != nil {
|
if !storedBlank.isEmpty, sentence.range(of: storedBlank, options: .caseInsensitive) != nil {
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ struct SentenceQuizEngineTests {
|
|||||||
examplesES: [
|
examplesES: [
|
||||||
"Ella prepara la cena.",
|
"Ella prepara la cena.",
|
||||||
"Los niños van al parque.",
|
"Los niños van al parque.",
|
||||||
"Quiero comer ahora."
|
"Quiero comer algo ahora."
|
||||||
],
|
],
|
||||||
examplesEN: ["", "", ""]
|
examplesEN: ["", "", ""]
|
||||||
)
|
)
|
||||||
@@ -66,6 +66,26 @@ struct SentenceQuizEngineTests {
|
|||||||
#expect(SentenceQuizEngine.resolvableIndices(for: card) == [2])
|
#expect(SentenceQuizEngine.resolvableIndices(for: card) == [2])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test("Phonetic glosses are rejected (too few words)")
|
||||||
|
func phoneticGlossRejected() {
|
||||||
|
let card = VocabCard(
|
||||||
|
front: "discutir",
|
||||||
|
back: "to discuss",
|
||||||
|
deckId: "d",
|
||||||
|
examplesES: [
|
||||||
|
"discutir(dees-koo-teer)",
|
||||||
|
"INTRANSITIVE VERB",
|
||||||
|
"Los amigos van a discutir el tema."
|
||||||
|
],
|
||||||
|
examplesEN: ["", "", "The friends are going to discuss the topic."]
|
||||||
|
)
|
||||||
|
// Only index 2 is a real sentence (≥4 words AND contains the target)
|
||||||
|
#expect(SentenceQuizEngine.hasValidSentence(for: card))
|
||||||
|
#expect(SentenceQuizEngine.resolvableIndices(for: card) == [2])
|
||||||
|
// Phonetic entry at index 0 returns nil
|
||||||
|
#expect(SentenceQuizEngine.buildQuestion(for: card, exampleIndex: 0) == nil)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - buildQuestion (deterministic)
|
// MARK: - buildQuestion (deterministic)
|
||||||
|
|
||||||
@Test("Builds question from substring match, preserves original casing")
|
@Test("Builds question from substring match, preserves original casing")
|
||||||
@@ -151,12 +171,12 @@ struct SentenceQuizEngineTests {
|
|||||||
front: "hola",
|
front: "hola",
|
||||||
back: "hello",
|
back: "hello",
|
||||||
deckId: "d",
|
deckId: "d",
|
||||||
examplesES: ["Hola, ¿cómo estás?"],
|
examplesES: ["Hola amiga, ¿cómo estás hoy?"],
|
||||||
examplesEN: ["Hello, how are you?"]
|
examplesEN: ["Hello friend, how are you today?"]
|
||||||
)
|
)
|
||||||
let question = SentenceQuizEngine.buildQuestion(for: card, exampleIndex: 0)
|
let question = SentenceQuizEngine.buildQuestion(for: card, exampleIndex: 0)
|
||||||
#expect(question?.blankWord == "Hola")
|
#expect(question?.blankWord == "Hola")
|
||||||
#expect(question?.displayTemplate == "_____, ¿cómo estás?")
|
#expect(question?.displayTemplate == "_____ amiga, ¿cómo estás hoy?")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test("Blanks phrase cards when target front contains spaces")
|
@Test("Blanks phrase cards when target front contains spaces")
|
||||||
@@ -208,14 +228,14 @@ struct SentenceQuizEngineTests {
|
|||||||
back: "to eat",
|
back: "to eat",
|
||||||
deckId: "d",
|
deckId: "d",
|
||||||
examplesES: [
|
examplesES: [
|
||||||
"Ella prepara la cena.", // unresolvable
|
"Ella prepara la cena.", // unresolvable (no "comer")
|
||||||
"Los niños van al parque.", // unresolvable
|
"Los niños van al parque.", // unresolvable
|
||||||
"Quiero comer ahora.", // resolvable (substring)
|
"Quiero comer algo rico ahora.", // resolvable (substring, ≥4 words)
|
||||||
"El perro come su comida." // unresolvable — note "come" is a substring but "comer" is not
|
"El perro come su comida diaria." // unresolvable — "come" but not "comer"
|
||||||
],
|
],
|
||||||
examplesEN: ["", "", "", ""]
|
examplesEN: ["", "", "", ""]
|
||||||
)
|
)
|
||||||
// Only index 2 is resolvable (contains "comer" literally)
|
// Only index 2 is resolvable (contains "comer" literally and has ≥4 words)
|
||||||
for _ in 0..<25 {
|
for _ in 0..<25 {
|
||||||
let q = SentenceQuizEngine.buildQuestion(for: card)
|
let q = SentenceQuizEngine.buildQuestion(for: card)
|
||||||
#expect(q?.exampleIndex == 2)
|
#expect(q?.exampleIndex == 2)
|
||||||
@@ -242,8 +262,8 @@ struct SentenceQuizEngineTests {
|
|||||||
front: "comer",
|
front: "comer",
|
||||||
back: "to eat",
|
back: "to eat",
|
||||||
deckId: "d",
|
deckId: "d",
|
||||||
examplesES: ["Yo como.", "Tú comes."],
|
examplesES: ["Yo como mucho pan.", "Tú comes en casa."],
|
||||||
examplesEN: ["I eat.", "You eat."],
|
examplesEN: ["I eat a lot of bread.", "You eat at home."],
|
||||||
examplesBlanks: ["como"] // only covers index 0
|
examplesBlanks: ["como"] // only covers index 0
|
||||||
)
|
)
|
||||||
// Index 0: stored blank match
|
// Index 0: stored blank match
|
||||||
@@ -260,8 +280,8 @@ struct SentenceQuizEngineTests {
|
|||||||
front: "perro",
|
front: "perro",
|
||||||
back: "dog",
|
back: "dog",
|
||||||
deckId: "d",
|
deckId: "d",
|
||||||
examplesES: ["El perro ladra."],
|
examplesES: ["El perro ladra todo el día."],
|
||||||
examplesEN: ["The dog barks."]
|
examplesEN: ["The dog barks all day."]
|
||||||
)
|
)
|
||||||
let q = SentenceQuizEngine.buildQuestion(for: card, exampleIndex: 0)
|
let q = SentenceQuizEngine.buildQuestion(for: card, exampleIndex: 0)
|
||||||
#expect(q?.displayTemplate.contains(SentenceQuizEngine.blankMarker) == true)
|
#expect(q?.displayTemplate.contains(SentenceQuizEngine.blankMarker) == true)
|
||||||
|
|||||||
Reference in New Issue
Block a user