Codex commited on
Commit ·
591e827
1
Parent(s): cec7e01
Fix lyric display and duration controls
Browse files- app.py +8 -4
- src/safety.py +12 -1
- tests/test_app_logic.py +14 -0
app.py
CHANGED
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
| 2 |
|
| 3 |
import random
|
| 4 |
import uuid
|
|
|
|
| 5 |
from pathlib import Path
|
| 6 |
|
| 7 |
import gradio as gr
|
|
@@ -263,10 +264,12 @@ def generate_time_machine_mix(
|
|
| 263 |
)
|
| 264 |
status_md = "\n".join([f"- {status}" for status in statuses])
|
| 265 |
route = timeline_route_html(source_era_label, source_genre_label, remix_era_label, remix_genre_label)
|
|
|
|
|
|
|
| 266 |
journey = f"""
|
| 267 |
{route}
|
| 268 |
-
<div class="micro"><h3>Fictional DJ intro</h3><p>{
|
| 269 |
-
<div class="micro lyric-box"><h3>Generated micro-lyrics</h3><p>{
|
| 270 |
"""
|
| 271 |
return (
|
| 272 |
final_path,
|
|
@@ -298,11 +301,12 @@ CSS = """
|
|
| 298 |
.mascot-label{position:absolute;left:22px;right:22px;bottom:22px;z-index:2;display:flex;justify-content:space-between;gap:12px;align-items:end}.mascot-label b{font-size:20px}.mascot-label span{display:block;color:#bdeff5;font-size:13px;margin-top:4px}.seal{padding:9px 12px;border-radius:999px;background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.22);font-size:12px}
|
| 299 |
.panel{background:rgba(255,255,255,.66)!important;border:1px solid rgba(255,255,255,.86)!important;border-radius:32px!important;padding:20px!important;box-shadow:0 24px 70px rgba(77,79,110,.13)!important}
|
| 300 |
.panel h2{margin:0 0 12px!important;font-size:18px!important}.compact-control label span{color:#64708a!important;font-size:12px!important;text-transform:uppercase!important;letter-spacing:.08em!important;font-weight:900!important}
|
| 301 |
-
.compact-control input,.compact-control select,.compact-control textarea{background:#fffdf4!important;border:1px solid var(--line)!important;border-radius:16px!important;color:var(--ink)!important;font-weight:700!important}
|
|
|
|
| 302 |
#generate-button{background:linear-gradient(135deg,var(--orange),#ffb84f)!important;color:#24121a!important;border:0!important;border-radius:18px!important;font-weight:1000!important;box-shadow:0 16px 32px rgba(242,106,56,.22)!important}
|
| 303 |
#surprise-button{background:linear-gradient(135deg,#c3f7ff,#a693ff)!important;color:#15213a!important;border:0!important;border-radius:18px!important;font-weight:1000!important}
|
| 304 |
.scroll{border-radius:34px;background:linear-gradient(180deg,#fffdf4,#fbebc5);border:1px solid rgba(255,255,255,.9);padding:24px;position:relative;overflow:hidden;min-height:470px}.path{display:grid;grid-template-columns:1fr auto 1fr;gap:14px;align-items:center;margin:12px 0 20px}.era-card{background:white;border:1px solid var(--line);border-radius:24px;padding:18px;box-shadow:0 14px 30px rgba(99,75,38,.08)}.era-card small{color:var(--teal);font-weight:900;text-transform:uppercase;letter-spacing:.1em}.era-card h3{font-size:26px;margin:9px 0 4px}.era-card p{margin:0;color:#657087;line-height:1.35}.swirl{width:76px;height:76px;border-radius:50%;display:grid;place-items:center;border:5px solid var(--teal);border-left-color:transparent;color:var(--orange);font-size:24px;font-weight:900}.wind-wave{height:112px;border-radius:28px;background:linear-gradient(135deg,rgba(29,184,197,.12),rgba(110,84,255,.14));border:1px dashed rgba(38,50,74,.18);position:relative;overflow:hidden}.wind-wave:before{content:"";position:absolute;inset:20px;background:repeating-radial-gradient(ellipse at center,transparent 0 12px,rgba(29,184,197,.65) 13px 16px,transparent 17px 28px);transform:skewX(-20deg)}
|
| 305 |
-
.micro{margin-top:18px;border-radius:24px;background:#172135;color:#fff;padding:18px}.micro h3{margin:0 0 8px;color:#f9d989}.micro p{font-size:18px;line-height:1.35;margin:0}.lyric-box{background:#26324a}
|
| 306 |
.audio-card{background:#172135!important;color:white!important;border-radius:24px!important;padding:14px!important}.mixtape-card{background:white;border:1px solid var(--line);border-radius:24px;padding:18px}.mixtape-title{font-size:12px;color:var(--orange);font-weight:1000;text-transform:uppercase;letter-spacing:.12em}.mixtape-card h3{margin:8px 0 12px}.ttm-kv{display:flex;justify-content:space-between;gap:12px;border-bottom:1px dashed var(--line);padding:8px 0;font-size:14px}.ttm-kv span{color:#7d8697}.ttm-kv b{text-align:right}.lyrics-chip{background:#fff4d2;border-radius:16px;padding:12px;margin:10px 0;font-weight:800}
|
| 307 |
@media(max-width:900px){.path{grid-template-columns:1fr}.swirl{margin:auto}.ttm-kv{display:block}.ttm-kv b{text-align:left;display:block;margin-top:4px}}
|
| 308 |
"""
|
|
|
|
| 2 |
|
| 3 |
import random
|
| 4 |
import uuid
|
| 5 |
+
import html
|
| 6 |
from pathlib import Path
|
| 7 |
|
| 8 |
import gradio as gr
|
|
|
|
| 264 |
)
|
| 265 |
status_md = "\n".join([f"- {status}" for status in statuses])
|
| 266 |
route = timeline_route_html(source_era_label, source_genre_label, remix_era_label, remix_genre_label)
|
| 267 |
+
intro_text = html.escape(intro["text"])
|
| 268 |
+
lyrics_html = html.escape(lyrics or "Instrumental / no lyric text requested.").replace(chr(10), "<br>")
|
| 269 |
journey = f"""
|
| 270 |
{route}
|
| 271 |
+
<div class="micro"><h3>Fictional DJ intro</h3><p>{intro_text}</p></div>
|
| 272 |
+
<div class="micro lyric-box"><h3>Generated micro-lyrics</h3><p>{lyrics_html}</p></div>
|
| 273 |
"""
|
| 274 |
return (
|
| 275 |
final_path,
|
|
|
|
| 301 |
.mascot-label{position:absolute;left:22px;right:22px;bottom:22px;z-index:2;display:flex;justify-content:space-between;gap:12px;align-items:end}.mascot-label b{font-size:20px}.mascot-label span{display:block;color:#bdeff5;font-size:13px;margin-top:4px}.seal{padding:9px 12px;border-radius:999px;background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.22);font-size:12px}
|
| 302 |
.panel{background:rgba(255,255,255,.66)!important;border:1px solid rgba(255,255,255,.86)!important;border-radius:32px!important;padding:20px!important;box-shadow:0 24px 70px rgba(77,79,110,.13)!important}
|
| 303 |
.panel h2{margin:0 0 12px!important;font-size:18px!important}.compact-control label span{color:#64708a!important;font-size:12px!important;text-transform:uppercase!important;letter-spacing:.08em!important;font-weight:900!important}
|
| 304 |
+
.compact-control input:not([type="radio"]):not([type="checkbox"]),.compact-control select,.compact-control textarea{background:#fffdf4!important;border:1px solid var(--line)!important;border-radius:16px!important;color:var(--ink)!important;font-weight:700!important}
|
| 305 |
+
.compact-control input[type="radio"],.compact-control input[type="checkbox"]{accent-color:var(--orange)!important;cursor:pointer!important}.compact-control label{cursor:pointer!important}
|
| 306 |
#generate-button{background:linear-gradient(135deg,var(--orange),#ffb84f)!important;color:#24121a!important;border:0!important;border-radius:18px!important;font-weight:1000!important;box-shadow:0 16px 32px rgba(242,106,56,.22)!important}
|
| 307 |
#surprise-button{background:linear-gradient(135deg,#c3f7ff,#a693ff)!important;color:#15213a!important;border:0!important;border-radius:18px!important;font-weight:1000!important}
|
| 308 |
.scroll{border-radius:34px;background:linear-gradient(180deg,#fffdf4,#fbebc5);border:1px solid rgba(255,255,255,.9);padding:24px;position:relative;overflow:hidden;min-height:470px}.path{display:grid;grid-template-columns:1fr auto 1fr;gap:14px;align-items:center;margin:12px 0 20px}.era-card{background:white;border:1px solid var(--line);border-radius:24px;padding:18px;box-shadow:0 14px 30px rgba(99,75,38,.08)}.era-card small{color:var(--teal);font-weight:900;text-transform:uppercase;letter-spacing:.1em}.era-card h3{font-size:26px;margin:9px 0 4px}.era-card p{margin:0;color:#657087;line-height:1.35}.swirl{width:76px;height:76px;border-radius:50%;display:grid;place-items:center;border:5px solid var(--teal);border-left-color:transparent;color:var(--orange);font-size:24px;font-weight:900}.wind-wave{height:112px;border-radius:28px;background:linear-gradient(135deg,rgba(29,184,197,.12),rgba(110,84,255,.14));border:1px dashed rgba(38,50,74,.18);position:relative;overflow:hidden}.wind-wave:before{content:"";position:absolute;inset:20px;background:repeating-radial-gradient(ellipse at center,transparent 0 12px,rgba(29,184,197,.65) 13px 16px,transparent 17px 28px);transform:skewX(-20deg)}
|
| 309 |
+
.micro{margin-top:18px;border-radius:24px;background:#172135;color:#fff;padding:18px}.micro h3{margin:0 0 8px;color:#f9d989!important}.micro p{font-size:18px;line-height:1.35;margin:0;color:#f7fbff!important}.lyric-box{background:#26324a}
|
| 310 |
.audio-card{background:#172135!important;color:white!important;border-radius:24px!important;padding:14px!important}.mixtape-card{background:white;border:1px solid var(--line);border-radius:24px;padding:18px}.mixtape-title{font-size:12px;color:var(--orange);font-weight:1000;text-transform:uppercase;letter-spacing:.12em}.mixtape-card h3{margin:8px 0 12px}.ttm-kv{display:flex;justify-content:space-between;gap:12px;border-bottom:1px dashed var(--line);padding:8px 0;font-size:14px}.ttm-kv span{color:#7d8697}.ttm-kv b{text-align:right}.lyrics-chip{background:#fff4d2;border-radius:16px;padding:12px;margin:10px 0;font-weight:800}
|
| 311 |
@media(max-width:900px){.path{grid-template-columns:1fr}.swirl{margin:auto}.ttm-kv{display:block}.ttm-kv b{text-align:left;display:block;margin-top:4px}}
|
| 312 |
"""
|
src/safety.py
CHANGED
|
@@ -25,6 +25,15 @@ BLOCKED_PATTERNS = [
|
|
| 25 |
|
| 26 |
FORBIDDEN_UI_TERMS = ["Aang", "Avatar", "Airbender", "Air Nomad", "four nations", "bending"]
|
| 27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
def copyright_safety_note() -> str:
|
| 30 |
return SAFETY_NOTE
|
|
@@ -66,6 +75,9 @@ def sanitize_generated_lyrics(lyrics: str) -> tuple[str, list[str]]:
|
|
| 66 |
line = re.sub(r"[^A-Za-zÀ-ÿ0-9 ,.'!?-]", "", line).strip()
|
| 67 |
if not line:
|
| 68 |
continue
|
|
|
|
|
|
|
|
|
|
| 69 |
words = line.split()
|
| 70 |
if len(words) > 12:
|
| 71 |
warnings.append("Trimmed a lyric line to 12 words.")
|
|
@@ -76,4 +88,3 @@ def sanitize_generated_lyrics(lyrics: str) -> tuple[str, list[str]]:
|
|
| 76 |
if not clean_lines:
|
| 77 |
warnings.append("Generated lyrics were empty after sanitization.")
|
| 78 |
return "\n".join(clean_lines), warnings
|
| 79 |
-
|
|
|
|
| 25 |
|
| 26 |
FORBIDDEN_UI_TERMS = ["Aang", "Avatar", "Airbender", "Air Nomad", "four nations", "bending"]
|
| 27 |
|
| 28 |
+
LYRIC_META_PATTERNS = [
|
| 29 |
+
r"\b(no|without)\s+(explanation|punctuation|commentary|markdown)\b",
|
| 30 |
+
r"\breturn\s+only\b",
|
| 31 |
+
r"\b(two|2)\s+lyric\s+lines?\b",
|
| 32 |
+
r"\bmaximum\s+\d+\s+words?\b",
|
| 33 |
+
r"\brules?\b",
|
| 34 |
+
r"\blyrics?:?\b",
|
| 35 |
+
]
|
| 36 |
+
|
| 37 |
|
| 38 |
def copyright_safety_note() -> str:
|
| 39 |
return SAFETY_NOTE
|
|
|
|
| 75 |
line = re.sub(r"[^A-Za-zÀ-ÿ0-9 ,.'!?-]", "", line).strip()
|
| 76 |
if not line:
|
| 77 |
continue
|
| 78 |
+
if any(re.search(pattern, line, flags=re.IGNORECASE) for pattern in LYRIC_META_PATTERNS):
|
| 79 |
+
warnings.append("Removed non-lyric instruction text from generated lyrics.")
|
| 80 |
+
continue
|
| 81 |
words = line.split()
|
| 82 |
if len(words) > 12:
|
| 83 |
warnings.append("Trimmed a lyric line to 12 words.")
|
|
|
|
| 88 |
if not clean_lines:
|
| 89 |
warnings.append("Generated lyrics were empty after sanitization.")
|
| 90 |
return "\n".join(clean_lines), warnings
|
|
|
tests/test_app_logic.py
CHANGED
|
@@ -10,6 +10,7 @@ import soundfile as sf
|
|
| 10 |
import app
|
| 11 |
from src.lyrics_generator import generate_micro_lyrics
|
| 12 |
from src.music_generator import generate_music
|
|
|
|
| 13 |
from src.tts import generate_dj_voice
|
| 14 |
|
| 15 |
|
|
@@ -49,6 +50,19 @@ class TurntableTimeMachineLogicTests(unittest.TestCase):
|
|
| 49 |
self.assertFalse(app.update_lyric_theme_visibility("Instrumental only")["visible"])
|
| 50 |
self.assertFalse(app.update_lyric_theme_visibility("Wordless vocal texture")["visible"])
|
| 51 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
def test_surprise_me_returns_complete_valid_state(self):
|
| 53 |
result = app.surprise_me()
|
| 54 |
self.assertEqual(len(result), 15)
|
|
|
|
| 10 |
import app
|
| 11 |
from src.lyrics_generator import generate_micro_lyrics
|
| 12 |
from src.music_generator import generate_music
|
| 13 |
+
from src.safety import sanitize_generated_lyrics
|
| 14 |
from src.tts import generate_dj_voice
|
| 15 |
|
| 16 |
|
|
|
|
| 50 |
self.assertFalse(app.update_lyric_theme_visibility("Instrumental only")["visible"])
|
| 51 |
self.assertFalse(app.update_lyric_theme_visibility("Wordless vocal texture")["visible"])
|
| 52 |
|
| 53 |
+
def test_micro_lyrics_sanitizer_removes_instruction_text(self):
|
| 54 |
+
clean, warnings = sanitize_generated_lyrics(
|
| 55 |
+
"No explanation, no punctuation\n"
|
| 56 |
+
"Vapor dreams drift on neon haze\n"
|
| 57 |
+
"We chase tomorrow through glass streets"
|
| 58 |
+
)
|
| 59 |
+
self.assertEqual(clean, "Vapor dreams drift on neon haze\nWe chase tomorrow through glass streets")
|
| 60 |
+
self.assertTrue(any("instruction" in warning for warning in warnings))
|
| 61 |
+
|
| 62 |
+
def test_compact_control_css_does_not_restyle_radio_inputs(self):
|
| 63 |
+
self.assertIn('input:not([type="radio"]):not([type="checkbox"])', app.CSS)
|
| 64 |
+
self.assertIn('input[type="radio"]', app.CSS)
|
| 65 |
+
|
| 66 |
def test_surprise_me_returns_complete_valid_state(self):
|
| 67 |
result = app.surprise_me()
|
| 68 |
self.assertEqual(len(result), 15)
|