L0SG commited on
Commit
eab17e0
·
1 Parent(s): 8fa3716

Harden unified audio generation

Browse files
Files changed (4) hide show
  1. README.md +1 -1
  2. app.py +35 -12
  3. audio_utils.py +4 -2
  4. tts_utils.py +12 -13
README.md CHANGED
@@ -27,7 +27,7 @@ The hosted ZeroGPU runtime uses a prebuilt `mamba-ssm==2.3.2.post1` Blackwell wh
27
  ## Run locally
28
 
29
  ```bash
30
- git clone https://huggingface.co/spaces/L0SG/Nemotron-Labs-Audex
31
  cd Nemotron-Labs-Audex
32
  bash setup_local.sh
33
  bash run_local.sh
 
27
  ## Run locally
28
 
29
  ```bash
30
+ git clone https://huggingface.co/spaces/nvidia/Nemotron-Labs-Audex
31
  cd Nemotron-Labs-Audex
32
  bash setup_local.sh
33
  bash run_local.sh
app.py CHANGED
@@ -53,7 +53,7 @@ DEFAULT_WHEELS_DIR = APP_DIR / ("wheels" if IS_ZERO_GPU else ".local/wheels")
53
  DEFAULT_BUILD_DIR = APP_DIR / ("_wheelout" if IS_ZERO_GPU else ".local/build")
54
  WHEELS_DIR = Path(os.environ.get("AUDEX_WHEELS_DIR", str(DEFAULT_WHEELS_DIR)))
55
  BUILD_DIR = Path(os.environ.get("AUDEX_BUILD_DIR", str(DEFAULT_BUILD_DIR)))
56
- REPO_ID = os.environ.get("SPACE_ID", "L0SG/Nemotron-Labs-Audex")
57
  CAUSAL_CONV1D_VERSION = "1.6.2.post1"
58
  MAMBA_SSM_VERSION = "2.3.2.post1"
59
  CAUSAL_CONV1D_SPEC = f"causal-conv1d=={CAUSAL_CONV1D_VERSION}"
@@ -89,6 +89,9 @@ MAX_TOKEN_WARNING = (
89
  "⚠️ Maximum new-token limit reached; this output is incomplete. "
90
  "Increase Max new tokens and run again."
91
  )
 
 
 
92
  TEXT_VOCAB_SIZE = 131072
93
  TEXT_SEED = 100
94
  TEXT_SYSTEM_PROMPT = (
@@ -1098,6 +1101,7 @@ def _generate_tts(
1098
  chunks: list[np.ndarray] = []
1099
  first_token_seconds: float | None = None
1100
  total_tokens = 0
 
1101
  request_tag = hashlib.sha256(text.encode()).hexdigest()[:8]
1102
  started_at = time.perf_counter()
1103
  torch.cuda.reset_peak_memory_stats()
@@ -1110,6 +1114,7 @@ def _generate_tts(
1110
  try:
1111
  for segment_index, segment in enumerate(segments, start=1):
1112
  segment_tokens = 0
 
1113
  for event in stream_tts(
1114
  model=runtime.model,
1115
  tokenizer=runtime.tokenizer,
@@ -1122,6 +1127,8 @@ def _generate_tts(
1122
  guidance_scale=guidance_scale,
1123
  ):
1124
  segment_tokens = event.token_count
 
 
1125
  if event.token_count and first_token_seconds is None:
1126
  first_token_seconds = time.perf_counter() - started_at
1127
  if event.pcm is not None:
@@ -1144,6 +1151,9 @@ def _generate_tts(
1144
  )
1145
 
1146
  total_tokens += segment_tokens
 
 
 
1147
  if segment_index < len(segments):
1148
  chunks.append(
1149
  np.zeros(
@@ -1164,11 +1174,17 @@ def _generate_tts(
1164
  f"[gpu] tts_request={request_tag} tts_tokens={total_tokens} "
1165
  f"ttfc_seconds={first_token_seconds:.2f} "
1166
  f"tokens_per_second={token_rate:.2f} audio_seconds={waveform.size / SAMPLE_RATE:.2f} "
1167
- f"elapsed_seconds={elapsed_seconds:.2f} peak_memory_gib={peak_gib:.2f}",
 
1168
  flush=True,
1169
  )
 
 
 
 
 
1170
  yield (
1171
- f"Complete · {waveform.size / SAMPLE_RATE:.1f}s audio",
1172
  wav_path,
1173
  (
1174
  player_value(sequence, token_count=total_tokens, done=True)
@@ -1191,11 +1207,13 @@ def _split_sentence_segments(text: str) -> list[str]:
1191
 
1192
 
1193
  def _clean_transcription(text: str) -> str:
1194
- first_quote = text.find("'")
1195
- last_quote = text.rfind("'")
1196
- if first_quote != -1 and last_quote > first_quote:
1197
- return text[first_quote + 1 : last_quote].strip()
1198
- return text.strip()
 
 
1199
 
1200
 
1201
  def _compose_s2s_input(response_prompt: str, transcript: str) -> str:
@@ -1267,7 +1285,7 @@ def _generate_s2s(
1267
  raise gr.Error("Speech-to-speech response generation produced no final answer.")
1268
  yield answer, thinking, gr.skip(), gr.skip()
1269
 
1270
- for _, wav_path, player in _generate_tts(
1271
  runtime,
1272
  answer,
1273
  S2S_TTS_MAX_NEW_TOKENS,
@@ -1278,7 +1296,12 @@ def _generate_s2s(
1278
  token_limit=S2S_TTS_MAX_NEW_TOKENS,
1279
  segment_sentences=True,
1280
  ):
1281
- yield answer, thinking, wav_path or gr.skip(), player
 
 
 
 
 
1282
 
1283
 
1284
  @spaces.GPU(duration=_estimate_text, size="xlarge")
@@ -1354,7 +1377,7 @@ def run_unified(
1354
  guidance_scale: float,
1355
  ) -> Iterator[tuple[object, object, object, object]]:
1356
  """Run a selected Audex model for audio, text, or speech generation."""
1357
- yield "", "", gr.skip(), gr.skip()
1358
  runtime = _get_runtime(model_name)
1359
  settings = TASK_SPECS[task]
1360
  if settings["modality"] == "s2s":
@@ -1400,7 +1423,7 @@ def run_unified(
1400
  yield "Please provide an audio input.", "", gr.skip(), gr.skip()
1401
  return
1402
 
1403
- instruction = (prompt.strip() or str(settings["template"])).replace("<sound>", "").strip()
1404
  for answer, thinking in _generate(
1405
  runtime,
1406
  audio,
 
53
  DEFAULT_BUILD_DIR = APP_DIR / ("_wheelout" if IS_ZERO_GPU else ".local/build")
54
  WHEELS_DIR = Path(os.environ.get("AUDEX_WHEELS_DIR", str(DEFAULT_WHEELS_DIR)))
55
  BUILD_DIR = Path(os.environ.get("AUDEX_BUILD_DIR", str(DEFAULT_BUILD_DIR)))
56
+ REPO_ID = os.environ.get("SPACE_ID", "nvidia/Nemotron-Labs-Audex")
57
  CAUSAL_CONV1D_VERSION = "1.6.2.post1"
58
  MAMBA_SSM_VERSION = "2.3.2.post1"
59
  CAUSAL_CONV1D_SPEC = f"causal-conv1d=={CAUSAL_CONV1D_VERSION}"
 
89
  "⚠️ Maximum new-token limit reached; this output is incomplete. "
90
  "Increase Max new tokens and run again."
91
  )
92
+ TTS_TOKEN_WARNING = (
93
+ "⚠️ Maximum speech-token limit reached; generated audio may be incomplete."
94
+ )
95
  TEXT_VOCAB_SIZE = 131072
96
  TEXT_SEED = 100
97
  TEXT_SYSTEM_PROMPT = (
 
1101
  chunks: list[np.ndarray] = []
1102
  first_token_seconds: float | None = None
1103
  total_tokens = 0
1104
+ truncated = False
1105
  request_tag = hashlib.sha256(text.encode()).hexdigest()[:8]
1106
  started_at = time.perf_counter()
1107
  torch.cuda.reset_peak_memory_stats()
 
1114
  try:
1115
  for segment_index, segment in enumerate(segments, start=1):
1116
  segment_tokens = 0
1117
+ segment_truncated = False
1118
  for event in stream_tts(
1119
  model=runtime.model,
1120
  tokenizer=runtime.tokenizer,
 
1127
  guidance_scale=guidance_scale,
1128
  ):
1129
  segment_tokens = event.token_count
1130
+ if event.done:
1131
+ segment_truncated = event.truncated
1132
  if event.token_count and first_token_seconds is None:
1133
  first_token_seconds = time.perf_counter() - started_at
1134
  if event.pcm is not None:
 
1151
  )
1152
 
1153
  total_tokens += segment_tokens
1154
+ if segment_truncated:
1155
+ truncated = True
1156
+ break
1157
  if segment_index < len(segments):
1158
  chunks.append(
1159
  np.zeros(
 
1174
  f"[gpu] tts_request={request_tag} tts_tokens={total_tokens} "
1175
  f"ttfc_seconds={first_token_seconds:.2f} "
1176
  f"tokens_per_second={token_rate:.2f} audio_seconds={waveform.size / SAMPLE_RATE:.2f} "
1177
+ f"elapsed_seconds={elapsed_seconds:.2f} peak_memory_gib={peak_gib:.2f} "
1178
+ f"truncated={truncated}",
1179
  flush=True,
1180
  )
1181
+ status = (
1182
+ f"{TTS_TOKEN_WARNING} · {waveform.size / SAMPLE_RATE:.1f}s audio"
1183
+ if truncated
1184
+ else f"Complete · {waveform.size / SAMPLE_RATE:.1f}s audio"
1185
+ )
1186
  yield (
1187
+ status,
1188
  wav_path,
1189
  (
1190
  player_value(sequence, token_count=total_tokens, done=True)
 
1207
 
1208
 
1209
  def _clean_transcription(text: str) -> str:
1210
+ text = text.strip()
1211
+ quoted = re.fullmatch(
1212
+ r"(?is)(?:(?:the\s+)?(?:transcription|transcript)"
1213
+ r"(?:\s+(?:is|reads))?\s*:?\s*)?(['\"])(.*)\1[.!]?",
1214
+ text,
1215
+ )
1216
+ return quoted.group(2).strip() if quoted else text
1217
 
1218
 
1219
  def _compose_s2s_input(response_prompt: str, transcript: str) -> str:
 
1285
  raise gr.Error("Speech-to-speech response generation produced no final answer.")
1286
  yield answer, thinking, gr.skip(), gr.skip()
1287
 
1288
+ for status, wav_path, player in _generate_tts(
1289
  runtime,
1290
  answer,
1291
  S2S_TTS_MAX_NEW_TOKENS,
 
1296
  token_limit=S2S_TTS_MAX_NEW_TOKENS,
1297
  segment_sentences=True,
1298
  ):
1299
+ visible_answer = (
1300
+ f"{answer}\n\n{TTS_TOKEN_WARNING}"
1301
+ if TTS_TOKEN_WARNING in status
1302
+ else answer
1303
+ )
1304
+ yield visible_answer, thinking, wav_path or gr.skip(), player
1305
 
1306
 
1307
  @spaces.GPU(duration=_estimate_text, size="xlarge")
 
1377
  guidance_scale: float,
1378
  ) -> Iterator[tuple[object, object, object, object]]:
1379
  """Run a selected Audex model for audio, text, or speech generation."""
1380
+ yield "", "", None, gr.skip()
1381
  runtime = _get_runtime(model_name)
1382
  settings = TASK_SPECS[task]
1383
  if settings["modality"] == "s2s":
 
1423
  yield "Please provide an audio input.", "", gr.skip(), gr.skip()
1424
  return
1425
 
1426
+ instruction = prompt.strip() or str(settings["template"])
1427
  for answer, thinking in _generate(
1428
  runtime,
1429
  audio,
audio_utils.py CHANGED
@@ -136,10 +136,12 @@ def build_prompt_template(
136
  raise ValueError(f"Unknown prompt repetition mode: {prompt_repitition}")
137
  if prompt_repitition == "repetition":
138
  prompt = f"{prompt}\n{prompt}"
 
 
139
 
140
  if reasoning:
141
- return f"<|im_start|>user\n<sound>\n{prompt}<|im_end|>\n<|im_start|>assistant\n<think>\n"
142
- return f"<|im_start|>user\n<sound>\n{prompt}<|im_end|>\n<|im_start|>assistant\n<think></think>"
143
 
144
 
145
  def expand_sound_placeholder(prompt: str, num_embeddings: int) -> str:
 
136
  raise ValueError(f"Unknown prompt repetition mode: {prompt_repitition}")
137
  if prompt_repitition == "repetition":
138
  prompt = f"{prompt}\n{prompt}"
139
+ if SOUND_PLACEHOLDER not in prompt:
140
+ prompt = f"{SOUND_PLACEHOLDER}\n{prompt}"
141
 
142
  if reasoning:
143
+ return f"<|im_start|>user\n{prompt}<|im_end|>\n<|im_start|>assistant\n<think>\n"
144
+ return f"<|im_start|>user\n{prompt}<|im_end|>\n<|im_start|>assistant\n<think></think>"
145
 
146
 
147
  def expand_sound_placeholder(prompt: str, num_embeddings: int) -> str:
tts_utils.py CHANGED
@@ -41,6 +41,7 @@ class TTSStreamEvent:
41
  token_count: int
42
  elapsed_seconds: float
43
  done: bool = False
 
44
 
45
 
46
  class SpeechTokenLogitsProcessor(LogitsProcessor):
@@ -256,11 +257,11 @@ def stream_tts(
256
  thread = Thread(target=generate, daemon=True)
257
  thread.start()
258
  token_count = 0
259
- completed = False
260
  try:
261
  for token_id in streamer:
262
  if token_id == token_map.end or token_id == token_map.eos:
263
- completed = True
264
  break
265
  if not token_map.codec_start <= token_id <= token_map.codec_end:
266
  raise RuntimeError(f"Unexpected token in TTS output: {token_id}")
@@ -279,24 +280,22 @@ def stream_tts(
279
  token_count=token_count,
280
  elapsed_seconds=time.perf_counter() - started_at,
281
  )
282
- else:
283
- completed = True
284
 
285
  if generation_error:
286
  raise generation_error[0]
287
- if completed:
288
- for _, pcm in session.flush():
289
- yield TTSStreamEvent(
290
- pcm=np.asarray(pcm, dtype=np.float32),
291
- token_count=token_count,
292
- elapsed_seconds=time.perf_counter() - started_at,
293
- )
294
  yield TTSStreamEvent(
295
- pcm=None,
296
  token_count=token_count,
297
  elapsed_seconds=time.perf_counter() - started_at,
298
- done=True,
299
  )
 
 
 
 
 
 
 
300
  finally:
301
  cancel_event.set()
302
  thread.join(timeout=10)
 
41
  token_count: int
42
  elapsed_seconds: float
43
  done: bool = False
44
+ truncated: bool = False
45
 
46
 
47
  class SpeechTokenLogitsProcessor(LogitsProcessor):
 
257
  thread = Thread(target=generate, daemon=True)
258
  thread.start()
259
  token_count = 0
260
+ terminal_token_seen = False
261
  try:
262
  for token_id in streamer:
263
  if token_id == token_map.end or token_id == token_map.eos:
264
+ terminal_token_seen = True
265
  break
266
  if not token_map.codec_start <= token_id <= token_map.codec_end:
267
  raise RuntimeError(f"Unexpected token in TTS output: {token_id}")
 
280
  token_count=token_count,
281
  elapsed_seconds=time.perf_counter() - started_at,
282
  )
 
 
283
 
284
  if generation_error:
285
  raise generation_error[0]
286
+ for _, pcm in session.flush():
 
 
 
 
 
 
287
  yield TTSStreamEvent(
288
+ pcm=np.asarray(pcm, dtype=np.float32),
289
  token_count=token_count,
290
  elapsed_seconds=time.perf_counter() - started_at,
 
291
  )
292
+ yield TTSStreamEvent(
293
+ pcm=None,
294
+ token_count=token_count,
295
+ elapsed_seconds=time.perf_counter() - started_at,
296
+ done=True,
297
+ truncated=not terminal_token_seen,
298
+ )
299
  finally:
300
  cancel_event.set()
301
  thread.join(timeout=10)