Codex commited on
Commit
7b926ae
·
1 Parent(s): 4a1f495

Replace DJ intro TTS with MMS models

Browse files
Files changed (5) hide show
  1. README.md +9 -6
  2. app.py +1 -1
  3. src/prompt_builder.py +1 -1
  4. src/tts.py +39 -9
  5. tests/test_app_logic.py +15 -1
README.md CHANGED
@@ -11,7 +11,10 @@ pinned: false
11
  license: mit
12
  models:
13
  - ACE-Step/Ace-Step1.5
14
- - hexgrad/Kokoro-82M
 
 
 
15
  - CohereLabs/tiny-aya-global
16
  tags:
17
  - build-small
@@ -37,7 +40,7 @@ Turntable Time Machine is a whimsical Gradio app built for the Hugging Face x Gr
37
 
38
  *The twist:* this is not a famous-song remix machine 🚫🎤. The app uses broad musical-era vocabularies and safety constraints to make new little audio artifacts: 1960s soul through a 1990s house lens 🪩, 1980s synth-pop polished into 2000s filtered disco house 💿, or a 2020s lo-fi signal drifting backward through old radio static 📡.
39
 
40
- This submission targets the **Thousand Token Wood** track 🪵: playful, compact, AI-native, and built around small/open model targets. ACE-Step, Kokoro, and Tiny Aya are used as optional model hooks, while deterministic templates and fallback audio keep the app demonstrable when model access or GPU runtime is limited.
41
 
42
  ## 🔗 Submission Links
43
 
@@ -60,7 +63,7 @@ A concise demo flow:
60
  8. 🌈 Keep **nostalgic and warm**, choose **Clean digital master**, and use the **10 second** duration.
61
  9. ⚡ Click **Bend the Timeline**.
62
  10. ▶️ Play the **Final broadcast**, then compare it with **Music only**.
63
- 11. 🎙️ Show the **Fictional DJ intro** text card. If Kokoro TTS is unavailable, note that the app shows the intro as text instead of pretending a tone is spoken word.
64
  12. ✍️ Show the **Generated micro-lyrics** card and the **Mixtape Card** with the route, model status, BPM, seed, texture, and safety note.
65
 
66
  🎬 Recommended one-video case: **1960s Folk pop -> 2000s indie rock** with **The Warehouse Announcer**, **French**, **Original micro-lyrics**, and **English + broadcast language**. It shows the app's full loop: era routing, fictional host text, multilingual prompt routing, generated micro-lyrics, safety-aware prompt, texture processing, and final audio.
@@ -93,7 +96,7 @@ User selections
93
  → BPM selection from genre ranges
94
  → optional Tiny Aya micro-lyrics
95
  → safety-aware music prompt
96
- → optional Kokoro DJ narration
97
  → ACE-Step music hook or fallback synth audio
98
  → nostalgia texture processing
99
  → final broadcast and mixtape card
@@ -106,11 +109,11 @@ User selections
106
  | Component | Model | Parameters | Used for |
107
  | --- | --- | ---: | --- |
108
  | Music generation target | [ACE-Step/Ace-Step1.5](https://huggingface.co/ACE-Step/Ace-Step1.5) | under 32B target stack | Original short music clips |
109
- | Spoken DJ intro target | [hexgrad/Kokoro-82M](https://huggingface.co/hexgrad/Kokoro-82M) | 82M | Optional fictional DJ narration |
110
  | Text / micro-lyrics target | [CohereLabs/tiny-aya-global](https://huggingface.co/CohereLabs/tiny-aya-global) | small multilingual model | Optional two-line original lyrics |
111
  | Template and synth fallback | none | 0 | Keeps the app usable in restricted runtimes |
112
 
113
- ACE-Step and Kokoro are lazy optional hooks. If model packages, weights, or hardware are unavailable, the app falls back to playable synthetic demo audio and text-only DJ intros. Tiny Aya falls back to deterministic micro-lyrics when access is restricted.
114
 
115
  ## 🗂️ Data
116
 
 
11
  license: mit
12
  models:
13
  - ACE-Step/Ace-Step1.5
14
+ - facebook/mms-tts-eng
15
+ - facebook/mms-tts-deu
16
+ - facebook/mms-tts-fra
17
+ - facebook/mms-tts-spa
18
  - CohereLabs/tiny-aya-global
19
  tags:
20
  - build-small
 
40
 
41
  *The twist:* this is not a famous-song remix machine 🚫🎤. The app uses broad musical-era vocabularies and safety constraints to make new little audio artifacts: 1960s soul through a 1990s house lens 🪩, 1980s synth-pop polished into 2000s filtered disco house 💿, or a 2020s lo-fi signal drifting backward through old radio static 📡.
42
 
43
+ This submission targets the **Thousand Token Wood** track 🪵: playful, compact, AI-native, and built around small/open model targets. ACE-Step, MMS TTS, and Tiny Aya are used as optional model hooks, while deterministic templates and fallback audio keep the app demonstrable when model access or GPU runtime is limited.
44
 
45
  ## 🔗 Submission Links
46
 
 
63
  8. 🌈 Keep **nostalgic and warm**, choose **Clean digital master**, and use the **10 second** duration.
64
  9. ⚡ Click **Bend the Timeline**.
65
  10. ▶️ Play the **Final broadcast**, then compare it with **Music only**.
66
+ 11. 🎙️ Show the **Fictional DJ intro** text card and the **Spoken DJ intro** audio when MMS TTS is available.
67
  12. ✍️ Show the **Generated micro-lyrics** card and the **Mixtape Card** with the route, model status, BPM, seed, texture, and safety note.
68
 
69
  🎬 Recommended one-video case: **1960s Folk pop -> 2000s indie rock** with **The Warehouse Announcer**, **French**, **Original micro-lyrics**, and **English + broadcast language**. It shows the app's full loop: era routing, fictional host text, multilingual prompt routing, generated micro-lyrics, safety-aware prompt, texture processing, and final audio.
 
96
  → BPM selection from genre ranges
97
  → optional Tiny Aya micro-lyrics
98
  → safety-aware music prompt
99
+ → optional MMS TTS DJ narration
100
  → ACE-Step music hook or fallback synth audio
101
  → nostalgia texture processing
102
  → final broadcast and mixtape card
 
109
  | Component | Model | Parameters | Used for |
110
  | --- | --- | ---: | --- |
111
  | Music generation target | [ACE-Step/Ace-Step1.5](https://huggingface.co/ACE-Step/Ace-Step1.5) | under 32B target stack | Original short music clips |
112
+ | Spoken DJ intro target | [facebook/mms-tts-eng](https://huggingface.co/facebook/mms-tts-eng), `deu`, `fra`, `spa` variants | 36.3M each for English model card | Optional fictional DJ narration in the selected broadcast language |
113
  | Text / micro-lyrics target | [CohereLabs/tiny-aya-global](https://huggingface.co/CohereLabs/tiny-aya-global) | small multilingual model | Optional two-line original lyrics |
114
  | Template and synth fallback | none | 0 | Keeps the app usable in restricted runtimes |
115
 
116
+ ACE-Step and MMS TTS are lazy optional hooks. If model packages, weights, or hardware are unavailable, the app falls back to playable synthetic demo audio and text-only DJ intros. Tiny Aya falls back to deterministic micro-lyrics when access is restricted.
117
 
118
  ## 🗂️ Data
119
 
app.py CHANGED
@@ -383,7 +383,7 @@ with gr.Blocks(title="Turntable Time Machine", css=CSS) as demo:
383
  final_audio = gr.Audio(label="Final broadcast", elem_classes=["audio-card"])
384
  music_audio = gr.Audio(label="Music only", elem_classes=["audio-card"])
385
  intro_audio = gr.Audio(label="Spoken DJ intro", elem_classes=["audio-card"])
386
- intro_audio_status = gr.Markdown("Spoken DJ intro appears here when Kokoro TTS is available.")
387
  mixtape_card = gr.HTML("<div class='mixtape-card'><div class='mixtape-title'>Mixtape Card</div><h3>Waiting for a timeline bend.</h3></div>")
388
  with gr.Accordion("Generated text and status", open=False):
389
  dj_intro_text = gr.Textbox(label="Fictional DJ intro text", lines=3)
 
383
  final_audio = gr.Audio(label="Final broadcast", elem_classes=["audio-card"])
384
  music_audio = gr.Audio(label="Music only", elem_classes=["audio-card"])
385
  intro_audio = gr.Audio(label="Spoken DJ intro", elem_classes=["audio-card"])
386
+ intro_audio_status = gr.Markdown("Spoken DJ intro appears here when MMS TTS is available.")
387
  mixtape_card = gr.HTML("<div class='mixtape-card'><div class='mixtape-title'>Mixtape Card</div><h3>Waiting for a timeline bend.</h3></div>")
388
  with gr.Accordion("Generated text and status", open=False):
389
  dj_intro_text = gr.Textbox(label="Fictional DJ intro text", lines=3)
src/prompt_builder.py CHANGED
@@ -143,7 +143,7 @@ def build_mixtape_card(
143
  ("Texture", texture.get("label")),
144
  ("Mood", mood),
145
  ("BPM / Seed", f"{bpm} / {seed}"),
146
- ("Model stack", "ACE-Step 1.5 music, Kokoro-82M spoken DJ, Tiny Aya text; graceful local fallbacks"),
147
  ("Status", " | ".join(statuses)),
148
  ("Safety", copyright_safety_note()),
149
  ]
 
143
  ("Texture", texture.get("label")),
144
  ("Mood", mood),
145
  ("BPM / Seed", f"{bpm} / {seed}"),
146
+ ("Model stack", "ACE-Step 1.5 music, MMS TTS spoken DJ, Tiny Aya text; graceful local fallbacks"),
147
  ("Status", " | ".join(statuses)),
148
  ("Safety", copyright_safety_note()),
149
  ]
src/tts.py CHANGED
@@ -1,5 +1,6 @@
1
  from __future__ import annotations
2
 
 
3
  from pathlib import Path
4
  import os
5
 
@@ -8,6 +9,23 @@ import numpy as np
8
  from .audio_utils import save_audio
9
 
10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  def generate_dj_voice(
12
  text: str,
13
  language_id: str,
@@ -15,7 +33,11 @@ def generate_dj_voice(
15
  output_path: str = "outputs/dj_intro.wav",
16
  ) -> dict:
17
  Path(output_path).parent.mkdir(parents=True, exist_ok=True)
 
18
  try:
 
 
 
19
  if os.getenv("TTM_TEST_MODEL_CALLS") == "1":
20
  sr = 24000
21
  duration = min(2.0, max(0.4, len(text.split()) * 0.08))
@@ -25,23 +47,31 @@ def generate_dj_voice(
25
  return {
26
  "path": output_path,
27
  "sample_rate": sr,
28
- "status": "Kokoro spoken-DJ model call simulated for tests.",
29
  "tts_succeeded": True,
30
  "model_attempted": True,
31
- "model_id": "hexgrad/Kokoro-82M",
32
  }
33
 
34
- # Kokoro is optional and lazily imported. This app never clones or imitates
35
- # voices; the text is only fictional DJ narration.
36
- import kokoro # type: ignore # noqa: F401
37
-
38
- raise RuntimeError("Kokoro wrapper hook is present, but local TTS inference is not configured.")
 
 
 
 
 
 
 
 
39
  except Exception as exc:
40
  return {
41
  "path": None,
42
  "sample_rate": None,
43
- "status": f"Spoken DJ intro unavailable; showing text intro only. Kokoro TTS unavailable or unsupported here: {exc}",
44
  "tts_succeeded": False,
45
  "model_attempted": True,
46
- "model_id": "hexgrad/Kokoro-82M",
47
  }
 
1
  from __future__ import annotations
2
 
3
+ from functools import lru_cache
4
  from pathlib import Path
5
  import os
6
 
 
9
  from .audio_utils import save_audio
10
 
11
 
12
+ MMS_TTS_MODEL_IDS = {
13
+ "en": "facebook/mms-tts-eng",
14
+ "de": "facebook/mms-tts-deu",
15
+ "fr": "facebook/mms-tts-fra",
16
+ "es": "facebook/mms-tts-spa",
17
+ }
18
+ DEFAULT_TTS_MODEL_ID = MMS_TTS_MODEL_IDS["en"]
19
+
20
+
21
+ @lru_cache(maxsize=4)
22
+ def load_tts_pipeline(model_id: str):
23
+ from transformers import pipeline
24
+
25
+ token = os.getenv("HF_TOKEN") or os.getenv("HUGGING_FACE_HUB_TOKEN")
26
+ return pipeline("text-to-speech", model=model_id, token=token)
27
+
28
+
29
  def generate_dj_voice(
30
  text: str,
31
  language_id: str,
 
33
  output_path: str = "outputs/dj_intro.wav",
34
  ) -> dict:
35
  Path(output_path).parent.mkdir(parents=True, exist_ok=True)
36
+ model_id = os.getenv("TTS_MODEL_ID") or MMS_TTS_MODEL_IDS.get(language_id, DEFAULT_TTS_MODEL_ID)
37
  try:
38
+ if os.getenv("TTM_DISABLE_TTS_MODEL") == "1":
39
+ raise RuntimeError("TTS model disabled by TTM_DISABLE_TTS_MODEL.")
40
+
41
  if os.getenv("TTM_TEST_MODEL_CALLS") == "1":
42
  sr = 24000
43
  duration = min(2.0, max(0.4, len(text.split()) * 0.08))
 
47
  return {
48
  "path": output_path,
49
  "sample_rate": sr,
50
+ "status": f"MMS TTS spoken-DJ model call simulated for tests with {model_id}.",
51
  "tts_succeeded": True,
52
  "model_attempted": True,
53
+ "model_id": model_id,
54
  }
55
 
56
+ generator = load_tts_pipeline(model_id)
57
+ speech = generator(text[:420])
58
+ audio = np.asarray(speech["audio"], dtype=np.float32).squeeze()
59
+ sample_rate = int(speech["sampling_rate"])
60
+ save_audio(output_path, audio, sample_rate)
61
+ return {
62
+ "path": output_path,
63
+ "sample_rate": sample_rate,
64
+ "status": f"Spoken DJ intro generated with {model_id}.",
65
+ "tts_succeeded": True,
66
+ "model_attempted": True,
67
+ "model_id": model_id,
68
+ }
69
  except Exception as exc:
70
  return {
71
  "path": None,
72
  "sample_rate": None,
73
+ "status": f"Spoken DJ intro unavailable; showing text intro only. MMS TTS unavailable or unsupported here: {exc}",
74
  "tts_succeeded": False,
75
  "model_attempted": True,
76
+ "model_id": model_id,
77
  }
tests/test_app_logic.py CHANGED
@@ -17,6 +17,7 @@ from src.tts import generate_dj_voice
17
  class TurntableTimeMachineLogicTests(unittest.TestCase):
18
  def tearDown(self):
19
  os.environ.pop("TTM_TEST_MODEL_CALLS", None)
 
20
  patterns = [
21
  "outputs/test_*.wav",
22
  "outputs/final_mix_*.wav",
@@ -148,9 +149,22 @@ class TurntableTimeMachineLogicTests(unittest.TestCase):
148
  self.assertFalse(lyrics["fallback_used"])
149
  self.assertIn("simulated for tests", music["status"])
150
  self.assertIn("simulated for tests", tts["status"])
 
151
  self.assertIn("simulated for tests", lyrics["status"])
152
 
 
 
 
 
 
 
 
 
 
 
 
153
  def test_generate_pipeline_all_vocal_modes_create_final_audio(self):
 
154
  for index, vocal_mode in enumerate(app.VOCAL_MODES):
155
  with self.subTest(vocal_mode=vocal_mode):
156
  result = app.generate_time_machine_mix(
@@ -231,7 +245,7 @@ class TurntableTimeMachineLogicTests(unittest.TestCase):
231
  self.assertTrue(Path(intro_path).exists())
232
  self.assertEqual(intro_status, "Spoken DJ intro generated.")
233
  self.assertIn("ACE-Step model call simulated", status)
234
- self.assertIn("Kokoro spoken-DJ model call simulated", status)
235
  self.assertIn("tiny-aya-global text model call simulated", status)
236
  self.assertIn("Lyrics:", prompt)
237
  self.assertGreaterEqual(len(lyrics_text.splitlines()), 2)
 
17
  class TurntableTimeMachineLogicTests(unittest.TestCase):
18
  def tearDown(self):
19
  os.environ.pop("TTM_TEST_MODEL_CALLS", None)
20
+ os.environ.pop("TTM_DISABLE_TTS_MODEL", None)
21
  patterns = [
22
  "outputs/test_*.wav",
23
  "outputs/final_mix_*.wav",
 
149
  self.assertFalse(lyrics["fallback_used"])
150
  self.assertIn("simulated for tests", music["status"])
151
  self.assertIn("simulated for tests", tts["status"])
152
+ self.assertEqual(tts["model_id"], "facebook/mms-tts-eng")
153
  self.assertIn("simulated for tests", lyrics["status"])
154
 
155
+ def test_tts_model_routes_by_language(self):
156
+ os.environ["TTM_TEST_MODEL_CALLS"] = "1"
157
+ tts = generate_dj_voice(
158
+ "Bonsoir, voyageurs du temps.",
159
+ "fr",
160
+ "warehouse_announcer",
161
+ "outputs/test_model_tts_fr.wav",
162
+ )
163
+ self.assertTrue(tts["tts_succeeded"])
164
+ self.assertEqual(tts["model_id"], "facebook/mms-tts-fra")
165
+
166
  def test_generate_pipeline_all_vocal_modes_create_final_audio(self):
167
+ os.environ["TTM_DISABLE_TTS_MODEL"] = "1"
168
  for index, vocal_mode in enumerate(app.VOCAL_MODES):
169
  with self.subTest(vocal_mode=vocal_mode):
170
  result = app.generate_time_machine_mix(
 
245
  self.assertTrue(Path(intro_path).exists())
246
  self.assertEqual(intro_status, "Spoken DJ intro generated.")
247
  self.assertIn("ACE-Step model call simulated", status)
248
+ self.assertIn("MMS TTS spoken-DJ model call simulated", status)
249
  self.assertIn("tiny-aya-global text model call simulated", status)
250
  self.assertIn("Lyrics:", prompt)
251
  self.assertGreaterEqual(len(lyrics_text.splitlines()), 2)