Skip to content

feat: add lyrics language validation and bounded repair - #137

Merged
IAnMove merged 2 commits into
mainfrom
feat/lyrics-language-guard
Sep 5, 2026
Merged

feat: add lyrics language validation and bounded repair#137
IAnMove merged 2 commits into
mainfrom
feat/lyrics-language-guard

Conversation

@IAnMove

@IAnMove IAnMove commented Sep 4, 2026

Copy link
Copy Markdown
Owner

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

  • Bajo

Estado

  • Listo para revisión
  • Validación local verde. CI pendiente tras el alta.

Summary

  • add app/services/lyrics_language.py with validate/repair/assert
  • add matching ui/src/lib/lyricsLanguageGuard.ts
  • tests cover Spanish OK, English chorus fail, CJK/Arabic fail, protected quotes, non-translating repair
  • docs: LYRICS_LANGUAGE.md

Overview

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.ts and agentActions.ts untouched.

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 passed
  • UI lyricsLanguageGuard.test.ts — 7 passed
  • bash scripts/validate_local.sh — PASS
  • git diff --check
  • Real media smoke: N/A

Code quality

  • Score: pending CI
  • Historical baseline not refreshed

CI and review

  • CI: pending
  • Cursor/Bugbot: pending
  • Human review: pending — do not merge automatically

Coste de la tarea

  • Tests simulados: 0 tokens externos
  • Tests reales: N/A
  • Llamadas LLM externas: 0
  • Tokens de prompt: N/A
  • Tokens de respuesta: N/A
  • Tokens totales: N/A
  • Generaciones de imágenes/audio/vídeo: 0
  • Tiempo transcurrido: ~2 min validate_local
  • Proveedores/modelos: N/A

Notes and limitations

Repair never translates an English chorus into Spanish. If English remains, ok stays 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

  • The executive summary is understandable without reading the code.
  • The detailed Summary/Overview has not been removed or shortened.
  • Tests and their actual results are recorded.
  • Generated assets, secrets and local-only files are not committed.
  • Required CI and Cursor/Bugbot review are complete, or the PR is clearly marked as waiting for them.

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 / validateLyricsLanguage check 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 / repairLyricsLanguage only 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_language raises 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.

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.
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

PR Review — Loreframe Studio

Risk: low
Scope: 6 file(s); +662/-0; React UI, backend services, docs

Automated review from scripts/analyze_pr.py. This is a heuristic pass (no LLM) so humans still own the merge decision.

Findings

  • low — UI changed — rebuild before merge
    Run cd ui && npm run build (CI already does this). Pinokio Update rebuilds for end users; keep ui/dist untracked.

Changed files

  • added: app/services/lyrics_language.py, docs/development/LYRICS_LANGUAGE.md, tests/test_lyrics_language.py, ui/src/lib/lyricsLanguageGuard.ts, ui/tests/lyricsLanguageGuard.test.ts
  • modified: docs/development/DOMAIN_MODEL_AND_ASSET_PROVENANCE.md

CONTRIBUTING checklist

  • python scripts/verify_clean_repo.py
  • python -m compileall -q app/services app/launch.py scripts
  • cd ui && npm run build if the UI changed
  • No weights, CivitAI sidecars, or generated guides
  • Stays local-first (no required accounts / telemetry)

Posted by the repo PR review workflow. Re-runs on each push to the PR.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

Code health

Quality score: 49.3/100

Higher is better. The score is a trend dashboard; the independent ratchet below remains the CI gate.

Component Weight Current Change
Cyclomatic health 45% 52.4 +0.2
File concentration 25% 54.3 +0.2
Oversized-file debt 20% 29.7 +0.2
Modularity 10% 62.7 +0.2

Change vs PR base: +0.1 points.

Metric Value
Production LOC 241,783
Production files 534
Test LOC 70,345
Functions measured 15,128
Functions complexity ≥ 15 784
Maximum complexity 667

Markdown, JSON catalogs and tests are out of this table. Only app/ runtime + ui/src TS/JS count.

Most complex functions

Complexity Where
667 app/wgp.py:7164 generate_video
374 ui/src/stores/useStore.ts:4021 Async method 'startGeneration'
355 app/_launch_runtime.py:23472 _run_generation
308 app/wgp.py:12281 generate_video_tab
271 ui/src/components/Sidebar/SceneAnimatorPanel.tsx:474 Function 'SceneAnimatorPanel'
266 ui/src/stores/useStore.ts:8563 Async method 'loadSettingsFromOutput'
258 app/services/director/planners/short_film.py:3433 ShortFilmPlanner._plan_story_driven
248 app/services/director_pipeline.py:13735 _run_video_generation
245 app/services/director_pipeline.py:7860 _run_pipeline
243 ui/src/features/agent/agentActions.ts:1109 Function 'parseAction'
226 app/services/director_pipeline.py:6689 update_comic_preview
225 ui/src/features/agent/agentActions.ts:2766 Async function 'executeAgentActions'

Trend vs baseline

Metric Δ
Production LOC +455
Test LOC +167
Functions ≥ 15 +0
Maximum complexity +0

Warnings

  • production LOC increased by +455

Ratchet passed.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

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.

Create PR

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], "")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7cfa436. Configure here.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@IAnMove
IAnMove merged commit 9ac4cac into main Sep 5, 2026
3 of 4 checks passed
@IAnMove
IAnMove deleted the feat/lyrics-language-guard branch September 5, 2026 11:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant