CodeAntidote commited on
Commit
65d4fec
·
verified ·
1 Parent(s): 99c236c

Replace Kids Rhyme Studio with clean updated build

Browse files
Files changed (1) hide show
  1. audio_engine.py +276 -0
audio_engine.py ADDED
@@ -0,0 +1,276 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Generate a short song with sung vocals from the edited rhyme."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ import re
8
+ import time
9
+ import traceback
10
+
11
+ # Import spaces before torch/diffusers so ZeroGPU can install its CUDA shim.
12
+ try:
13
+ import spaces
14
+ except ModuleNotFoundError:
15
+ if os.environ.get("SPACE_ID"):
16
+ raise # A Space needs the ZeroGPU runtime and its spaces module.
17
+
18
+ # Local CPU machines can still open the form and write rhymes.
19
+ class _LocalSpaces:
20
+ @staticmethod
21
+ def GPU(**_kwargs):
22
+ return lambda fn: fn
23
+
24
+ spaces = _LocalSpaces()
25
+
26
+ import numpy as np
27
+ from rhyme_engine import LANGUAGES, validate_lyric_for_audio
28
+
29
+ logging.basicConfig(level=logging.INFO)
30
+ logger = logging.getLogger("kids_rhyme.audio")
31
+
32
+ MODEL_ID = "ACE-Step/acestep-v15-xl-turbo-diffusers"
33
+ VOCAL_LANGUAGE = {key: value["code"] for key, value in LANGUAGES.items()}
34
+ FALLBACK_SAMPLE_RATE = 48000
35
+
36
+ # Turn this on temporarily in Hugging Face Settings -> Variables and secrets:
37
+ # KIDS_DEBUG=1
38
+ # Leave it unset (or set it to 0) when the public app is working.
39
+ DEBUG_ERRORS = os.environ.get("KIDS_DEBUG", "0") == "1"
40
+
41
+ # ZeroGPU expects the model to be placed on CUDA at module import time. Its
42
+ # CUDA shim makes this possible before the real GPU is allocated to a request.
43
+ _pipe = None
44
+ _load_error = None
45
+ try:
46
+ import torch
47
+ except ModuleNotFoundError:
48
+ torch = None
49
+
50
+ if torch is not None and torch.cuda.is_available():
51
+ try:
52
+ from diffusers import AceStepPipeline
53
+
54
+ # diffusers versions have used both names; support either without
55
+ # changing the rest of the app.
56
+ try:
57
+ _pipe = AceStepPipeline.from_pretrained(MODEL_ID, dtype=torch.bfloat16)
58
+ except TypeError:
59
+ _pipe = AceStepPipeline.from_pretrained(MODEL_ID, torch_dtype=torch.bfloat16)
60
+
61
+ _pipe = _pipe.to("cuda")
62
+ logger.info("Singing model loaded: %s", MODEL_ID)
63
+ except Exception as exc:
64
+ _load_error = exc
65
+ logger.error("Singing model failed to load:\n%s", traceback.format_exc())
66
+
67
+
68
+ def _debug_detail(exc: BaseException) -> str:
69
+ """Return a short browser-safe debug suffix only while KIDS_DEBUG=1."""
70
+ if not DEBUG_ERRORS:
71
+ return ""
72
+ message = str(exc).replace("\n", " ").strip()
73
+ return f" [debug: {type(exc).__name__}: {message[:500]}]"
74
+
75
+
76
+ def _require_pipeline():
77
+ if _pipe is None:
78
+ if _load_error is not None:
79
+ raise RuntimeError(
80
+ "The singing model could not load. Check the Space logs."
81
+ + _debug_detail(_load_error)
82
+ ) from _load_error
83
+ raise RuntimeError(
84
+ "Singing needs GPU hardware. Set the Space hardware to ZeroGPU."
85
+ )
86
+ return _pipe
87
+
88
+
89
+ def _sample_rate() -> int:
90
+ pipe = _require_pipeline()
91
+ rate = getattr(pipe, "sample_rate", None)
92
+ if rate is None:
93
+ vae_cfg = getattr(getattr(pipe, "vae", None), "config", None)
94
+ rate = getattr(vae_cfg, "sampling_rate", None)
95
+ if rate is None:
96
+ logger.warning(
97
+ "No sample rate found on pipeline; using %s Hz for %s",
98
+ FALLBACK_SAMPLE_RATE,
99
+ MODEL_ID,
100
+ )
101
+ rate = FALLBACK_SAMPLE_RATE
102
+ if isinstance(rate, (bool, np.bool_)) or not isinstance(rate, (int, np.integer)) or rate <= 0:
103
+ raise ValueError(f"The pipeline returned an invalid sample rate: {rate!r}")
104
+ return int(rate)
105
+
106
+
107
+ def _song_lyrics(text: str) -> str:
108
+ """Preserve every edited line and mark its two parts for the song model."""
109
+ text = text.replace("\r\n", "\n").replace("\r", "\n")
110
+ stanzas = [part.strip() for part in re.split(r"\n\s*\n", text) if part.strip()]
111
+ if len(stanzas) >= 2:
112
+ first = stanzas[0]
113
+ second = "\n".join(stanzas[1:])
114
+ else:
115
+ lines = [line.strip() for line in text.splitlines() if line.strip()]
116
+ if len(lines) < 2:
117
+ raise ValueError("Add at least two short lines to sing.")
118
+ midpoint = (len(lines) + 1) // 2
119
+ first, second = "\n".join(lines[:midpoint]), "\n".join(lines[midpoint:])
120
+ return f"[verse]\n{first}\n[chorus]\n{second}"
121
+
122
+
123
+ def _song_settings(
124
+ text: str,
125
+ language: str,
126
+ mood: str,
127
+ theme: str = "",
128
+ theme_prompt: str = "",
129
+ ) -> dict:
130
+ text = validate_lyric_for_audio(text)
131
+ if language not in VOCAL_LANGUAGE or mood not in ("Bouncy", "Calm"):
132
+ raise ValueError("Write a rhyme first to select its language and music mood.")
133
+
134
+ if mood == "Calm":
135
+ prompt = (
136
+ "Gentle original children's lullaby, a clear warm voice SINGING a simple "
137
+ "memorable melody in the language of the lyrics. Soft piano, glockenspiel, "
138
+ "light acoustic guitar, slow swaying rhythm. Vocal-forward mix. "
139
+ "Sing the supplied lyrics; no spoken words or narration."
140
+ )
141
+ bpm = 82
142
+ else:
143
+ prompt = (
144
+ "Playful original children's sing-along, a clear cheerful voice SINGING "
145
+ "a simple catchy melody in the language of the lyrics. Ukulele, handclaps, "
146
+ "toy piano, bright steady beat. Vocal-forward mix. "
147
+ "Sing the supplied lyrics; no spoken words or narration."
148
+ )
149
+ bpm = 112
150
+
151
+ language_name = LANGUAGES[language]["name"]
152
+ prompt = f"Sing all vocals in {language_name}. " + prompt
153
+ if theme:
154
+ prompt += f" Song theme: {theme}."
155
+ if theme_prompt:
156
+ prompt += f" Topic: {theme_prompt}."
157
+
158
+ line_count = sum(bool(line.strip()) for line in text.splitlines())
159
+ return {
160
+ "prompt": prompt,
161
+ "lyrics": _song_lyrics(text),
162
+ "vocal_language": VOCAL_LANGUAGE[language],
163
+ # 40 seconds for a normal rhyme, with a little extra room for longer text.
164
+ "audio_duration": min(56.0, 40.0 + 4.0 * max(0, line_count - 8)),
165
+ "num_inference_steps": 8,
166
+ "bpm": bpm,
167
+ "task_type": "text2music",
168
+ "output_type": "np",
169
+ }
170
+
171
+
172
+ def _extract_audio(result) -> np.ndarray:
173
+ """Extract ACE-Step audio and normalize it to [channels, samples]."""
174
+ value = None
175
+
176
+ # Current diffusers ACE-Step output uses `audios`.
177
+ if hasattr(result, "audios"):
178
+ value = result.audios
179
+ elif hasattr(result, "audio"):
180
+ value = result.audio
181
+ elif isinstance(result, dict):
182
+ for key in ("audios", "audio", "waveform", "sample"):
183
+ if key in result:
184
+ value = result[key]
185
+ break
186
+ elif isinstance(result, (tuple, list)) and result:
187
+ value = result[0]
188
+
189
+ if value is None:
190
+ raise ValueError(f"No audio was returned by {type(result).__name__}.")
191
+
192
+ if hasattr(value, "detach"):
193
+ value = value.detach().float().cpu().numpy()
194
+
195
+ arr = np.asarray(value, dtype=np.float32)
196
+
197
+ # Typical result: [batch, channels, samples]. Take first batch.
198
+ while arr.ndim > 2:
199
+ arr = arr[0]
200
+ if arr.ndim == 1:
201
+ arr = arr[None, :]
202
+ if arr.ndim != 2:
203
+ raise ValueError(f"Unexpected audio rank/shape {arr.shape}.")
204
+
205
+ # If it came back [samples, channels], transpose it.
206
+ if arr.shape[0] > 2 and arr.shape[1] in (1, 2):
207
+ arr = arr.T
208
+
209
+ return np.ascontiguousarray(arr)
210
+
211
+
212
+ def _polish(wave: np.ndarray, sr: int) -> np.ndarray:
213
+ """Peak-normalize and fade the tail so playback ends cleanly."""
214
+ wave = np.array(wave, dtype=np.float32, copy=True)
215
+ peak = float(np.max(np.abs(wave)))
216
+ if peak > 0:
217
+ wave *= 0.89 / peak
218
+ n = min(int(0.6 * sr), wave.shape[1] // 4)
219
+ if n > 0:
220
+ wave[:, -n:] *= np.linspace(1.0, 0.0, n, dtype=np.float32)
221
+ return wave
222
+
223
+
224
+ def generate_song(settings: dict) -> tuple[int, np.ndarray]:
225
+ """Inference core. The caller must provide GPU access."""
226
+ pipe = _require_pipeline()
227
+ started = time.monotonic()
228
+
229
+ result = pipe(**settings)
230
+ waveform = _extract_audio(result)
231
+ sr = _sample_rate()
232
+
233
+ logger.info("ACE-Step returned audio shape %s at %s Hz", waveform.shape, sr)
234
+
235
+ if waveform.shape[0] not in (1, 2) or waveform.shape[1] < sr:
236
+ raise ValueError(f"Unexpected audio shape {waveform.shape} at {sr} Hz")
237
+ if not np.isfinite(waveform).all() or float(np.max(np.abs(waveform))) < 0.005:
238
+ raise ValueError("Model returned empty or invalid audio")
239
+
240
+ waveform = _polish(waveform, sr)
241
+ pcm = (np.clip(waveform.T, -1.0, 1.0) * 32767).astype(np.int16)
242
+
243
+ logger.info(
244
+ "Generated %.1fs of audio at %s Hz in %.1fs",
245
+ len(pcm) / sr,
246
+ sr,
247
+ time.monotonic() - started,
248
+ )
249
+ return sr, pcm
250
+
251
+
252
+ @spaces.GPU(duration=120)
253
+ def _generate_on_gpu(settings: dict) -> tuple[int, np.ndarray]:
254
+ return generate_song(settings)
255
+
256
+
257
+ def make_sung_song(
258
+ text: str,
259
+ language: str,
260
+ mood: str,
261
+ theme: str = "",
262
+ theme_prompt: str = "",
263
+ ) -> tuple[tuple[int, np.ndarray], str]:
264
+ """Validate first, then generate. Keep the app's five-argument API."""
265
+ settings = _song_settings(text, language, mood, theme, theme_prompt)
266
+ _require_pipeline()
267
+
268
+ try:
269
+ audio = _generate_on_gpu(settings)
270
+ except Exception as exc:
271
+ logger.error("Song generation failed:\n%s", traceback.format_exc())
272
+ raise RuntimeError(
273
+ "Singing failed this time. Please try again." + _debug_detail(exc)
274
+ ) from exc
275
+
276
+ return audio, "Your sung song is ready to listen to and download."