Codex commited on
Commit
2d8ad35
·
1 Parent(s): 591e827

Clarify prompt modes and diversify fallbacks

Browse files
Files changed (5) hide show
  1. app.py +2 -2
  2. src/music_generator.py +63 -14
  3. src/prompt_builder.py +3 -4
  4. src/tts.py +32 -3
  5. tests/test_app_logic.py +29 -3
app.py CHANGED
@@ -75,7 +75,7 @@ MOODS = [
75
  "futuristic and strange",
76
  "relaxed and intimate",
77
  ]
78
- PROMPT_MODES = ["English prompt for best stability", "Selected language prompt", "Bilingual prompt"]
79
  VOCAL_MODES = ["Instrumental only", "Wordless vocal texture", "Original micro-lyrics"]
80
  DURATIONS = [10, 15, 20]
81
 
@@ -355,7 +355,7 @@ with gr.Blocks(title="Turntable Time Machine", css=CSS) as demo:
355
  broadcast_language = gr.Dropdown(list(LANGUAGE_LABEL_TO_ID.keys()), value="English", label="Broadcast language", elem_classes=["compact-control"])
356
  vocal_mode = gr.Dropdown(VOCAL_MODES, value="Instrumental only", label="Vocal mode", elem_classes=["compact-control"])
357
  lyric_theme = gr.Dropdown(list(THEME_LABEL_TO_ID.keys()), value=list(THEME_LABEL_TO_ID.keys())[0], label="Lyric theme", visible=False, elem_classes=["compact-control"])
358
- prompt_language_mode = gr.Dropdown(PROMPT_MODES, value=PROMPT_MODES[0], label="Prompt language mode", elem_classes=["compact-control"])
359
  mood = gr.Dropdown(MOODS, value=MOODS[0], label="Mood", elem_classes=["compact-control"])
360
  audio_texture = gr.Dropdown(list(TEXTURE_LABEL_TO_ID.keys()), value=list(TEXTURE_LABEL_TO_ID.keys())[-1], label="Audio texture", elem_classes=["compact-control"])
361
  with gr.Row():
 
75
  "futuristic and strange",
76
  "relaxed and intimate",
77
  ]
78
+ PROMPT_MODES = ["Best stability (English)", "Broadcast language", "English + broadcast language"]
79
  VOCAL_MODES = ["Instrumental only", "Wordless vocal texture", "Original micro-lyrics"]
80
  DURATIONS = [10, 15, 20]
81
 
 
355
  broadcast_language = gr.Dropdown(list(LANGUAGE_LABEL_TO_ID.keys()), value="English", label="Broadcast language", elem_classes=["compact-control"])
356
  vocal_mode = gr.Dropdown(VOCAL_MODES, value="Instrumental only", label="Vocal mode", elem_classes=["compact-control"])
357
  lyric_theme = gr.Dropdown(list(THEME_LABEL_TO_ID.keys()), value=list(THEME_LABEL_TO_ID.keys())[0], label="Lyric theme", visible=False, elem_classes=["compact-control"])
358
+ prompt_language_mode = gr.Dropdown(PROMPT_MODES, value=PROMPT_MODES[0], label="Music prompt language", elem_classes=["compact-control"])
359
  mood = gr.Dropdown(MOODS, value=MOODS[0], label="Mood", elem_classes=["compact-control"])
360
  audio_texture = gr.Dropdown(list(TEXTURE_LABEL_TO_ID.keys()), value=list(TEXTURE_LABEL_TO_ID.keys())[-1], label="Audio texture", elem_classes=["compact-control"])
361
  with gr.Row():
src/music_generator.py CHANGED
@@ -1,6 +1,7 @@
1
  from __future__ import annotations
2
 
3
  from pathlib import Path
 
4
  import os
5
 
6
  import numpy as np
@@ -34,36 +35,79 @@ def _tone(freq: float, t: np.ndarray, wave: str = "sine") -> np.ndarray:
34
  return np.sin(phase).astype(np.float32)
35
 
36
 
 
 
 
 
 
 
37
  def generate_fallback_music(prompt: str, duration: int, bpm: int, seed: int | None, output_path: str) -> dict:
38
  sr = 44100
39
- rng = np.random.default_rng(seed)
40
  total = int(duration * sr)
41
  audio = np.zeros(total, dtype=np.float32)
42
  beat = 60.0 / max(bpm, 1)
43
  samples_per_beat = int(beat * sr)
44
- key = int(rng.integers(0, 7))
45
- root = 110.0 * (2 ** (key / 12))
46
- chords = [root, root * 2 ** (3 / 12), root * 2 ** (7 / 12), root * 2 ** (10 / 12)]
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  t = np.arange(total) / sr
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
  bass_notes = [root / 2, root * 2 ** (5 / 12) / 2, root * 2 ** (7 / 12) / 2, root * 2 ** (3 / 12) / 2]
50
  for i in range(0, total, samples_per_beat):
51
  n = min(samples_per_beat, total - i)
52
  local_t = np.arange(n) / sr
53
- kick = np.sin(2 * np.pi * (62 * np.exp(-local_t * 24)) * local_t) * np.exp(-local_t * 18)
54
- audio[i : i + n] += kick.astype(np.float32) * 0.55
55
- bass = _tone(bass_notes[(i // samples_per_beat) % len(bass_notes)], local_t, "square")
56
- audio[i : i + n] += bass * _env(n, sr, 0.005, 0.12) * 0.12
 
 
 
 
 
 
57
 
58
  bar = max(samples_per_beat * 4, 1)
59
  for i in range(0, total, bar):
60
  n = min(bar, total - i)
61
  local_t = np.arange(n) / sr
62
- chord = sum(_tone(freq, local_t, "saw") for freq in chords) / len(chords)
63
- audio[i : i + n] += chord * _env(n, sr, 0.4, 0.4) * 0.09
64
  chords = chords[1:] + chords[:1]
65
 
66
- hat_interval = max(samples_per_beat // 2, 1)
67
  noise = rng.normal(0, 1, total).astype(np.float32)
68
  if butter is not None and lfilter is not None:
69
  b, a = butter(1, 7000 / (sr / 2), btype="high")
@@ -72,9 +116,14 @@ def generate_fallback_music(prompt: str, duration: int, bpm: int, seed: int | No
72
  noise = (noise - np.convolve(noise, np.ones(64, dtype=np.float32) / 64, mode="same")).astype(np.float32)
73
  for i in range(0, total, hat_interval):
74
  n = min(int(0.045 * sr), total - i)
75
- audio[i : i + n] += noise[i : i + n] * _env(n, sr, 0.001, 0.035) * 0.055
76
-
77
- shimmer = _tone(root * 4, t, "sine") * 0.018 + _tone(root * 6, t, "sine") * 0.011
 
 
 
 
 
78
  audio += shimmer.astype(np.float32)
79
  stereo = np.stack([audio, np.roll(audio, int(0.009 * sr))], axis=1)
80
  save_audio(output_path, stereo, sr)
 
1
  from __future__ import annotations
2
 
3
  from pathlib import Path
4
+ import hashlib
5
  import os
6
 
7
  import numpy as np
 
35
  return np.sin(phase).astype(np.float32)
36
 
37
 
38
+ def _prompt_rng(prompt: str, seed: int | None) -> np.random.Generator:
39
+ digest = hashlib.sha256(f"{seed or 0}:{prompt}".encode("utf-8")).digest()
40
+ route_seed = int.from_bytes(digest[:8], "little") % (2**32)
41
+ return np.random.default_rng(route_seed)
42
+
43
+
44
  def generate_fallback_music(prompt: str, duration: int, bpm: int, seed: int | None, output_path: str) -> dict:
45
  sr = 44100
46
+ rng = _prompt_rng(prompt, seed)
47
  total = int(duration * sr)
48
  audio = np.zeros(total, dtype=np.float32)
49
  beat = 60.0 / max(bpm, 1)
50
  samples_per_beat = int(beat * sr)
51
+ prompt_lower = prompt.lower()
52
+ key = int(rng.integers(0, 12))
53
+ root = 82.41 * (2 ** (key / 12))
54
+ if any(term in prompt_lower for term in ("lo-fi", "trip-hop", "folk", "bedroom")):
55
+ root *= 0.82
56
+ elif any(term in prompt_lower for term in ("hyperpop", "festival", "garage", "techno")):
57
+ root *= 1.18
58
+
59
+ chord_shapes = [
60
+ [0, 3, 7, 10],
61
+ [0, 4, 7, 11],
62
+ [0, 5, 7, 12],
63
+ [0, 2, 7, 9],
64
+ ]
65
+ chord_shape = chord_shapes[int(rng.integers(0, len(chord_shapes)))]
66
+ chords = [root * 2 ** (step / 12) for step in chord_shape]
67
  t = np.arange(total) / sr
68
+ beat_accent = 0.62
69
+ bass_wave = "square"
70
+ chord_wave = "saw"
71
+ swing = 1.0
72
+ hat_divider = 2
73
+
74
+ if any(term in prompt_lower for term in ("disco", "house", "club", "garage", "amapiano")):
75
+ beat_accent = 0.72
76
+ hat_divider = 2
77
+ if any(term in prompt_lower for term in ("lo-fi", "trip-hop", "bedroom", "folk")):
78
+ beat_accent = 0.42
79
+ bass_wave = "sine"
80
+ chord_wave = "sine"
81
+ swing = 1.18
82
+ if any(term in prompt_lower for term in ("techno", "hyperpop", "future", "ai-era")):
83
+ bass_wave = "saw"
84
+ chord_wave = "square"
85
+ hat_divider = 1
86
 
87
  bass_notes = [root / 2, root * 2 ** (5 / 12) / 2, root * 2 ** (7 / 12) / 2, root * 2 ** (3 / 12) / 2]
88
  for i in range(0, total, samples_per_beat):
89
  n = min(samples_per_beat, total - i)
90
  local_t = np.arange(n) / sr
91
+ beat_index = i // samples_per_beat
92
+ kick_freq = 48 + int(rng.integers(0, 24))
93
+ kick = np.sin(2 * np.pi * (kick_freq * np.exp(-local_t * 24)) * local_t) * np.exp(-local_t * 18)
94
+ audio[i : i + n] += kick.astype(np.float32) * beat_accent
95
+ bass = _tone(bass_notes[beat_index % len(bass_notes)], local_t, bass_wave)
96
+ bass_amount = 0.08 + float(rng.random()) * 0.08
97
+ audio[i : i + n] += bass * _env(n, sr, 0.005, 0.12) * bass_amount
98
+ if "amapiano" in prompt_lower and beat_index % 2 == 1:
99
+ log_hit = _tone(root * 0.74, local_t, "sine") * np.exp(-local_t * 9)
100
+ audio[i : i + n] += log_hit.astype(np.float32) * 0.18
101
 
102
  bar = max(samples_per_beat * 4, 1)
103
  for i in range(0, total, bar):
104
  n = min(bar, total - i)
105
  local_t = np.arange(n) / sr
106
+ chord = sum(_tone(freq, local_t, chord_wave) for freq in chords) / len(chords)
107
+ audio[i : i + n] += chord * _env(n, sr, 0.25, 0.5) * (0.06 + float(rng.random()) * 0.05)
108
  chords = chords[1:] + chords[:1]
109
 
110
+ hat_interval = max(int(samples_per_beat / hat_divider * swing), 1)
111
  noise = rng.normal(0, 1, total).astype(np.float32)
112
  if butter is not None and lfilter is not None:
113
  b, a = butter(1, 7000 / (sr / 2), btype="high")
 
116
  noise = (noise - np.convolve(noise, np.ones(64, dtype=np.float32) / 64, mode="same")).astype(np.float32)
117
  for i in range(0, total, hat_interval):
118
  n = min(int(0.045 * sr), total - i)
119
+ audio[i : i + n] += noise[i : i + n] * _env(n, sr, 0.001, 0.035) * (0.035 + float(rng.random()) * 0.04)
120
+
121
+ lead_ratio = float(rng.choice([3, 4, 5, 6, 7]))
122
+ shimmer = _tone(root * lead_ratio, t, "sine") * 0.014 + _tone(root * (lead_ratio + 2), t, "sine") * 0.009
123
+ if any(term in prompt_lower for term in ("surf", "psychedelic", "vhs", "cassette")):
124
+ shimmer += _tone(root * 2.5, t, "saw") * 0.014
125
+ if "hyperpop" in prompt_lower:
126
+ shimmer += _tone(root * 8, t, "square") * 0.012
127
  audio += shimmer.astype(np.float32)
128
  stereo = np.stack([audio, np.roll(audio, int(0.009 * sr))], axis=1)
129
  save_audio(output_path, stereo, sr)
src/prompt_builder.py CHANGED
@@ -76,15 +76,15 @@ def build_music_prompt(
76
  duration,
77
  bpm,
78
  language_id="en",
79
- prompt_language_mode="English prompt for best stability",
80
  vocal_mode="Instrumental only",
81
  lyrics=None,
82
  ) -> str:
83
  english = _english_prompt(source_era, source_genre, remix_era, remix_genre, texture, mood, duration, bpm)
84
  localized, _ = _localized_prompt(language_id, source_era, source_genre, remix_era, remix_genre, texture, mood, duration, bpm)
85
- if prompt_language_mode == "Selected language prompt":
86
  prompt = localized
87
- elif prompt_language_mode == "Bilingual prompt" and language_id != "en":
88
  prompt = f"{english}\n{localized}"
89
  else:
90
  prompt = english
@@ -159,4 +159,3 @@ def build_mixtape_card(
159
  {kv}
160
  </div>
161
  """
162
-
 
76
  duration,
77
  bpm,
78
  language_id="en",
79
+ prompt_language_mode="Best stability (English)",
80
  vocal_mode="Instrumental only",
81
  lyrics=None,
82
  ) -> str:
83
  english = _english_prompt(source_era, source_genre, remix_era, remix_genre, texture, mood, duration, bpm)
84
  localized, _ = _localized_prompt(language_id, source_era, source_genre, remix_era, remix_genre, texture, mood, duration, bpm)
85
+ if prompt_language_mode in ("Broadcast language", "Selected language prompt"):
86
  prompt = localized
87
+ elif prompt_language_mode in ("English + broadcast language", "Bilingual prompt") and language_id != "en":
88
  prompt = f"{english}\n{localized}"
89
  else:
90
  prompt = english
 
159
  {kv}
160
  </div>
161
  """
 
src/tts.py CHANGED
@@ -1,6 +1,7 @@
1
  from __future__ import annotations
2
 
3
  from pathlib import Path
 
4
  import os
5
 
6
  import numpy as np
@@ -8,6 +9,32 @@ 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,
@@ -37,11 +64,13 @@ def generate_dj_voice(
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"Text-only DJ intro fallback. Kokoro 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 pathlib import Path
4
+ import hashlib
5
  import os
6
 
7
  import numpy as np
 
9
  from .audio_utils import save_audio
10
 
11
 
12
+ def generate_fallback_dj_signal(text: str, output_path: str) -> dict:
13
+ sr = 24000
14
+ words = max(4, min(28, len((text or "").split())))
15
+ duration = min(4.0, max(1.0, words * 0.16))
16
+ total = int(sr * duration)
17
+ t = np.arange(total) / sr
18
+ digest = hashlib.sha256((text or "timeline radio").encode("utf-8")).digest()
19
+ base = 150 + digest[0]
20
+ carrier = np.sin(2 * np.pi * base * t) * 0.045
21
+ radio = np.sin(2 * np.pi * (base * 1.5) * t) * 0.025
22
+ envelope = 0.6 + 0.4 * np.sin(2 * np.pi * (2.2 + digest[1] / 128) * t)
23
+ audio = (carrier + radio) * envelope
24
+ for index in range(words):
25
+ start = int((index / words) * total)
26
+ stop = min(total, start + int(sr * 0.055))
27
+ if stop > start:
28
+ audio[start:stop] += np.sin(2 * np.pi * (base + 120 + (index % 5) * 38) * t[: stop - start]) * 0.055
29
+ save_audio(output_path, audio.astype(np.float32), sr)
30
+ return {
31
+ "path": output_path,
32
+ "sample_rate": sr,
33
+ "tts_succeeded": False,
34
+ "fallback_audio": True,
35
+ }
36
+
37
+
38
  def generate_dj_voice(
39
  text: str,
40
  language_id: str,
 
64
 
65
  raise RuntimeError("Kokoro wrapper hook is present, but local TTS inference is not configured.")
66
  except Exception as exc:
67
+ fallback = generate_fallback_dj_signal(text, output_path)
68
  return {
69
+ "path": fallback["path"],
70
+ "sample_rate": fallback["sample_rate"],
71
+ "status": f"Fallback DJ radio-signal intro audio generated. Kokoro spoken TTS unavailable or unsupported here: {exc}",
72
  "tts_succeeded": False,
73
+ "fallback_audio": True,
74
  "model_attempted": True,
75
  "model_id": "hexgrad/Kokoro-82M",
76
  }
tests/test_app_logic.py CHANGED
@@ -76,6 +76,12 @@ class TurntableTimeMachineLogicTests(unittest.TestCase):
76
  self.assertIn(result[11], app.DURATIONS)
77
  self.assertIn("era-card", result[14])
78
 
 
 
 
 
 
 
79
  def test_music_wrapper_attempts_model_by_default_then_falls_back(self):
80
  result = generate_music(
81
  "Create an original music clip. No vocals. No lyrics.",
@@ -91,6 +97,25 @@ class TurntableTimeMachineLogicTests(unittest.TestCase):
91
  self.assertEqual(sr, 44100)
92
  self.assertGreater(audio.shape[0], 0)
93
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  def test_audio_model_calls_can_be_exercised_with_test_switch(self):
95
  os.environ["TTM_TEST_MODEL_CALLS"] = "1"
96
  music = generate_music(
@@ -137,7 +162,7 @@ class TurntableTimeMachineLogicTests(unittest.TestCase):
137
  "Clean digital master",
138
  "nostalgic and warm",
139
  "English",
140
- "English prompt for best stability",
141
  vocal_mode,
142
  "Night drive",
143
  1,
@@ -147,8 +172,9 @@ class TurntableTimeMachineLogicTests(unittest.TestCase):
147
  final_path, music_path, intro_path, _intro_text, lyrics_text, prompt, _card, status = result[:8]
148
  self.assertTrue(Path(final_path).exists())
149
  self.assertTrue(Path(music_path).exists())
150
- self.assertIsNone(intro_path)
151
  self.assertIn("ACE-Step", status)
 
152
  audio, sr = sf.read(final_path)
153
  self.assertEqual(sr, 44100)
154
  self.assertGreater(audio.shape[0], 0)
@@ -171,7 +197,7 @@ class TurntableTimeMachineLogicTests(unittest.TestCase):
171
  "Clean digital master",
172
  "nostalgic and warm",
173
  "English",
174
- "English prompt for best stability",
175
  "Original micro-lyrics",
176
  "Night drive",
177
  1,
 
76
  self.assertIn(result[11], app.DURATIONS)
77
  self.assertIn("era-card", result[14])
78
 
79
+ def test_prompt_mode_labels_are_plain_language(self):
80
+ self.assertEqual(
81
+ app.PROMPT_MODES,
82
+ ["Best stability (English)", "Broadcast language", "English + broadcast language"],
83
+ )
84
+
85
  def test_music_wrapper_attempts_model_by_default_then_falls_back(self):
86
  result = generate_music(
87
  "Create an original music clip. No vocals. No lyrics.",
 
97
  self.assertEqual(sr, 44100)
98
  self.assertGreater(audio.shape[0], 0)
99
 
100
+ def test_fallback_music_varies_by_prompt_route(self):
101
+ first = generate_music(
102
+ "Create an original music clip. Start from 1960s Motown-inspired soul. Transform it into 2000s filtered disco house.",
103
+ duration=1,
104
+ bpm=118,
105
+ seed=123,
106
+ output_path="outputs/test_route_one.wav",
107
+ )
108
+ second = generate_music(
109
+ "Create an original music clip. Start from 2020s lo-fi chill. Transform it into 1970s funk.",
110
+ duration=1,
111
+ bpm=118,
112
+ seed=123,
113
+ output_path="outputs/test_route_two.wav",
114
+ )
115
+ audio_one, _ = sf.read(first["path"])
116
+ audio_two, _ = sf.read(second["path"])
117
+ self.assertGreater(float(abs(audio_one - audio_two).mean()), 0.001)
118
+
119
  def test_audio_model_calls_can_be_exercised_with_test_switch(self):
120
  os.environ["TTM_TEST_MODEL_CALLS"] = "1"
121
  music = generate_music(
 
162
  "Clean digital master",
163
  "nostalgic and warm",
164
  "English",
165
+ "Best stability (English)",
166
  vocal_mode,
167
  "Night drive",
168
  1,
 
172
  final_path, music_path, intro_path, _intro_text, lyrics_text, prompt, _card, status = result[:8]
173
  self.assertTrue(Path(final_path).exists())
174
  self.assertTrue(Path(music_path).exists())
175
+ self.assertTrue(Path(intro_path).exists())
176
  self.assertIn("ACE-Step", status)
177
+ self.assertIn("Fallback DJ radio-signal intro audio generated", status)
178
  audio, sr = sf.read(final_path)
179
  self.assertEqual(sr, 44100)
180
  self.assertGreater(audio.shape[0], 0)
 
197
  "Clean digital master",
198
  "nostalgic and warm",
199
  "English",
200
+ "Best stability (English)",
201
  "Original micro-lyrics",
202
  "Night drive",
203
  1,