package asr import ( "strings" "unicode" ) // looksLikePromptLeak reports whether text appears to be the model echoing // the --prompt biasing content back verbatim instead of genuinely // transcribing the segment's audio — a known failure mode of // prompt-conditioned generation, confirmed via real testing against // qwen_asr (see transcriptor-ui/CLAUDE.md, "Glossary prompt-leakage bug"): // a whole segment came back as literally the glossary terms, twice, on two // separate 5-minute test runs, even after switching to the tool's // recommended "Preserve spelling: ..." prompt framing (which reduces but // does not eliminate the risk). // // Heuristic: if most of text's words also appear among prompt's words, and // there are enough of them to rule out a legitimate short mention of one // or two glossary terms in real speech, treat it as a leak. Both observed // real leaks were near-total reproductions of the prompt's term list, so a // strict threshold (80% word overlap, at least 4 words) catches them // without plausibly false-flagging a normal sentence that happens to // mention "Claude" or "Mistral" once — normal speech has enough other // words to keep the overlap ratio well below that. func looksLikePromptLeak(text, prompt string) bool { if prompt == "" || text == "" { return false } textWords := normalizeWords(text) if len(textWords) < 4 { return false } promptWords := wordSet(normalizeWords(prompt)) matched := 0 for _, w := range textWords { if promptWords[w] { matched++ } } return float64(matched)/float64(len(textWords)) >= 0.8 } // normalizeWords lowercases and splits on anything that isn't a letter or // digit — unicode.IsLetter (not an ASCII-only check) so accented // characters (French: é, è, ï, ç, ...) stay part of their word instead of // being split into fragments. func normalizeWords(s string) []string { return strings.FieldsFunc(strings.ToLower(s), func(r rune) bool { return !unicode.IsLetter(r) && !unicode.IsDigit(r) }) } func wordSet(words []string) map[string]bool { set := make(map[string]bool, len(words)) for _, w := range words { set[w] = true } return set }