feat: add lyrics language validation and bounded repair - #137
Conversation
Reject accidental English, CJK or Arabic contamination in Spanish lyrics while keeping structural tags and quoted verbatim spans. Repair only strips unrequested foreign scripts; it never invents a translation.
PR Review — Loreframe StudioRisk: low Automated review from Findings
Changed files
CONTRIBUTING checklist
Posted by the repo PR review workflow. Re-runs on each push to the PR. |
Code healthQuality score: 49.3/100Higher is better. The score is a trend dashboard; the independent ratchet below remains the CI gate.
Change vs PR base: +0.1 points.
Markdown, JSON catalogs and tests are out of this table. Only Most complex functions
Trend vs baseline
Warnings
Ratchet passed. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Language aliases skip real names
- Python and TypeScript now share the same alias table plus token matching so names like “Español de España” canonicalize to es, while two-letter tokens are skipped so “en español” is not treated as English.
Or push these changes by commenting:
@cursor push 0bc2a4b2b6
Preview (0bc2a4b2b6)
diff --git a/app/services/lyrics_language.py b/app/services/lyrics_language.py
--- a/app/services/lyrics_language.py
+++ b/app/services/lyrics_language.py
@@ -64,7 +64,16 @@
def canonical_lyrics_language(value: str) -> str:
key = _folded(value)
- return LANGUAGE_ALIASES.get(key) or LANGUAGE_ALIASES.get(key.split("-")[0], "")
+ exact = LANGUAGE_ALIASES.get(key) or LANGUAGE_ALIASES.get(key.split("-")[0], "")
+ if exact:
+ return exact
+ # Story Lab names such as "Español de España" are not exact aliases.
+ # Skip 2-letter tokens so "en español" does not become English.
+ for token in re.findall(r"[a-z]+", key):
+ mapped = LANGUAGE_ALIASES.get(token)
+ if mapped and len(token) > 2:
+ return mapped
+ return ""
def _protected_texts(segments: Sequence[Mapping[str, Any]] | None) -> list[str]:
diff --git a/tests/test_lyrics_language.py b/tests/test_lyrics_language.py
--- a/tests/test_lyrics_language.py
+++ b/tests/test_lyrics_language.py
@@ -6,6 +6,7 @@
from services.lyrics_language import (
assert_lyrics_language,
+ canonical_lyrics_language,
repair_lyrics_language,
validate_lyrics_language,
)
@@ -57,6 +58,15 @@
assert any("English" in reason for reason in report["reasons"])
+def test_story_lab_spoken_language_name_is_scored():
+ assert canonical_lyrics_language("Español de España") == "es"
+ assert canonical_lyrics_language("en español") == "es"
+ report = validate_lyrics_language(ENGLISH_CHORUS, "Español de España")
+ assert report["ok"] is False
+ assert report["language_mismatch"] is True
+ assert any("English" in reason for reason in report["reasons"])
+
+
def test_chinese_and_arabic_fail_spanish_lyrics():
chinese = validate_lyrics_language(CJK, "castellano")
arabic = validate_lyrics_language(ARABIC, "es")
diff --git a/ui/src/lib/lyricsLanguageGuard.ts b/ui/src/lib/lyricsLanguageGuard.ts
--- a/ui/src/lib/lyricsLanguageGuard.ts
+++ b/ui/src/lib/lyricsLanguageGuard.ts
@@ -41,14 +41,26 @@
strippedSpans: { script: string; text: string }[]
}
+const LANGUAGE_ALIASES: Record<string, string> = {
+ es: 'es', espanol: 'es', español: 'es', castellano: 'es',
+ spanish: 'es', 'es-es': 'es', 'es-mx': 'es',
+ en: 'en', english: 'en', ingles: 'en', inglés: 'en',
+}
+
function folded(value: string): string {
return value.normalize('NFKD').replace(/[\u0300-\u036f]/g, '').toLocaleLowerCase().trim()
}
export function canonicalLyricsLanguage(value: string): string {
const key = folded(value)
- if (key.startsWith('es') || key.includes('spanish') || key.includes('espanol') || key.includes('castellano')) return 'es'
- if (key.startsWith('en') || key.includes('english') || key.includes('ingles')) return 'en'
+ const exact = LANGUAGE_ALIASES[key] || LANGUAGE_ALIASES[key.split('-')[0]] || ''
+ if (exact) return exact
+ // Story Lab names such as "Español de España" are not exact aliases.
+ // Skip 2-letter tokens so "en español" does not become English.
+ for (const token of key.match(/[a-z]+/g) || []) {
+ const mapped = LANGUAGE_ALIASES[token]
+ if (mapped && token.length > 2) return mapped
+ }
return ''
}
diff --git a/ui/tests/lyricsLanguageGuard.test.ts b/ui/tests/lyricsLanguageGuard.test.ts
--- a/ui/tests/lyricsLanguageGuard.test.ts
+++ b/ui/tests/lyricsLanguageGuard.test.ts
@@ -1,6 +1,6 @@
import assert from 'node:assert/strict'
import test from 'node:test'
-import { repairLyricsLanguage, validateLyricsLanguage } from '../src/lib/lyricsLanguageGuard'
+import { canonicalLyricsLanguage, repairLyricsLanguage, validateLyricsLanguage } from '../src/lib/lyricsLanguageGuard'
const SPANISH_OK = `[Verse]
En la red despierta el sysadmin.
@@ -28,6 +28,17 @@
assert.equal(report.languageMismatch, true)
})
+test('Story Lab spoken-language names score as Spanish', () => {
+ const report = validateLyricsLanguage(
+ '[Verse]\nEn la red despierta el sysadmin y la noche canta.\n[Chorus]\nThe server fights through the night and we sing for our network.',
+ 'Español de España',
+ )
+ assert.equal(canonicalLyricsLanguage('Español de España'), 'es')
+ assert.equal(canonicalLyricsLanguage('en español'), 'es')
+ assert.equal(report.ok, false)
+ assert.equal(report.languageMismatch, true)
+})
+
test('Chinese or Arabic runs fail Spanish lyrics', () => {
assert.equal(validateLyricsLanguage('[Verse]\nEn la red.\n[Chorus]\n夜晚在服务器里唱歌', 'castellano').ok, false)
assert.equal(validateLyricsLanguage('[Verse]\nEn la red.\n[Chorus]\nالليل يغني', 'es').ok, false)You can send follow-ups to the cloud agent here.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 7cfa436. Configure here.
|
|
||
| def canonical_lyrics_language(value: str) -> str: | ||
| key = _folded(value) | ||
| return LANGUAGE_ALIASES.get(key) or LANGUAGE_ALIASES.get(key.split("-")[0], "") |
There was a problem hiding this comment.
Language aliases skip real names
High Severity
canonical_lyrics_language only hits exact folded aliases, so Story Lab values such as Español de España become an empty code and validate_lyrics_language returns ok without scoring. canonicalLyricsLanguage uses a different startsWith heuristic, so the Python and TypeScript guards disagree on the same input.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 7cfa436. Configure here.
There was a problem hiding this comment.
Fixed in 8c696b2. canonical_lyrics_language now uses the same startswith/substring heuristic as the TypeScript guard, so labels such as Español de España score as Spanish instead of skipping validation. The regional-name case is covered in tests/test_lyrics_language.py and ui/tests/lyricsLanguageGuard.test.ts.
Español de España and similar Story Lab labels must run the Spanish contamination checks instead of skipping them.



Resumen ejecutivo
Qué cambia
Añade un validador de idioma para letras cantadas (Python y TypeScript). Una letra en español no puede mezclar inglés, chino o árabe por accidente. Los fragmentos entrecomillados se conservan. La reparación acotada borra scripts no pedidos; no traduce.
Para qué sirve
El smoke real ya demostró que un WAV válido no prueba fidelidad lingüística. Este contrato se puede ejecutar en CI sin GPU.
Impacto para el usuario
Ninguno todavía: la librería no está cableada a Generate. El cableado espera a que #135 suelte Story Lab / write-song.
Riesgo
Estado
Summary
app/services/lyrics_language.pywith validate/repair/assertui/src/lib/lyricsLanguageGuard.tsLYRICS_LANGUAGE.mdOverview
User language, lyric language and technical prompt stay separate.
[Verse]tags are allowed. Protected verbatim spans are masked before scoring.Detailed changes
Backend
New module only. No launch, no LLM router, no FastAPI.
UI and Wizard
New pure helper + tests.
songLanguage.tsandagentActions.tsuntouched.Data, provenance and compatibility
Additive documentation on the domain model page.
Files and ownership
Owned: lyrics_language.py, lyricsLanguageGuard.ts, tests, LYRICS_LANGUAGE.md.
Forbidden files not touched:
_launch_runtime.py,useStore.ts,agentActions.ts, StoryLabPanel, llm.py.Validation
pytest tests/test_lyrics_language.py— 9 passedlyricsLanguageGuard.test.ts— 7 passedbash scripts/validate_local.sh— PASSgit diff --checkCode quality
CI and review
Coste de la tarea
Notes and limitations
Repair never translates an English chorus into Spanish. If English remains,
okstays false and generate must not proceed once wired.Follow-up work
Wire into write-song and Story generate after #135. Optional: call from the opt-in real smoke.
Checklist
Note
Low Risk
Additive library and docs only; no runtime or API behavior changes until follow-up wiring.
Overview
Introduces a lyrics language contract as standalone Python and TypeScript libraries (not yet hooked into Story Lab generate or
write-song; wiring waits on #135).validate_lyrics_language/validateLyricsLanguagecheck sung text against the requested language: English[Verse]-style tags are ignored; protected dialogue/quote spans are masked so intentional English in a Spanish song can pass. For Spanish, heuristics flag accidental English lines, missing Spanish signal, and unrequested Han/Arabic/Cyrillic/Hangul/Kana runs; English requests reject foreign scripts only.repair_lyrics_language/repairLyricsLanguageonly strip those foreign-script runs and normalize whitespace—they do not translate (e.g. an English chorus in a Spanish song stays failing).assert_lyrics_languageraises with a Spanish error message when validation still fails after optional repair. Instrumental mode rejects non-empty vocal lyrics.Adds
LYRICS_LANGUAGE.md, a short pointer in the domain/provenance doc, and provider-free CI tests mirroring the same scenarios in Python and UI.Reviewed by Cursor Bugbot for commit 7cfa436. Configure here.