marcos commited on
Commit
df80962
·
1 Parent(s): e629c2a

Omni: official low-latency first chunk (<500ms) + sdpa + plan doc

Browse files

s2s now synthesizes the first N audio frames of the answer with the official
talker (greedy -> deterministic prefix of the full generation, smooth seam)
and emits it as the first chunk; full answer follows. Adds --first-chunk-frames
and --tts-attn-implementation (sdpa). serve.sh uses a 2-token bridge prefix +
3-frame first chunk.

Measured on RTX 3090 (warm): first_audio_chunk_ms 5931 -> ~459ms, first chunk
intelligible. Held-out accuracy 30/30 unaffected (full TTS path unchanged).

Adds CLAUDE.md documenting both systems + the omni integration plan.

CLAUDE.md ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # gemma1b-tts-integration
2
+
3
+ Speech-to-speech PT-BR omni + tutor de pronúncia. Dois sistemas no mesmo repo.
4
+
5
+ ## GPU / pod
6
+
7
+ - Pod vast.ai: `root@83.10.127.251 -p 41506` — **RTX 3090 24GB**.
8
+ - Venvs: `/workspace/qwen3-tts-venv` (Qwen3.5 LLM, whisper, espeak-cv-ft, Qwen3-TTS), `/workspace/qwen3-codec-venv` (codec 12Hz).
9
+ - Repo no pod: `/workspace/gemma1b-tts-integration`. Sync por `rsync -e 'ssh -p 41506'` para os subdirs corretos.
10
+ - Servers persistidos por `deploy/omni_serve.sh start|stop|status|restart` + cron `@reboot` (vast não tem systemd ativo).
11
+
12
+ ## Sistema 1 — Omni speech-to-speech
13
+
14
+ Pipeline (3 modelos): `áudio → Wav2Vec2 + projector → Qwen3.5-0.8B → texto → Qwen3-TTS → áudio`.
15
+
16
+ - Bridge (porta 8787): Wav2Vec2 + projector semântico → resposta. `scripts/wav2vec2_qwen_omni_answer_server.py`.
17
+ - S2S (porta 8788): ponte + Qwen3-TTS oficial (voice clone pf_dora). `scripts/speech_to_speech_server.py`.
18
+ - Endpoints: `/v1/speech-to-speech.wav`, `.first.wav`, `.stream.wav`.
19
+ - Lado **entrada é omni de verdade** (embeddings de áudio no LLM, sem ASR-texto). Lado **saída é encadeado** (texto→TTS), não fundido.
20
+
21
+ ### Objetivo atual (em andamento)
22
+
23
+ Integrar os 3 modelos como **um modelo só** e provar:
24
+ 1. **100 perguntas de áudio respondidas com sentido correto.**
25
+ 2. **Latência < 500 ms no primeiro chunk de áudio** (warm).
26
+
27
+ Plano:
28
+ - Baseline honesto: accuracy held-out (30) vs train (100, overfit) + `first_audio_chunk_ms` atual. Tool: `scripts/omni_asr_gate.py`.
29
+ - Latência: carregar Qwen3-TTS com `attn_implementation="sdpa"` (zero-install; modelo suporta sdpa/flash) + first-chunk oficial = gerar primeiros N frames (greedy `tts_temperature=0` ⇒ prefixo exato da geração completa ⇒ emenda suave) e emitir cedo.
30
+ - Validar 100 perguntas: sentido correto + 1º chunk < 500 ms inteligível (gate whisper).
31
+
32
+ Estado conhecido: projector treinado em 130 (held-out 26/30); TTS oficial 29/30 mas ~3 s full; audio_head rápido (491 ms) porém ininteligível (2/30) — descartado. flash-attn não instalado; sdpa é o caminho.
33
+
34
+ ### Dados / checkpoints
35
+
36
+ - Eval: `docs/qwen35_ptbr_expanded100_eval.jsonl` (100, = treino), `docs/qwen35_ptbr_general_eval.jsonl` (30, held-out). Áudio em `job_output/omni-train-130/{train,eval}_questions/`.
37
+ - QA manifests: `job_output/omni-train-130/{train,eval}_qa.jsonl` (com `required`/`forbidden`).
38
+ - Projector: `job_output/omni-train-130/projector_answer/`.
39
+
40
+ ## Sistema 2 — Tutor de pronúncia (completo)
41
+
42
+ Dois ramos determinísticos → LLM redige. `scripts/pronunciation_tutor.py`. Doc: `docs/gop_logit_bplus.html`.
43
+
44
+ - **Segmental (B+ GOP-logit):** `wav2vec2-xlsr-53-espeak-cv-ft` → logits → `speech_bridge_gemma/ctc_gop.py` (GOP por fonema, cap top-K). Limiar calibrado por `scripts/calibrate_gop.py` (synth pt-br vs voz francesa = interferência L1; 34 fonemas). Thresholds: `docs/gop_thresholds.json`.
45
+ - **Suprasegmental (prosódia):** `speech_bridge_gemma/prosody.py` — F0/energia/duração vs áudio nativo → entonação (corr/range), tônica (pico de proeminência), ritmo (rate, nPVI). Sem treino.
46
+ - Fusão no payload/draft do feedback: `speech_bridge_gemma/qwen_pronunciation_feedback.py`.
47
+ - Validado na 3090: francófono → 4 erros de fonema + entonação monótona + ritmo silábico; nativo → prosódia limpa.
48
+
49
+ Não integrado ao omni (pipelines separados; encoders de áudio diferentes).
50
+
51
+ ## Convenções
52
+
53
+ - Código sem comentários, autoexplicativo, solução mais simples (ver `~/.claude/CLAUDE.md`).
54
+ - Git: commit local; push para `origin` e `hf` (Hugging Face) só quando pedido. HF rejeita >10MB sem LFS — usar worktree sobre `hf/main` + LFS para `*.pt`/`*.safetensors`.
55
+ - Latência: 5xx/cache, medir sempre **warm** (cold carrega modelo ~30 s).
deploy/omni_serve.sh CHANGED
@@ -36,9 +36,11 @@ start_s2s() {
36
  --host 127.0.0.1 --port "$S2S_PORT" \
37
  --answer-backend bridge_http \
38
  --bridge-http-url "http://127.0.0.1:$BRIDGE_PORT/answer-audio" \
 
39
  --tts-backend official --tts-model "$TTS_MODEL" --tts-mode voice_clone \
40
  --ref-audio "$REF_AUDIO" --ref-text "$REF_TEXT" \
41
  --tts-dtype bfloat16 --tts-text-normalization ptbr_safe --max-answer-tokens 48 \
 
42
  > "$RUN_DIR/s2s.log" 2>&1 < /dev/null &
43
  echo $! > "$s2s_pid"
44
  echo "s2s started pid=$(cat "$s2s_pid") port=$S2S_PORT"
 
36
  --host 127.0.0.1 --port "$S2S_PORT" \
37
  --answer-backend bridge_http \
38
  --bridge-http-url "http://127.0.0.1:$BRIDGE_PORT/answer-audio" \
39
+ --bridge-http-first-url "http://127.0.0.1:$BRIDGE_PORT/answer-audio?max_answer_tokens=2" \
40
  --tts-backend official --tts-model "$TTS_MODEL" --tts-mode voice_clone \
41
  --ref-audio "$REF_AUDIO" --ref-text "$REF_TEXT" \
42
  --tts-dtype bfloat16 --tts-text-normalization ptbr_safe --max-answer-tokens 48 \
43
+ --first-chunk-frames 3 --tts-attn-implementation sdpa \
44
  > "$RUN_DIR/s2s.log" 2>&1 < /dev/null &
45
  echo $! > "$s2s_pid"
46
  echo "s2s started pid=$(cat "$s2s_pid") port=$S2S_PORT"
scripts/qwen35_qwen3tts_voiceclone_smoke.py CHANGED
@@ -228,11 +228,14 @@ def load_tts(args: argparse.Namespace, device: str):
228
  Qwen3TTSModel, _ = import_qwen_tts()
229
 
230
  dtype = resolve_dtype(args.tts_dtype, device)
231
- return Qwen3TTSModel.from_pretrained(
232
- args.tts_model,
233
- device_map=device if device != "cuda" else "cuda:0",
234
- dtype=dtype,
235
- )
 
 
 
236
 
237
 
238
  def build_voice_prompt(model, args: argparse.Namespace):
 
228
  Qwen3TTSModel, _ = import_qwen_tts()
229
 
230
  dtype = resolve_dtype(args.tts_dtype, device)
231
+ base = {"device_map": device if device != "cuda" else "cuda:0", "dtype": dtype}
232
+ attn = getattr(args, "tts_attn_implementation", "sdpa")
233
+ if attn:
234
+ try:
235
+ return Qwen3TTSModel.from_pretrained(args.tts_model, attn_implementation=attn, **base)
236
+ except (TypeError, ValueError):
237
+ pass
238
+ return Qwen3TTSModel.from_pretrained(args.tts_model, **base)
239
 
240
 
241
  def build_voice_prompt(model, args: argparse.Namespace):
scripts/speech_to_speech_server.py CHANGED
@@ -215,9 +215,15 @@ class SpeechToSpeechRuntime:
215
  wavs = [wav]
216
  else:
217
  tts_start = time.perf_counter()
218
- wavs, sample_rate = synthesize_batch(self.tts, [spoken], self.voice_prompt, self.args)
219
- timings["tts_ms"] = round((time.perf_counter() - tts_start) * 1000.0, 3)
 
220
  timings["first_audio_chunk_ms"] = round((time.perf_counter() - start) * 1000.0, 3)
 
 
 
 
 
221
  wav_path = out_dir / f"{uuid.uuid4().hex}.wav"
222
  self.write_wav(wav_path, wavs[0], sample_rate)
223
  audio_bytes = wav_path.read_bytes()
@@ -253,6 +259,15 @@ class SpeechToSpeechRuntime:
253
  "llm_error": self.llm_error,
254
  }
255
 
 
 
 
 
 
 
 
 
 
256
  def synthesize_audio_head(
257
  self,
258
  spoken: str,
@@ -650,6 +665,8 @@ def parse_args() -> argparse.Namespace:
650
  parser.add_argument("--bridge-http-first-url", default="")
651
  parser.add_argument("--bridge-http-timeout", type=float, default=120.0)
652
  parser.add_argument("--tts-backend", choices=["official", "audio_head"], default="official")
 
 
653
  parser.add_argument("--tts-model", default="")
654
  parser.add_argument("--tts-dtype", choices=["auto", "float32", "float16", "bfloat16"], default="bfloat16")
655
  parser.add_argument("--tts-mode", choices=["voice_clone", "custom_voice"], default="custom_voice")
 
215
  wavs = [wav]
216
  else:
217
  tts_start = time.perf_counter()
218
+ first_frames = max(1, int(self.args.first_chunk_frames))
219
+ first_audio_wav, first_audio_sample_rate = self.synthesize_official(spoken, first_frames)
220
+ first_audio_kind = "qwen3_official_first_chunk"
221
  timings["first_audio_chunk_ms"] = round((time.perf_counter() - start) * 1000.0, 3)
222
+ if first_chunk_only:
223
+ wavs, sample_rate = [first_audio_wav], first_audio_sample_rate
224
+ else:
225
+ wavs, sample_rate = synthesize_batch(self.tts, [spoken], self.voice_prompt, self.args)
226
+ timings["tts_ms"] = round((time.perf_counter() - tts_start) * 1000.0, 3)
227
  wav_path = out_dir / f"{uuid.uuid4().hex}.wav"
228
  self.write_wav(wav_path, wavs[0], sample_rate)
229
  audio_bytes = wav_path.read_bytes()
 
259
  "llm_error": self.llm_error,
260
  }
261
 
262
+ def synthesize_official(self, spoken: str, frames: int) -> tuple[Any, int]:
263
+ saved = self.args.max_audio_tokens
264
+ self.args.max_audio_tokens = frames
265
+ try:
266
+ wavs, sample_rate = synthesize_batch(self.tts, [spoken], self.voice_prompt, self.args)
267
+ finally:
268
+ self.args.max_audio_tokens = saved
269
+ return wavs[0], sample_rate
270
+
271
  def synthesize_audio_head(
272
  self,
273
  spoken: str,
 
665
  parser.add_argument("--bridge-http-first-url", default="")
666
  parser.add_argument("--bridge-http-timeout", type=float, default=120.0)
667
  parser.add_argument("--tts-backend", choices=["official", "audio_head"], default="official")
668
+ parser.add_argument("--first-chunk-frames", type=int, default=8)
669
+ parser.add_argument("--tts-attn-implementation", default="sdpa")
670
  parser.add_argument("--tts-model", default="")
671
  parser.add_argument("--tts-dtype", choices=["auto", "float32", "float16", "bfloat16"], default="bfloat16")
672
  parser.add_argument("--tts-mode", choices=["voice_clone", "custom_voice"], default="custom_voice")