Improve Inference Code
Browse files- .gitattributes +5 -0
- HANDOFF.md +191 -0
- STATUS.md +286 -0
- VIBEVOICE_LICENSE +21 -0
- common.py +82 -13
- cpu_fp32/acoustic_encoder.onnx +2 -2
- cpu_int4/acoustic_encoder.onnx +2 -2
- fp32.wav +3 -0
- inference.py +130 -96
- inference_asr.py +123 -123
- inference_tokenizer.py +119 -0
- int4.wav +3 -0
- optimize.py +16 -1
- pyproject.toml +5 -7
- samples/text_examples/1p_abs.txt +3 -0
- samples/voices/en-Alice_woman.wav +3 -0
- tts_out.wav +3 -0
- user_script.py +32 -37
- vibevoice/__init__.py +16 -0
- vibevoice/configs/qwen2.5_1.5b_64k.json +112 -0
- vibevoice/configs/qwen2.5_7b_32k.json +113 -0
- vibevoice/modular/__init__.py +14 -0
- vibevoice/modular/configuration_vibevoice.py +406 -0
- vibevoice/modular/configuration_vibevoice_streaming.py +104 -0
- vibevoice/modular/modeling_vibevoice.py +496 -0
- vibevoice/modular/modeling_vibevoice_asr.py +522 -0
- vibevoice/modular/modeling_vibevoice_streaming.py +190 -0
- vibevoice/modular/modeling_vibevoice_streaming_inference.py +906 -0
- vibevoice/modular/modular_vibevoice_diffusion_head.py +287 -0
- vibevoice/modular/modular_vibevoice_text_tokenizer.py +313 -0
- vibevoice/modular/modular_vibevoice_tokenizer.py +1207 -0
- vibevoice/modular/streamer.py +264 -0
- vibevoice/processor/__init__.py +11 -0
- vibevoice/processor/audio_utils.py +217 -0
- vibevoice/processor/vibevoice_asr_processor.py +572 -0
- vibevoice/processor/vibevoice_processor.py +692 -0
- vibevoice/processor/vibevoice_streaming_processor.py +409 -0
- vibevoice/processor/vibevoice_tokenizer_processor.py +413 -0
- vibevoice/schedule/__init__.py +0 -0
- vibevoice/schedule/dpm_solver.py +1065 -0
- vibevoice/schedule/timestep_sampler.py +19 -0
- vibevoice/scripts/__init__.py +0 -0
- why1.wav +3 -0
.gitattributes
CHANGED
|
@@ -35,3 +35,8 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
*.jpg filter=lfs diff=lfs merge=lfs -text
|
| 37 |
*.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
*.jpg filter=lfs diff=lfs merge=lfs -text
|
| 37 |
*.png filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
fp32.wav filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
int4.wav filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
samples/voices/en-Alice_woman.wav filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
tts_out.wav filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
why1.wav filter=lfs diff=lfs merge=lfs -text
|
HANDOFF.md
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# VibeVoice → ONNX — Engineer's Handoff Note
|
| 2 |
+
|
| 3 |
+
_Written by the departing engineer. Everything here was learned the hard way; read it before
|
| 4 |
+
touching the code. Companion files: `STATUS.md` (running log), `.claude/skills/vibevoice-onnx/`
|
| 5 |
+
(the operational skill), and the checklist at the bottom of this note._
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
## 1. The one-paragraph mental model
|
| 10 |
+
|
| 11 |
+
The VibeVoice family is **one Qwen2 LLM backbone + audio VAE tokenizers + small glue MLPs**, in
|
| 12 |
+
four checkpoints that *look* alike but differ in critical details. We do NOT export the composite
|
| 13 |
+
model. We decompose each checkpoint into standalone ONNX sub-parts — LLM decoder via
|
| 14 |
+
onnxruntime-genai **ModelBuilder** (int4/fp16, KV cache), everything else via **Olive**
|
| 15 |
+
(dynamo exporter) — and re-compose them at inference time in plain Python + onnxruntime
|
| 16 |
+
(`common.py`). Every exported part is parity-checked against PyTorch (cosine ~1.0) before we
|
| 17 |
+
trust it. That decomposition + parity discipline is the whole method.
|
| 18 |
+
|
| 19 |
+
## 2. The four checkpoints — what actually differs (memorize this table)
|
| 20 |
+
|
| 21 |
+
| Key | HF layout | LLM | lm_head? | Acoustic tokenizer naming | Loads via |
|
| 22 |
+
|---|---|---|---|---|---|
|
| 23 |
+
| `1.5b` | codes/ (`vibevoice`) | Qwen2.5-1.5B, 28L | **No** (head = diffusion) | `encoder.downsample_layers.*` | `codes/` |
|
| 24 |
+
| `asr` | codes/ (`vibevoice`, arch `VibeVoiceForASRTraining`) | Qwen2.5-**7B** | **Yes** (top-level `lm_head.weight`) | same as 1.5b | `codes/` |
|
| 25 |
+
| `asr-hf` | transformers-native (`vibevoice_asr`) | Qwen2.5-**7B** | **Yes** (`language_model.lm_head.*`) | `acoustic_tokenizer_encoder.conv_layers.*` | transformers |
|
| 26 |
+
| `realtime` | codes/ (`vibevoice_streaming`) | Qwen2.5-0.5B, **20L (config says 24!)** | No | `decoder.stages.*` — **decoder-only, no encoder shipped** | `codes/` |
|
| 27 |
+
|
| 28 |
+
Rules that fall out of this:
|
| 29 |
+
- **Never assume tokenizer classes are interchangeable across checkpoints.** Same class name,
|
| 30 |
+
three different weight namings. Always load with `strict=False` and **assert 0 missing /
|
| 31 |
+
0 unexpected** — that assert is the tripwire that caught every mismatch.
|
| 32 |
+
- **TTS backbones** build with `exclude_embeds + exclude_lm_head` (inputs_embeds → hidden_states);
|
| 33 |
+
**ASR decoders keep lm_head** (→ logits). `optimize.py`'s `MODELS` registry encodes this.
|
| 34 |
+
- `asr` vs `asr-hf` are **different models** with different key layouts — resist merging them.
|
| 35 |
+
- Realtime has **TWO embed tables** (`language_model` + `tts_language_model`) that differ.
|
| 36 |
+
`common.EMBED_KEY` maps each key to the exact table matching the exported decoder. A
|
| 37 |
+
first-match heuristic here silently produced garbage audio once. Don't reintroduce it.
|
| 38 |
+
|
| 39 |
+
## 3. Traps that cost real time (do not rediscover these)
|
| 40 |
+
|
| 41 |
+
1. **`import vibevoice` collides with transformers** (both register the same model_type) and the
|
| 42 |
+
package `__init__` pulls diffusers + a qwen2-tokenizer module renamed in transformers ≥5.10.
|
| 43 |
+
Fix: `_codes_import()` in `user_script.py` — inject empty namespace packages so only the one
|
| 44 |
+
target module is imported, and shim `Auto*.register` to swallow duplicate registrations.
|
| 45 |
+
2. **dynamo ignores `dynamic_axes`.** Use `dynamic_shapes` in io_configs or the dim is baked
|
| 46 |
+
static. Also: the frame axis of `latents[B, frames, 64]` is **dim 1** (dim 2 is vae_dim) —
|
| 47 |
+
we shipped that off-by-one once.
|
| 48 |
+
3. **Timesteps must be float32.** The diffusion head's `TimestepEmbedder` casts its sinusoidal
|
| 49 |
+
embedding back to `t.dtype` before a float MLP; int64 timesteps crash the matmul.
|
| 50 |
+
4. **CFG needs ONE sample.** Conditional and unconditional eps must come from the SAME noisy
|
| 51 |
+
latent (duplicate the sample for the head call only, each step). Two independent samples =
|
| 52 |
+
guidance mixed with a noise difference = degraded audio, no error.
|
| 53 |
+
5. **int4 is fine for token generation, lossy for raw hidden states** (first-token hidden cos
|
| 54 |
+
~0.83 vs fp32 across 28 layers of 4-bit RTN). The TTS diffusion head is conditioned on hidden
|
| 55 |
+
states → **use fp16 builds for audio quality**; int4 for footprint experiments only.
|
| 56 |
+
6. **7B/8B int4 ModelBuilder serialize needs ~16–20 GB free RAM** (chandra-8B and both ASR 7Bs
|
| 57 |
+
hit this). Extraction is separate and cheap — stream shards, `del` after each. Subprocess
|
| 58 |
+
isolation does NOT help a single large serialize; you need physical RAM.
|
| 59 |
+
7. **Parity metric footnote:** cosine collapses on near-silent outputs (decoder fed random
|
| 60 |
+
latents ≈ silence → cosine noise-dominated while max|Δ| ≈ 1e-10). Pass criterion is
|
| 61 |
+
`cos ≥ 0.99 OR max|Δ| tiny` — that's deliberate, not sloppy.
|
| 62 |
+
8. **Windows consoles are cp1252.** Olive logs emoji → `UnicodeEncodeError`. Every entrypoint
|
| 63 |
+
reconfigures stdout/stderr to UTF-8 at import. Keep that block.
|
| 64 |
+
9. **`argparse nargs='*'` + trailing positional don't mix** — `--components` is comma-separated
|
| 65 |
+
for that reason.
|
| 66 |
+
10. **VibeVoice repos ship no tokenizer/processor.** We fetch the plain Qwen2.5 tokenizer; exact
|
| 67 |
+
prompt layout and voice-cloning need the original processor assets (see codes/ `processor/`).
|
| 68 |
+
11. **fp16 conversion needs an `op_block_list` for shape/resample ops.** `OnnxFloatToFloat16` on the
|
| 69 |
+
VAE/conv graphs will emit an invalid graph if it touches `ConstantOfShape` (its fp16 output hits a
|
| 70 |
+
float32 consumer → `Type Error … does not match expected type (tensor(float))`), and `ConvTranspose`
|
| 71 |
+
/`Resize`/`Range` gain nothing from fp16. `build_olive` blocks `["ConstantOfShape","ConvTranspose",
|
| 72 |
+
"Resize","Range"]`. (The standalone `acoustic` encoder is where this first bit — the codes/-layout
|
| 73 |
+
1.5b acoustic encoder happened not to.)
|
| 74 |
+
12. **int4 only quantizes the LLM.** Keys with no LLM (`acoustic`) or the audio/VAE/DiT components have
|
| 75 |
+
no MatMulNBits-quantizable weights, so int4 is a no-op that silently re-emits fp32. `build_model`
|
| 76 |
+
warns and downgrades `int4→fp32` for LLM-less keys so you don't ship a misnamed fp32 copy.
|
| 77 |
+
13. **Eval must feed each ONNX its DECLARED input dtype.** fp16 builds expect `float16`; feeding fp32
|
| 78 |
+
dummies → `Unexpected input data type`. `eval.py` (`parity_component` + `whole_pipeline._feed`) casts
|
| 79 |
+
per graph; float32-pinned inputs (diffusion `timesteps`) stay fp32. Without this, fp16 builds falsely
|
| 80 |
+
"fail" eval though the graphs are fine.
|
| 81 |
+
14. **The VibeVoice source is VENDORED at `VibeVoice/vibevoice/` (~591 KB, MIT — see `VIBEVOICE_LICENSE`).**
|
| 82 |
+
NOT a submodule, NOT a pip/git dependency. `_vibevoice_dir()` (user_script + common) returns that
|
| 83 |
+
directory and raises if it's missing. The former `codes/` submodule, the `vibevoice-repo/` workspace
|
| 84 |
+
clone (~263 MB each), the `[submodule]` in `.gitmodules`, the `vibevoice @ git+…` line in the onnx
|
| 85 |
+
pyprojects, and the root-pyproject `vibevoice` dep + `vibevoice-repo` workspace were ALL removed so
|
| 86 |
+
the tree is self-contained and uploadable. Imports still go through the isolated-import shim
|
| 87 |
+
(`_codes_import` / the namespace-injection in `make_scheduler`/`_load_vv_processor`) — see trap 1;
|
| 88 |
+
never `import vibevoice`. Only the required subtree is vendored (modular/, processor/, schedule/,
|
| 89 |
+
scripts/, configs/ — no demo/, finetuning-asr/, vllm_plugin/).
|
| 90 |
+
15. **The 1.5B has a learned EOS — do NOT ship a fixed frame budget.** `lm_head` is TIED to
|
| 91 |
+
`embed_tokens` (`_tied_weights_keys=["lm_head.weight"]`), so logits = `hidden @ embedᵀ` with NO
|
| 92 |
+
lm_head export. VibeVoice reuses vision tokens for speech: stop when the model predicts speech-end
|
| 93 |
+
(`<|vision_end|>`) or `<|endoftext|>`. `inference.py` computes this each frame via
|
| 94 |
+
`common.load_embed_matrix()` and breaks. This killed the "jargon tail" (a real sonnet stopped at
|
| 95 |
+
frame 101 of a 172 cap → 13.5 s not 22.9 s). `--max-frames` is now just a safety cap. Upstream
|
| 96 |
+
ships no non-streaming 1.5B `generate` (codes/ @ 303b283 = training forward + streaming only);
|
| 97 |
+
single-shot AR + this EOS is our reconstruction and it works — chunking regressed it, do not reintroduce.
|
| 98 |
+
(Upstream check was against the vendored `vibevoice/modular/modeling_vibevoice.py` @ upstream 303b283.)
|
| 99 |
+
|
| 100 |
+
## 4. Architecture of our code (7 files, one direction of dependency)
|
| 101 |
+
|
| 102 |
+
```
|
| 103 |
+
optimize.py MODELS registry (per-key: extract fn, exclude flags, olive component specs)
|
| 104 |
+
│ + ensure_checkpoint (auto-download) + detect_model_type + output layout
|
| 105 |
+
▼
|
| 106 |
+
user_script.py ALL loaders. codes/ isolated-import shim; per-checkpoint weight collectors;
|
| 107 |
+
4× extract_qwen2_* (standalone HF dirs for ModelBuilder); io_configs; dummies.
|
| 108 |
+
▼
|
| 109 |
+
common.py Inference primitives: OnnxLLM (manual KV-cache driver over the genai graph,
|
| 110 |
+
hidden_states OR logits), DiffusionSampler (DPM + CFG), audio io, EMBED_KEY,
|
| 111 |
+
resolve_from_path (parses onnx/{key}/{device}_{precision}).
|
| 112 |
+
▼
|
| 113 |
+
inference.py / inference_asr.py / inference_realtime.py thin drivers (one positional: built dir)
|
| 114 |
+
eval.py parity (A) + whole-pipeline TTS checks (B); reuses the registry.
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
Conventions: output layout `onnx/{key}/{device}_{precision}`; model keyword is always the FINAL
|
| 118 |
+
positional; drivers take ONLY the built dir and derive model_id/device/precision from the path;
|
| 119 |
+
`codes/` = unmodified `github.com/microsoft/VibeVoice` @ `303b283` (submodule-able, imported
|
| 120 |
+
read-only by path — never pip-install it, never edit it).
|
| 121 |
+
|
| 122 |
+
## 5. What is done vs pending
|
| 123 |
+
|
| 124 |
+
**Done & parity-verified (cos ~1.0):** 19 sub-models — 1.5b (7/7 incl. int4 LLM), realtime (4/4),
|
| 125 |
+
asr front-end (5), asr-hf front-end (3). End-to-end TTS runs for 1.5b + realtime.
|
| 126 |
+
|
| 127 |
+
**Pending:** (a) 7B ASR LLM builds — RAM-bound, run on a big-memory machine; front-end +
|
| 128 |
+
`inference_asr.py` are ready and degrade cleanly without it. (b) fp16 builds for TTS quality.
|
| 129 |
+
(c) Encoders bake audio length at 24000 samples (switch io_configs to `dynamic_shapes` to lift).
|
| 130 |
+
(d) Learned EOS (1.5b lm_head / realtime `tts_eos_classifier`) not exported → `--max-frames`
|
| 131 |
+
stop. (e) Cleanup: fold the 4 extractors into one helper; dedupe the ~9 shard-collect loops;
|
| 132 |
+
delete dead stub classes; factor the duplicated driver frame-loop into `common.py`.
|
| 133 |
+
|
| 134 |
+
**Environment:** use the PROJECT env (`uv run` from the repo; deps in root `pyproject.toml`).
|
| 135 |
+
Do NOT add PEP-723 inline headers — they bypass the curated venv. CUDA needs GPU builds of
|
| 136 |
+
onnxruntime/genai present in the env. `.gitmodules` needs fixing: root-level,
|
| 137 |
+
`path = VibeVoice/codes`, url `https://github.com/microsoft/VibeVoice`, pin `303b283`.
|
| 138 |
+
|
| 139 |
+
---
|
| 140 |
+
|
| 141 |
+
# THE CHECKLIST — adding a new VibeVoice-family checkpoint (or re-running everything)
|
| 142 |
+
|
| 143 |
+
## A. Recon (30 min — do not skip)
|
| 144 |
+
- [ ] Dump `config.json`: `model_type`, `architectures`, sub-configs (`decoder_config` /
|
| 145 |
+
`text_config`, `*_tokenizer_config`, `diffusion_head_config` — note `hidden_size`!).
|
| 146 |
+
- [ ] Dump weight groups: `Counter('.'.join(k.split('.')[:2]) for k in index/weight_map)`.
|
| 147 |
+
Identify: LLM prefix, lm_head (present? where?), tokenizer groups + their naming style,
|
| 148 |
+
connectors, heads. Compare against the table in §2.
|
| 149 |
+
- [ ] Decide codes/ vs transformers-native per component by **instantiating and loading with
|
| 150 |
+
strict=False; require 0 missing / 0 unexpected**. Try both if unsure.
|
| 151 |
+
- [ ] Check actual layer count vs config (`realtime` lied: 20 real vs 24 configured).
|
| 152 |
+
|
| 153 |
+
## B. Wire loaders (`user_script.py`)
|
| 154 |
+
- [ ] Weight collector per component (prefix-strip; stream shards for >2 GB; assert 0/0).
|
| 155 |
+
- [ ] Extractor for the LLM → standalone Qwen2 dir (correct config sub-key, tokenizer id,
|
| 156 |
+
lm_head kept iff the model emits text; override layer count if config lies).
|
| 157 |
+
- [ ] io_config: `dynamic_shapes` (NOT `dynamic_axes`), frame axis = dim 1, float32 timesteps.
|
| 158 |
+
- [ ] Register in `optimize.py` `MODELS` (+ `HF_REPO`, `common.EMBED_KEY`, `common.MODEL_IDS`).
|
| 159 |
+
|
| 160 |
+
## C. Build & verify (never skip verify)
|
| 161 |
+
- [ ] `uv run optimize.py --device cuda --precision fp16 <key>` (int4 only for the LLM footprint;
|
| 162 |
+
`--exclude-llm` if RAM-bound). Output: `onnx/<key>/<device>_<precision>/`.
|
| 163 |
+
- [ ] `uv run eval.py <key>` → every component PASS; whole-pipeline codec round-trip
|
| 164 |
+
corr > 0.99 / SNR > +15 dB on the tone test.
|
| 165 |
+
- [ ] LLM structural check: MatMulNBits/GQA counts = layer count; inputs = inputs_embeds (TTS)
|
| 166 |
+
or logits output (ASR). KV-driver sanity: incremental step == full prefill (cos 1.0).
|
| 167 |
+
- [ ] Smoke the matching driver end-to-end (`--max-frames 8` is enough to prove the loop).
|
| 168 |
+
|
| 169 |
+
## D. Ship
|
| 170 |
+
- [ ] Update `STATUS.md` matrix row + a dated section (what, parity numbers, gotchas hit).
|
| 171 |
+
- [ ] No scratch files (`_*.py`, `*.log`), no stale `qwen2_*_standalone/` (15 GB each!), no
|
| 172 |
+
old-layout outputs. Disk-full here corrupts builds mid-serialize.
|
| 173 |
+
- [ ] If it's a new trap, add it to §3 of this note. That's the contract.
|
| 174 |
+
|
| 175 |
+
# THE 5-MINUTE OPERATOR CARD (for someone who just wants to run it)
|
| 176 |
+
|
| 177 |
+
```bash
|
| 178 |
+
# build (downloads checkpoint if absent; output → onnx/<key>/<device>_<precision>)
|
| 179 |
+
uv run optimize.py --device cuda --precision fp16 1.5b # or asr | asr-hf | realtime | all
|
| 180 |
+
uv run optimize.py --exclude-llm asr # front-end only (low RAM)
|
| 181 |
+
|
| 182 |
+
# verify
|
| 183 |
+
uv run eval.py 1.5b
|
| 184 |
+
|
| 185 |
+
# run
|
| 186 |
+
uv run inference.py --text "Hello world." onnx/1.5b/cuda_fp16
|
| 187 |
+
uv run inference_realtime.py --text "Hi." onnx/realtime/cuda_fp16
|
| 188 |
+
uv run inference_asr.py --audio speech.wav onnx/asr-hf/cuda_fp16
|
| 189 |
+
```
|
| 190 |
+
Drivers print `model_id / device / precision` derived from the path and refuse the wrong model
|
| 191 |
+
type. If ASR says the 7B LLM isn't built, build it on a ≥32 GB-free-RAM machine.
|
STATUS.md
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# VibeVoice-1.5B — Current Status
|
| 2 |
+
|
| 3 |
+
> **Handoff:** read [HANDOFF.md](HANDOFF.md) — mental model, per-checkpoint table, the 10 traps, the new-checkpoint checklist, and the 5-minute operator card. Skill: `.claude/skills/vibevoice-onnx/`.
|
| 4 |
+
|
| 5 |
+
_Updated: 2026-07-17_
|
| 6 |
+
|
| 7 |
+
## 2026-07-17 — 1.5b rebuilt & re-verified on CPU (after checkpoints re-downloaded)
|
| 8 |
+
|
| 9 |
+
Checkpoints re-downloaded: **1.5B** (`model/`, 3/3 shards ✅) and **realtime** (`realtime/`, ✅)
|
| 10 |
+
complete; **asr** (3/8) and **asr-hf** (4/8) still downloading; `acoustic/` config-only. This box
|
| 11 |
+
is **CPU-only (no NVIDIA)**, so builds use `--device cpu` (LLM int4 — genai has no fp16-CPU).
|
| 12 |
+
|
| 13 |
+
Rebuilt all 7 **1.5b** sub-parts → `onnx/1.5b/cpu_int4/` (5.2 GB: llm_decoder int4 + acoustic
|
| 14 |
+
enc/dec + semantic enc + diffusion_head + acoustic/semantic connectors). `eval.py 1.5b` = **9/9
|
| 15 |
+
PASS** (llm: 140 MatMulNBits / 28 GQA / inputs_embeds; encoders/head/connectors cos 1.0;
|
| 16 |
+
acoustic_decoder cos 0.935 but max|Δ| 3.5e-10 = near-silent case; codec round-trip corr +0.996 /
|
| 17 |
+
SNR +19 dB; tts chain smoke OK). Driver smoke: `inference.py … onnx/1.5b/cpu_int4` → 2.13 s WAV.
|
| 18 |
+
|
| 19 |
+
Rebuilt all 4 **realtime** sub-parts → `onnx/realtime/cpu_int4/` (1.7 GB: llm_decoder int4 [20
|
| 20 |
+
layers, decoder-only] + acoustic_decoder + diffusion_head + acoustic_connector). `eval.py realtime`
|
| 21 |
+
= **5/5 PASS** (llm 100 MatMulNBits / 20 GQA / inputs_embeds; diffusion_head + connector cos 1.0;
|
| 22 |
+
acoustic_decoder cos 0.893 but max|Δ| 3.07e-10 near-silent; tts chain smoke H=896 OK). Driver smoke:
|
| 23 |
+
`inference_realtime.py … onnx/realtime/cpu_int4` → 2.13 s WAV. Scratch `qwen2_*_standalone/` removed.
|
| 24 |
+
|
| 25 |
+
Built + verified both **7B front-ends** on CPU (`--exclude-llm`; the 7B LLM int4 serialize is still
|
| 26 |
+
RAM-bound → needs a ≥32 GB-free box):
|
| 27 |
+
- **asr-hf** → `onnx/asr-hf/cpu_int4/` (acoustic_encoder + semantic_encoder + multi_modal_projector).
|
| 28 |
+
`eval.py asr-hf` = **4/4 PASS** (encoders + projector cos 1.0; asr fusion chain fused=(1,7,3584);
|
| 29 |
+
LLM/codec/tts-chain SKIP as expected without the LLM).
|
| 30 |
+
- **asr** → `onnx/asr/cpu_int4/` (acoustic enc/dec + semantic enc + acoustic/semantic connectors).
|
| 31 |
+
`eval.py asr` = **6/6 PASS** (encoders/connectors cos 1.0; acoustic_decoder cos 0.935 maxd 3.9e-10
|
| 32 |
+
near-silent; codec round-trip corr +0.996 / SNR +19 dB).
|
| 33 |
+
|
| 34 |
+
Added **`acoustic`** registry key — the standalone `vibevoice_acoustic_tokenizer` checkpoint
|
| 35 |
+
(`acoustic/`, 1.3 GB), transformers-native (`AutoModel`, no `codes/`). New loaders
|
| 36 |
+
`get_acoustic_std_{encoder,decoder}_model` (encode→`.latents`, decode→`.sample`); `all_components`
|
| 37 |
+
handles the no-LLM case; `detect_model_type` maps `vibevoice_acoustic_tokenizer`→`acoustic`. Built
|
| 38 |
+
→ `onnx/acoustic/cpu_fp32/`; `eval.py --precision fp32 acoustic` = **3/3 PASS** (encoder cos 0.999,
|
| 39 |
+
decoder cos 0.939 near-silent, codec round-trip corr +0.999 / SNR +22.9 dB).
|
| 40 |
+
|
| 41 |
+
All checkpoints downloaded: 1.5b, realtime, asr (17 GB), asr-hf (16 GB), standalone `acoustic` (1.3 GB).
|
| 42 |
+
|
| 43 |
+
`.gitmodules` fixed: `codes` submodule URL corrected `huggingface/vibevoice` → `microsoft/VibeVoice`
|
| 44 |
+
(pin 303b283) — the old URL left `codes/` empty on fresh clones → `ModuleNotFoundError:
|
| 45 |
+
vibevoice.modular.modular_vibevoice_tokenizer` in every codes/-backed loader.
|
| 46 |
+
|
| 47 |
+
Audio-quality caveat stands for the TTS builds — int4 hidden-state conditioning is lossy; fp16 would
|
| 48 |
+
need a GPU (genai has no fp16-on-CPU). Remaining: the three **7B ASR LLM decoders** (asr, asr-hf +
|
| 49 |
+
lm_heads) — build on a big-RAM machine; front-ends are done and `inference_asr.py` degrades cleanly.
|
| 50 |
+
|
| 51 |
+
### Later 2026-07-17 — fp16 eval fix, acoustic fp16/int4, git-dependency option
|
| 52 |
+
|
| 53 |
+
- **eval.py made dtype-aware** (`parity_component` + `whole_pipeline._feed`): feed each ONNX its
|
| 54 |
+
DECLARED input dtype (fp16 graphs want float16; timesteps stay fp32). Fixes false "failures" on fp16
|
| 55 |
+
builds. `1.5b/cpu_fp16` now evals **9/9** (it was fine all along — the harness fed fp32). HANDOFF trap 13.
|
| 56 |
+
- **1.5b CPU variants:** `cpu_int4` 9/9, `cpu_fp32` 9/9, `cpu_fp16` 9/9. `gpu_*` can't be built/run here
|
| 57 |
+
(CPU-only box; `torch +cpu`, no CUDAExecutionProvider). The user-supplied fp16-on-CPU **LLM** is genai's
|
| 58 |
+
unfused GQA×0 build needing `position_ids` (not driver-compatible) — a GPU fp16 build avoids that.
|
| 59 |
+
- **acoustic:** `fp32` 3/3 ✅; `fp16` rebuilt with `op_block_list=[ConstantOfShape,ConvTranspose,Resize,
|
| 60 |
+
Range]` → 3/3 ✅ (ConstantOfShape had emitted fp16 into a float32 consumer — HANDOFF trap 11); `int4`
|
| 61 |
+
removed — it's a no-op on a conv VAE, so `build_model` now **warns + downgrades int4→fp32** for
|
| 62 |
+
LLM-less keys (HANDOFF trap 12).
|
| 63 |
+
- **`optimize.py` acoustic key** finalized: `HF_REPO["acoustic"]=microsoft/VibeVoice-AcousticTokenizer`,
|
| 64 |
+
listed in `--help`/`--list`, transformers-native loaders (no `codes/`).
|
| 65 |
+
- **2026-07-22 — VibeVoice source VENDORED, submodule + git-dep removed (uploadable).** The required
|
| 66 |
+
upstream subset (modular/, processor/, schedule/, scripts/, configs/ — ~591 KB, MIT/`VIBEVOICE_LICENSE`)
|
| 67 |
+
now ships as `VibeVoice/vibevoice/` and (copied) `onnx/1.5b/vibevoice/`. `_vibevoice_dir()` returns that
|
| 68 |
+
tree and raises if absent — no pip/git fallback. Removed: `codes/` submodule, `vibevoice-repo/` clone
|
| 69 |
+
(~263 MB each), `.gitmodules`, the `vibevoice @ git+…` line in all three onnx pyprojects, and the
|
| 70 |
+
root-pyproject `vibevoice` dep + `vibevoice-repo` workspace. Imports still use the isolated shim
|
| 71 |
+
(trap 1). Verified: modular/schedule/processor all resolve from the vendored tree with codes/ deleted;
|
| 72 |
+
the `onnx/1.5b/` package resolves its own copy. HANDOFF trap 14.
|
| 73 |
+
|
| 74 |
+
## 2026-07-17 — faithful 1.5B TTS voice-cloning inference built (from source)
|
| 75 |
+
|
| 76 |
+
No upstream 1.5B TTS `generate` exists (not in transformers — only `vibevoice_asr`/`vibevoice_acoustic_tokenizer`; not in the HF repo; `codes/` has only the training `forward`). Reconstructed it on the ONNX sub-parts, replicating from source:
|
| 77 |
+
- **Dynamic acoustic encoder** — fixed `dynamic_axes`→`dynamic_shapes` (trap #2); now emits `samples/3200` frames (was baked 24000 → 7 vs 7.5 drift), aligning with the processor's `speech_tok_compress_ratio=3200`. Also fixes the codec round-trip length drift.
|
| 78 |
+
- **Voice+prompt prefill** (`common.voice_prompt_embeds`) — runs `codes/` `VibeVoiceProcessor` (with trap-#1 shims: qwen2-fast alias + empty namespaces) → `input_ids` + `speech_input_mask` (70) + reference `speech_tensors`; acoustic-encodes the (24 kHz) voice, applies checkpoint `speech_scaling_factor`/`bias` (0.196/−0.049), connects, scatters the voice embeds into the masked positions. Replicates `forward_speech_features` (acoustic-only; TTS has no semantic tensors).
|
| 79 |
+
- **CFG negative** — parallel `<|image_pad|>` (id 151655) LLM context, prefilled + stepped alongside the positive one (from the streaming `generate`; NOT the old zero-hidden). `inference.py --voice` (default `samples/voices/en-Alice_woman.wav`).
|
| 80 |
+
|
| 81 |
+
Signal-level result (can't audition here): output went unconditioned→voiced→sharper as each piece landed — RMS 0.004→0.029, ZCR 0.110→0.052, centroid 1721→1065 Hz, sub-4 kHz energy 0.89→0.97. Intelligibility not verified (needs listening); remaining is quality/tuning, not missing machinery.
|
| 82 |
+
|
| 83 |
+
Notes: the voice path needs the **dynamic** acoustic_encoder (re-exported into `cpu_fp32` + `cpu_int4`). Inference now pulls `codes/` (processor + scheduler) — no longer onnxruntime-only, inherent to VibeVoice's prompt format.
|
| 84 |
+
|
| 85 |
+
### Working + tuned (later 2026-07-17)
|
| 86 |
+
Confirmed by listening — the pipeline produces **intelligible human speech** in the reference voice. Tuning applied:
|
| 87 |
+
- **fp32, not int4** — int4's raw-hidden quantization adds audible "hiss/background" (trap #5); fp32 is clear. (The `*_bgm.wav` sample voices carry real background music; `en-Alice_woman.wav` is clean.)
|
| 88 |
+
- **CFG negative = static `<|image_pad|>`** (prefill once, reuse) — cfg 1.3 preferred over 1.0; keeps voice while staying **single-session** (fp32 ~5 GB, not ~10 GB).
|
| 89 |
+
- **Auto frame-length** (`--max-frames 0`): ~5 frames/word (no EOS classifier is exported → fixed budget, not auto-stop). Prevents truncation of long text.
|
| 90 |
+
- **Sentence chunking**: split on `[.!?]`, generate each chunk short, concat (150 ms gaps) → keeps each generation in the stable short regime.
|
| 91 |
+
|
| 92 |
+
**Resolved — the generate works** (single-shot; earlier "chunking" was a regression and is removed). Confirmed intelligible on prose AND a Shakespeare sonnet with the reference voice. Final recipe:
|
| 93 |
+
- **Single-shot** generation (whole prompt in one context) — chunking split phrasing and made some inputs noise; reverted.
|
| 94 |
+
- **Learned EOS (the key fix)** — the 1.5B `lm_head` is **tied to `embed_tokens`**, so `logits = hidden @ embedᵀ` needs no lm_head export. VibeVoice reuses vision tokens for speech; generation **stops when lm_head predicts `<|vision_end|>` (speech-end) or `<|endoftext|>` (EOS)**. This removed the "jargon tail" at the source (e.g. sonnet stopped at frame 101 of a 172 budget → 13.5 s not 22.9 s). `--max-frames` is now just a safety cap. **So "no EOS exported" is NOT a limitation** — it's recovered from the tied weights.
|
| 95 |
+
- **fp32, not int4** for clarity (int4 hidden-quant → hiss). **static `<|image_pad|>` CFG negative**, cfg 1.3. Voice-conditioned prefill (processor + acoustic splice + scale/bias).
|
| 96 |
+
|
| 97 |
+
**Remaining minor** (1.5B model quality, not pipeline): approximate timbre cloning (gender not always captured); occasional pronunciation quirks ("boy"→"bow") and weak digit reading; fp32 CPU ~2–3 s/frame (GPU `gpu_fp16` far faster). The 7B model would improve fidelity.
|
| 98 |
+
|
| 99 |
+
## Status matrix (model × component)
|
| 100 |
+
|
| 101 |
+
Legend: ✅ converted & parity-verified · ⚠️ pending / partial / memory-bound · ❌ not started or blocked · — not applicable
|
| 102 |
+
|
| 103 |
+
| Model | LLM decoder | Acoustic enc | Acoustic dec | Semantic tok | Diffusion head | Connectors / projector |
|
| 104 |
+
|---|:---:|:---:|:---:|:---:|:---:|:---:|
|
| 105 |
+
| **VibeVoice-1.5B** (TTS) | ✅ int4 | ✅ cos 1.0 | ✅ cos 1.0 | ✅ cos 1.0 (enc) | ✅ cos 1.0 | ✅ cos 1.0 (ac+sem) |
|
| 106 |
+
| **VibeVoice-ASR-HF** (7B) | ⚠️ extracted, int4 OOM | ✅ cos 1.0 | — | ✅ cos 1.0 | — | ✅ cos 1.0 (mm_projector) |
|
| 107 |
+
| **VibeVoice-ASR** (7B) | ⚠️ 7B int4 OOM | ✅ cos 1.0 | ✅ cos 1.0² | ✅ cos 1.0 | — none | ✅ cos 1.0 (ac+sem) |
|
| 108 |
+
| **Realtime-0.5B** | ✅ int4 (tts) | — (none shipped) | ✅ cos 1.0 | — none | ✅ cos 1.0 | ✅ cos 1.0 (acoustic) |
|
| 109 |
+
|
| 110 |
+
Encoder note: all acoustic/semantic **encoders** (1.5B, ASR, ASR-HF) currently bake the audio `samples` dim (24000 = 1 s @ 24 kHz) — the shared io_config uses `dynamic_axes`, which the dynamo exporter fixes. Frames/latents outputs are correct; a cross-cutting `dynamic_shapes` switch would make audio length variable (not yet applied).
|
| 111 |
+
|
| 112 |
+
Notes: the three **7B LLMs** (ASR-HF / ASR text decoders + their lm_heads) all hit the int4-serialize OOM wall at current free RAM (peak ~16–20 GB, same as chandra-8B) — extraction paths ready, re-run after freeing RAM. Realtime **acoustic enc** = none shipped (decoder-only checkpoint); Realtime **semantic tok** = none in that checkpoint. ASR acoustic dec is off the audio→text path (see ²).
|
| 113 |
+
|
| 114 |
+
## Architecture (confirmed from downloaded checkpoint)
|
| 115 |
+
`VibeVoiceForConditionalGeneration` (`model_type: vibevoice`, auto_map null → needs the custom
|
| 116 |
+
`vibevoice` package; not in transformers). Weight groups (model.safetensors.index.json):
|
| 117 |
+
- `model.language_model.*` (338) — **Qwen2.5-1.5B backbone** (1536/28L/12h/2kv, q/k/v bias,
|
| 118 |
+
tied embeddings, **no lm_head** — the head is the diffusion prediction_head).
|
| 119 |
+
- `model.acoustic_tokenizer.*` (552) — acoustic VAE/codec.
|
| 120 |
+
- `model.semantic_tokenizer.*` (276) — semantic tokenizer.
|
| 121 |
+
- `model.prediction_head.*` (26) — DiT-style diffusion denoiser (adaLN + ffn).
|
| 122 |
+
- `model.acoustic_connector.*` / `model.semantic_connector.*` (5 each) — projection MLPs.
|
| 123 |
+
|
| 124 |
+
## Done ✅
|
| 125 |
+
- ✅ Model downloaded to `model/` (3 shards). Tokenizer NOT in repo → fetched Qwen2.5-1.5B's.
|
| 126 |
+
- ✅ **`llm_decoder` BUILT (cpu int4)** → `cpu_int4/models/llm_decoder.onnx` (+857 MB data),
|
| 127 |
+
`genai_config.json`, tokenizer. Pipeline: `user_script.extract_qwen2_standalone` remaps
|
| 128 |
+
`model.language_model.*` → a standalone Qwen2ForCausalLM dir; `optimize.py` runs ModelBuilder
|
| 129 |
+
INT4 with **exclude_embeds + exclude_lm_head** → `inputs_embeds → hidden_states` + KV cache.
|
| 130 |
+
Verified: 140 MatMulNBits (int4) + 28 GroupQueryAttention; genai_config wires inputs_embeds→hidden.
|
| 131 |
+
- Build: `python optimize.py --skip-download --components llm` (or no args to also download).
|
| 132 |
+
|
| 133 |
+
## Pending ⏳ (need the custom `vibevoice` package)
|
| 134 |
+
- `acoustic_tokenizer`, `semantic_tokenizer`, `diffusion_head`, connectors → Olive.
|
| 135 |
+
`auto_map` is null and the classes aren't in transformers, so loading the custom modules for
|
| 136 |
+
export needs `pip install vibevoice` (or the github source). `user_script.py` has stub loaders
|
| 137 |
+
that raise a clear message until that's wired.
|
| 138 |
+
- Full TTS inference (`inference.py`) + `eval.py`: text → embed+connectors → llm_decoder → hidden
|
| 139 |
+
→ diffusion head (iterative denoise) → acoustic latents → acoustic decoder → waveform. The
|
| 140 |
+
diffusion sampling loop + acoustic VAE are the novelty/risk (expect custom-op / fixed-shape work).
|
| 141 |
+
|
| 142 |
+
## Files
|
| 143 |
+
`optimize.py` (LLM build), `user_script.py` (Qwen2 extraction + audio stubs), scaffold (info.yml,
|
| 144 |
+
README, requirements). Target: CPU INT4.
|
| 145 |
+
|
| 146 |
+
## ✅ AcousticTokenizer (from VibeVoice-1.5B) — CONVERTED & verified
|
| 147 |
+
Handled per-checkpoint (acoustic tokenizers differ across the family: 1.5B=downsample_layers,
|
| 148 |
+
Realtime=stages/head, ASR-HF=conv_layers). For **1.5B**, the vendored `codes/`
|
| 149 |
+
`VibeVoiceAcousticTokenizerModel` matches its weights EXACTLY (552 tensors, 0 missing).
|
| 150 |
+
Loaded via an isolated import (bypasses the package __init__ that pulls diffusers + a
|
| 151 |
+
transformers-5.10.2-renamed qwen2 tokenizer) + an Auto*.register shim (coexists with
|
| 152 |
+
transformers' built-in vibevoice_acoustic_tokenizer). Exported both halves via Olive (fp32, dynamo):
|
| 153 |
+
- `acoustic_encoder.onnx` — `audio[B,1,T] → latents[B,8,64]` (VAE `.mean`)
|
| 154 |
+
- `acoustic_decoder.onnx` — `latents[B,8,64] → audio[B,L]`
|
| 155 |
+
**Parity vs PyTorch: cosine 1.0** (encoder max|Δ| ~1e-4, decoder ~5e-6). `user_script.py`
|
| 156 |
+
(`_load_acoustic` + get_acoustic_encoder/decoder_*), `optimize.py --components acoustic_encoder acoustic_decoder`.
|
| 157 |
+
(Each file ~1.37 GB — the codec is a large VAE; both currently carry full weights.)
|
| 158 |
+
|
| 159 |
+
## Deps note
|
| 160 |
+
`codes/` needs `diffusers` (installed via uv) for its diffusion scheduler; the acoustic tokenizer
|
| 161 |
+
itself is imported in isolation and does NOT need it. transformers 5.10.2 has native
|
| 162 |
+
`vibevoice_acoustic_tokenizer` + `vibevoice_asr` (different weight-namings than 1.5B — see above).
|
| 163 |
+
|
| 164 |
+
## Next
|
| 165 |
+
ASR (note: `VibeVoice-ASR` and `VibeVoice-ASR-HF` are DIFFERENT models — HF = transformers-native
|
| 166 |
+
8B `vibevoice_asr`), then Realtime-0.5B (vibevoice_streaming, via codes/).
|
| 167 |
+
|
| 168 |
+
## ✅ ASR-HF acoustic encoder — CONVERTED & verified (transformers-native)
|
| 169 |
+
`VibeVoice-ASR-HF` (`vibevoice_asr`, transformers-native, 8 shards / 16.7 GB, LLM = Qwen2.5-**7B**)
|
| 170 |
+
is DISTINCT from `VibeVoice-ASR` (`VibeVoiceForASRTraining`, codes/-format, config-only here).
|
| 171 |
+
Its acoustic encoder is a DIFFERENT arch than 1.5B (`acoustic_tokenizer_encoder.conv_layers.*`) and
|
| 172 |
+
loads NATIVELY via transformers `VibeVoiceAcousticTokenizerEncoderModel` — no codes/ / shim.
|
| 173 |
+
Loaded ONLY the `acoustic_tokenizer_encoder.*` weights (276, 0 missing) — not the 7B LLM — so it
|
| 174 |
+
fits in memory. Exported (Olive fp32, dynamo) → `asr-hf/cpu_int4/models/acoustic_encoder.onnx`
|
| 175 |
+
(`audio[B,1,T] → latents[B,7,64]`). **Parity vs PyTorch: cosine 1.0**, max|Δ| ~1e-4.
|
| 176 |
+
`user_script.get_asrhf_acoustic_encoder_*`.
|
| 177 |
+
|
| 178 |
+
## ✅ ASR-HF semantic encoder + multi_modal_projector — CONVERTED & verified
|
| 179 |
+
- **semantic_encoder** — SAME transformers-native class as the acoustic encoder
|
| 180 |
+
(`vibevoice_acoustic_tokenizer_encoder`), just a different config + weight prefix
|
| 181 |
+
(`semantic_tokenizer_encoder.*`, hidden 128). `audio[B,1,T] → latents[B,frames,128]`.
|
| 182 |
+
**cos 1.0**, max|Δ| ~4e-5. `user_script.get_asrhf_semantic_encoder_*` (shared `_load_asrhf_encoder`).
|
| 183 |
+
- **multi_modal_projector** (`VibeVoiceAsrMultiModalProjector`) — fuses
|
| 184 |
+
acoustic[B,T,64] + semantic[B,T,128] → LLM features[B,T,3584] (two 2-layer MLPs + RMSNorm, summed).
|
| 185 |
+
Exported with dynamo **dynamic_shapes** (frames dynamic — verified T=8/20/37). **cos 1.0**, max|Δ| ~7e-6.
|
| 186 |
+
`→ asr-hf/cpu_int4/models/{semantic_encoder,multi_modal_projector}.onnx`.
|
| 187 |
+
|
| 188 |
+
## ASR-HF LLM — extraction DONE, int4 serialize OOM-blocked
|
| 189 |
+
- **Extraction ✅**: `user_script.extract_qwen2_asrhf` streams shards → standalone `Qwen2ForCausalLM`
|
| 190 |
+
dir `qwen2_asrhf_standalone/` (**WITH lm_head** — ASR emits text; `language_model.model.*`→`model.*`,
|
| 191 |
+
`language_model.lm_head.*`→`lm_head.*`; config = text_config = Qwen2.5-7B: 3584/28L/28h/4kv/vocab
|
| 192 |
+
152064, untied). 15.2 GB `model.safetensors` + Qwen2.5-7B tokenizer written. Ready to build.
|
| 193 |
+
- **int4 build ⚠️ OOM**: `create_model(..., "int4", "cpu")` (WITH embeds+lm_head) crashed mid-serialize
|
| 194 |
+
at ~2–3 GB free RAM (peak need ~16–20 GB) — SAME wall as chandra-8B. No `llm_decoder.onnx` produced.
|
| 195 |
+
Run `_build_asrhf_llm.py` again after freeing RAM (close Visual Studio ~3 GB + WSL) — extraction is
|
| 196 |
+
cached so it resumes straight to ModelBuilder. Subprocess isolation can't shrink a single 7B serialize.
|
| 197 |
+
|
| 198 |
+
## ✅ Realtime-0.5B (vibevoice_streaming) — LLM + acoustic decoder CONVERTED & verified
|
| 199 |
+
Streaming TTS checkpoint (`model.safetensors`, single file). Key groups: `tts_language_model.*`
|
| 200 |
+
(Qwen2.5-0.5B backbone, **20 layers**, no lm_head), `acoustic_tokenizer.*` (**DECODER-ONLY**, 276 —
|
| 201 |
+
no encoder shipped: inference only decodes generated latents), `language_model.*` (4-layer base),
|
| 202 |
+
`prediction_head.*` (diffusion), `acoustic_connector.*`, `tts_eos_classifier.*`.
|
| 203 |
+
- **tts llm_decoder ✅ int4** → `realtime/cpu_int4/models/llm_decoder.onnx` (+192 MB data). Built
|
| 204 |
+
0.5B in-memory (no OOM). `extract_qwen2_realtime` remaps `tts_language_model.*`→standalone Qwen2
|
| 205 |
+
(config = decoder_config with num_hidden_layers overridden to actual 20), ModelBuilder int4
|
| 206 |
+
**exclude_embeds+exclude_lm_head** → inputs_embeds→hidden. Verified 100 MatMulNBits + 20 GQA.
|
| 207 |
+
- **acoustic_decoder ✅** → `realtime/cpu_int4/models/acoustic_decoder.onnx` (1.38 GB). codes/ class
|
| 208 |
+
matches EXACTLY (decoder 0 missing/0 unexpected; stages/head naming — NOT transformers-native
|
| 209 |
+
conv_layers/convtr). Decoder-only load (`decoder.*` weights, encoder=None). `latents[B,T,64] →
|
| 210 |
+
audio[B,samples]`, dynamo **dynamic_shapes** (frames dynamic — verified T=10/25/50). **cos 1.0**,
|
| 211 |
+
max|Δ| ~4e-6. `user_script.get_realtime_acoustic_decoder_*`, `_codes_tokenizer` (shared codes/ import).
|
| 212 |
+
- **Acoustic ENCODER: none** — not in the streaming checkpoint (nothing to convert).
|
| 213 |
+
|
| 214 |
+
## Realtime pending
|
| 215 |
+
- prediction_head (diffusion denoiser) + acoustic_connector + tts_eos_classifier + 4-layer base
|
| 216 |
+
language_model → the streaming generation loop (text → tts backbone → diffusion → latents →
|
| 217 |
+
acoustic decoder → audio). Same diffusion-sampling novelty/risk as the 1.5B TTS head.
|
| 218 |
+
|
| 219 |
+
## ✅ Diffusion prediction_head + speech connectors (1.5B & Realtime) — CONVERTED & verified
|
| 220 |
+
Shared `codes/` classes (`_codes_import` isolated-import helper). Exported via Olive (fp32, dynamo).
|
| 221 |
+
- **diffusion_head** (`VibeVoiceDiffusionHead`) — ONE DDPM denoise step:
|
| 222 |
+
`(noisy_images[B,64], timesteps[B] FLOAT, condition[B,H]) → pred[B,64]`. The ~20-step sampling
|
| 223 |
+
loop stays in the pipeline; ONNX = one step. `H`=1536 (1.5B) / 896 (Realtime). **Batch dynamic**
|
| 224 |
+
(verified B=4/7). **cos 1.0**, max|Δ| ~1e-6. Note: timesteps MUST be float32 — `TimestepEmbedder`
|
| 225 |
+
casts its sinusoidal embedding back to `t.dtype` before the float MLP.
|
| 226 |
+
- **connectors** (`SpeechConnector`: fc1→RMSNorm→fc2) — project VAE latents into LLM hidden space,
|
| 227 |
+
frames dynamic. 1.5B: `acoustic_connector` 64→1536 + `semantic_connector` 128→1536; Realtime:
|
| 228 |
+
`acoustic_connector` 64→896. All **cos 1.0**, max|Δ| ≤5e-7.
|
| 229 |
+
- `user_script.get_diffusion_head_* / get_{acoustic,semantic}_connector_*`. Head dummy hidden via
|
| 230 |
+
env `VV_HEAD_HIDDEN` (1536 default; set 896 for Realtime).
|
| 231 |
+
|
| 232 |
+
## Sub-model tally: 13 ONNX parts converted & parity-verified (cos 1.0)
|
| 233 |
+
- **1.5B** (6): llm_decoder(int4), acoustic_encoder, acoustic_decoder, diffusion_head,
|
| 234 |
+
acoustic_connector, semantic_connector. Pending: semantic_tokenizer encoder; end-to-end pipeline.
|
| 235 |
+
- **ASR-HF** (3): acoustic_encoder, semantic_encoder, multi_modal_projector. LLM extracted (int4 OOM).
|
| 236 |
+
- **Realtime** (4): llm_decoder(int4), acoustic_decoder, diffusion_head, acoustic_connector.
|
| 237 |
+
Remaining to reach end-to-end TTS: wire the sampling pipeline (text → embed+connector → llm_decoder →
|
| 238 |
+
hidden → DDPM loop over diffusion_head → acoustic latents → acoustic_decoder → waveform) + eval.
|
| 239 |
+
|
| 240 |
+
² ASR acoustic_decoder frames baked at 8 (shared 1.5B io_config uses dynamic_axes, which dynamo
|
| 241 |
+
ignores) — harmless: the decoder is NOT on the ASR audio→text path. Encoders + connectors are frames-dynamic.
|
| 242 |
+
|
| 243 |
+
## ✅ VibeVoice-ASR (weights now downloaded) — audio front-end CONVERTED & verified
|
| 244 |
+
`VibeVoiceForASRTraining` (`model_type vibevoice`, auto_map null → codes/), 8 shards. Same codes/
|
| 245 |
+
family as 1.5B TTS, but: LLM = Qwen2.5-**7B** (decoder_config 3584/28L) **WITH** top-level
|
| 246 |
+
`lm_head.weight` (audio→text), and **NO prediction_head** (no audio generation). Weight groups:
|
| 247 |
+
acoustic_tokenizer (552, enc+dec), semantic_tokenizer (276, ENCODE-only), language_model (338) +
|
| 248 |
+
lm_head, acoustic/semantic connectors (5 each). All front-end pieces load via the EXISTING codes/
|
| 249 |
+
loaders (matched exactly: acoustic 552/0-miss, semantic 276/0-miss) — no per-checkpoint divergence
|
| 250 |
+
here (unlike the ASR-HF transformers-native tokenizers). Exported (Olive fp32, dynamo) →
|
| 251 |
+
`asr/cpu_int4/models/`:
|
| 252 |
+
- **acoustic_encoder** `audio[B,1,T]→latents[B,f,64]` (VAE .mean) — cos 1.0
|
| 253 |
+
- **acoustic_decoder** `latents[B,8,64]→audio` — cos 1.0 (frames fixed 8, off-path — see ²)
|
| 254 |
+
- **semantic_encoder** (`VibeVoiceSemanticTokenizerModel`, encode-only) `audio→latents[B,f,128]` — cos 1.0
|
| 255 |
+
- **acoustic_connector** 64→3584, **semantic_connector** 128→3584 — cos 1.0, frames-dynamic
|
| 256 |
+
`user_script.get_semantic_tokenizer_encoder_*` (new) + reused `_load_acoustic`/`_load_connector`.
|
| 257 |
+
|
| 258 |
+
## VibeVoice-ASR LLM — extraction ready, int4 serialize OOM (same 7B wall)
|
| 259 |
+
`extract_qwen2_asr` remaps `model.language_model.*`→`model.*` + top-level `lm_head.weight` (differs
|
| 260 |
+
from ASR-HF's `language_model.model.*`/`language_model.lm_head.*` layout), config = decoder_config
|
| 261 |
+
(Qwen2.5-7B), fetches Qwen2.5-7B tok. NOT run yet (avoids a 2nd 15 GB standalone dir on disk); the
|
| 262 |
+
int4 ModelBuilder serialize would OOM at current free RAM exactly like ASR-HF. Run after freeing RAM.
|
| 263 |
+
|
| 264 |
+
## Updated tally: 19 ONNX parts converted & parity-verified (cos ~1.0)
|
| 265 |
+
1.5B(7) + ASR-HF(3) + Realtime(4) + **ASR(5)**. Remaining: the three 7B
|
| 266 |
+
LLMs (ASR-HF/ASR extracted-or-ready but int4 OOM-bound); end-to-end pipelines.
|
| 267 |
+
|
| 268 |
+
## Inference (ONNX) — inference_common.py + 3 drivers
|
| 269 |
+
`inference_common.py` (shared): resolve() (reuses optimize registry), **OnnxLLM** (drives the
|
| 270 |
+
genai llm_decoder.onnx directly via ORT — 28-layer KV cache + growing attn mask; verified
|
| 271 |
+
KV-incremental==full cos 1.0; handles hidden_states/logits outputs), OnnxOp, **DiffusionSampler**
|
| 272 |
+
(diffusion_head.onnx + codes/ DPM scheduler + CFG), audio load/normalize(-25dBFS)/save,
|
| 273 |
+
embed_tokens lookup, load_scaling, acoustic_decode_to_wav.
|
| 274 |
+
- **inference.py** — VibeVoice-1.5B TTS: text→embed→prefill→[hidden→diffuse→latent→connector→step]→
|
| 275 |
+
decode all latents→wav. Verified: 8 frames → 1.07 s wav.
|
| 276 |
+
- **inference_realtime.py** — Realtime-0.5B streaming TTS: same loop, per-frame decode (dynamic
|
| 277 |
+
decoder) streamed. Verified: 6 frames → 0.80 s wav.
|
| 278 |
+
- **inference_asr.py** — asr/asr-hf audio→text: encoders→(connectors|projector)→fuse at speech-pad
|
| 279 |
+
positions→prefill(lm_head→logits)→greedy decode. Verified: front-end → fused [N,3584]; degrades
|
| 280 |
+
cleanly when the 7B llm_decoder.onnx isn't built.
|
| 281 |
+
Caveats (documented in files): int4 is lossy in raw-hidden space (first-token hidden cos ~0.83 vs
|
| 282 |
+
fp32) — prefer fp16 for audio fidelity; learned EOS (1.5b lm_head / realtime tts_eos_classifier)
|
| 283 |
+
isn't in the sub-part set → stops at --max-frames; exact prompt layout/voice-clone needs the
|
| 284 |
+
original VibeVoice processor/tokenizer assets (repos ship none → Qwen2.5 tokenizer used). Fixed a
|
| 285 |
+
real bug: acoustic_decoder io_config marked the vae_dim (dim 2) as "frames" instead of dim 1 →
|
| 286 |
+
frames stayed static; switched to dynamic_shapes on dim 1 and re-exported.
|
VIBEVOICE_LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2025 Microsoft
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
common.py
CHANGED
|
@@ -10,7 +10,7 @@ same primitives, factored here:
|
|
| 10 |
the ASR decoder. One prefill(embeds)->hidden + step(embeds)->hidden loop.
|
| 11 |
* OnnxOp — thin wrapper over a single-file ONNX session (encoders/decoder/
|
| 12 |
connector/diffusion_head/projector).
|
| 13 |
-
* DiffusionSampler — the DDPM/DPM denoise loop (diffusion_head.onnx +
|
| 14 |
* audio load / normalize (-25 dBFS) / save — 24 kHz mono (VibeVoice standard).
|
| 15 |
* load_tokenizer, load_scaling — Qwen2.5 tokenizer + the stored speech scaling/bias factors.
|
| 16 |
|
|
@@ -162,19 +162,13 @@ def load_scaling(src):
|
|
| 162 |
return scale, bias
|
| 163 |
|
| 164 |
|
| 165 |
-
# --------------------------------------------------------------------
|
| 166 |
def _vibevoice_dir():
|
| 167 |
-
"""vibevoice source
|
| 168 |
-
local = HERE / "
|
| 169 |
if local.is_dir():
|
| 170 |
return str(local)
|
| 171 |
-
|
| 172 |
-
spec = importlib.util.find_spec("vibevoice")
|
| 173 |
-
if spec and spec.submodule_search_locations:
|
| 174 |
-
return list(spec.submodule_search_locations)[0]
|
| 175 |
-
raise ModuleNotFoundError(
|
| 176 |
-
"vibevoice source not found — populate codes/ or install "
|
| 177 |
-
"'vibevoice @ git+https://github.com/microsoft/VibeVoice.git@303b283'")
|
| 178 |
|
| 179 |
|
| 180 |
def make_scheduler(diffusion_cfg):
|
|
@@ -252,11 +246,17 @@ class OnnxLLM:
|
|
| 252 |
outs = [o.name for o in self.sess.get_outputs()]
|
| 253 |
# TTS backbone emits 'hidden_states'; an ASR decoder that kept lm_head emits 'logits'.
|
| 254 |
self.out_name = "logits" if "logits" in outs else "hidden_states"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
self.hidden = None
|
| 256 |
self._reset()
|
| 257 |
|
| 258 |
def _reset(self, batch=1):
|
| 259 |
-
z = np.zeros((batch, self.kv_heads, 0, self.head_dim), dtype=
|
| 260 |
self.past = {}
|
| 261 |
for i in range(self.n_layers):
|
| 262 |
self.past[f"past_key_values.{i}.key"] = z.copy()
|
|
@@ -265,11 +265,14 @@ class OnnxLLM:
|
|
| 265 |
self.batch = batch
|
| 266 |
|
| 267 |
def _run(self, embeds):
|
| 268 |
-
embeds = np.asarray(embeds, dtype=
|
| 269 |
b, s, _ = embeds.shape
|
|
|
|
| 270 |
self.total += s
|
| 271 |
feed = {"inputs_embeds": embeds,
|
| 272 |
"attention_mask": np.ones((b, self.total), dtype=np.int64)}
|
|
|
|
|
|
|
| 273 |
feed.update(self.past)
|
| 274 |
outs = self.sess.run(None, feed)
|
| 275 |
names = [o.name for o in self.sess.get_outputs()]
|
|
@@ -316,6 +319,72 @@ def embed_tokens(src, token_ids, model_key):
|
|
| 316 |
raise RuntimeError(f"{key} not found in {src}")
|
| 317 |
|
| 318 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
def acoustic_decode_to_wav(dec_op: OnnxOp, latents, scale, bias):
|
| 320 |
"""latents [B,T,64] (LLM-space) -> waveform. Applies /scale - bias then acoustic_decoder."""
|
| 321 |
lat = (np.asarray(latents, dtype=np.float32) / (scale if scale else 1.0)) - bias
|
|
|
|
| 10 |
the ASR decoder. One prefill(embeds)->hidden + step(embeds)->hidden loop.
|
| 11 |
* OnnxOp — thin wrapper over a single-file ONNX session (encoders/decoder/
|
| 12 |
connector/diffusion_head/projector).
|
| 13 |
+
* DiffusionSampler — the DDPM/DPM denoise loop (diffusion_head.onnx + vendored vibevoice/ scheduler + CFG).
|
| 14 |
* audio load / normalize (-25 dBFS) / save — 24 kHz mono (VibeVoice standard).
|
| 15 |
* load_tokenizer, load_scaling — Qwen2.5 tokenizer + the stored speech scaling/bias factors.
|
| 16 |
|
|
|
|
| 162 |
return scale, bias
|
| 163 |
|
| 164 |
|
| 165 |
+
# -------------------------------------------------------------------- vendored vibevoice scheduler
|
| 166 |
def _vibevoice_dir():
|
| 167 |
+
"""The vendored vibevoice source tree shipped alongside this code (VibeVoice/vibevoice/)."""
|
| 168 |
+
local = HERE / "vibevoice"
|
| 169 |
if local.is_dir():
|
| 170 |
return str(local)
|
| 171 |
+
raise ModuleNotFoundError(f"vendored vibevoice source missing at {local}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
|
| 173 |
|
| 174 |
def make_scheduler(diffusion_cfg):
|
|
|
|
| 246 |
outs = [o.name for o in self.sess.get_outputs()]
|
| 247 |
# TTS backbone emits 'hidden_states'; an ASR decoder that kept lm_head emits 'logits'.
|
| 248 |
self.out_name = "logits" if "logits" in outs else "hidden_states"
|
| 249 |
+
# Fused GQA builds compute positions internally; the unfused fallback (fp16-on-CPU / fp32-on-CUDA)
|
| 250 |
+
# declares a 'position_ids' input we must feed. Match the graph's float dtype (fp16 build wants
|
| 251 |
+
# float16 embeds + KV cache), else ORT rejects the feed.
|
| 252 |
+
self.wants_pos = "position_ids" in self.in_names
|
| 253 |
+
emb_t = next(i.type for i in self.sess.get_inputs() if i.name == "inputs_embeds")
|
| 254 |
+
self.float_dt = np.float16 if "float16" in emb_t else np.float32
|
| 255 |
self.hidden = None
|
| 256 |
self._reset()
|
| 257 |
|
| 258 |
def _reset(self, batch=1):
|
| 259 |
+
z = np.zeros((batch, self.kv_heads, 0, self.head_dim), dtype=self.float_dt)
|
| 260 |
self.past = {}
|
| 261 |
for i in range(self.n_layers):
|
| 262 |
self.past[f"past_key_values.{i}.key"] = z.copy()
|
|
|
|
| 265 |
self.batch = batch
|
| 266 |
|
| 267 |
def _run(self, embeds):
|
| 268 |
+
embeds = np.asarray(embeds, dtype=self.float_dt)
|
| 269 |
b, s, _ = embeds.shape
|
| 270 |
+
prev = self.total
|
| 271 |
self.total += s
|
| 272 |
feed = {"inputs_embeds": embeds,
|
| 273 |
"attention_mask": np.ones((b, self.total), dtype=np.int64)}
|
| 274 |
+
if self.wants_pos: # positions of the newly-appended tokens
|
| 275 |
+
feed["position_ids"] = np.arange(prev, prev + s, dtype=np.int64)[None, :].repeat(b, 0)
|
| 276 |
feed.update(self.past)
|
| 277 |
outs = self.sess.run(None, feed)
|
| 278 |
names = [o.name for o in self.sess.get_outputs()]
|
|
|
|
| 319 |
raise RuntimeError(f"{key} not found in {src}")
|
| 320 |
|
| 321 |
|
| 322 |
+
def _load_vv_processor(src):
|
| 323 |
+
"""Load the VibeVoiceProcessor from the vendored vibevoice/ source with the trap-#1 shims: alias
|
| 324 |
+
the pre-5.10 qwen2 fast tokenizer path, inject empty vibevoice namespaces (no package __init__),
|
| 325 |
+
swallow Auto*.register collisions (see _vibevoice_dir — vendored VibeVoice/vibevoice/)."""
|
| 326 |
+
import os, types, importlib
|
| 327 |
+
base = _vibevoice_dir() # VibeVoice/vibevoice/ (vendored source)
|
| 328 |
+
root = os.path.dirname(base)
|
| 329 |
+
import transformers as _tf
|
| 330 |
+
qm = types.ModuleType("transformers.models.qwen2.tokenization_qwen2_fast")
|
| 331 |
+
qm.Qwen2TokenizerFast = _tf.Qwen2TokenizerFast
|
| 332 |
+
sys.modules.setdefault("transformers.models.qwen2.tokenization_qwen2_fast", qm)
|
| 333 |
+
for name, sub in [("vibevoice", ""), ("vibevoice.processor", "processor"), ("vibevoice.modular", "modular")]:
|
| 334 |
+
if name not in sys.modules:
|
| 335 |
+
m = types.ModuleType(name); m.__path__ = [os.path.join(base, sub) if sub else base]
|
| 336 |
+
sys.modules[name] = m
|
| 337 |
+
from transformers import AutoConfig, AutoModel
|
| 338 |
+
for cls in (AutoConfig, AutoModel):
|
| 339 |
+
r = cls.register
|
| 340 |
+
def _safe(*a, __r=r, **k):
|
| 341 |
+
try: __r(*a, **k)
|
| 342 |
+
except Exception: pass
|
| 343 |
+
cls.register = staticmethod(_safe)
|
| 344 |
+
VP = importlib.import_module("vibevoice.processor.vibevoice_processor").VibeVoiceProcessor
|
| 345 |
+
return VP.from_pretrained(str(src))
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
def voice_prompt_embeds(src, onnx_dir, text, voice_path, scale, bias, device="cpu", hop=3200):
|
| 349 |
+
"""Build the TTS prefill embeddings the way VibeVoice's forward_speech_features does, but on ONNX:
|
| 350 |
+
processor(text, voice) -> input_ids + speech_input_mask + reference speech_tensors
|
| 351 |
+
voice -> acoustic_encoder.onnx -> latents -> (latents+bias)*scale -> acoustic_connector.onnx
|
| 352 |
+
inputs_embeds = embed_tokens(input_ids); scatter the voice embeds into the masked positions.
|
| 353 |
+
Returns (inputs_embeds [1,S,H] float32, input_ids [1,S])."""
|
| 354 |
+
proc = _load_vv_processor(src)
|
| 355 |
+
if not text.strip().lower().startswith("speaker"):
|
| 356 |
+
text = "Speaker 1: " + text
|
| 357 |
+
wav = load_audio(voice_path, SR) # mono @ 24 kHz
|
| 358 |
+
out = proc(text=[text], voice_samples=[[wav]], return_tensors="pt")
|
| 359 |
+
input_ids = np.asarray(out["input_ids"]) # [1,S]
|
| 360 |
+
mask = np.asarray(out["speech_input_mask"]).astype(bool) # [1,S]
|
| 361 |
+
x = embed_tokens(src, input_ids[0], "1.5b").astype(np.float32) # [1,S,H]
|
| 362 |
+
n_fr = int(mask.sum())
|
| 363 |
+
if n_fr:
|
| 364 |
+
v = np.asarray(out["speech_tensors"], np.float32).ravel() # reference samples @ 24 kHz
|
| 365 |
+
pad = n_fr * hop # pad to exactly n_fr frames
|
| 366 |
+
v = np.pad(v, (0, max(0, pad - len(v))))[:pad][None, None] # [1,1,n_fr*hop]
|
| 367 |
+
lat = OnnxOp(onnx_dir / "acoustic_encoder.onnx", device).run(audio=v) # [1,n_fr,64]
|
| 368 |
+
audio_feat = (lat.astype(np.float32) + bias) * scale # checkpoint speech scale/bias
|
| 369 |
+
conn = OnnxOp(onnx_dir / "acoustic_connector.onnx", device)
|
| 370 |
+
vemb = conn.run(**{conn.inames[0]: audio_feat}) # [1,n_fr,H]
|
| 371 |
+
x[0][mask[0]] = vemb[0]
|
| 372 |
+
return x, input_ids
|
| 373 |
+
|
| 374 |
+
|
| 375 |
+
def load_embed_matrix(src, model_key):
|
| 376 |
+
"""Full token-embedding matrix [vocab, H] for `model_key`. The 1.5B lm_head is TIED to these
|
| 377 |
+
embeddings, so lm_head logits = hidden @ matrix.T — lets us detect the learned speech-end/EOS
|
| 378 |
+
token without exporting lm_head (it was excluded; head is the diffusion head)."""
|
| 379 |
+
from safetensors.torch import load_file
|
| 380 |
+
key = EMBED_KEY.get(model_key)
|
| 381 |
+
for sf in glob.glob(str(Path(src) / "*.safetensors")):
|
| 382 |
+
d = load_file(sf)
|
| 383 |
+
if key in d:
|
| 384 |
+
return d[key].float().numpy().astype(np.float32) # [vocab, H]
|
| 385 |
+
raise RuntimeError(f"{key} not found in {src}")
|
| 386 |
+
|
| 387 |
+
|
| 388 |
def acoustic_decode_to_wav(dec_op: OnnxOp, latents, scale, bias):
|
| 389 |
"""latents [B,T,64] (LLM-space) -> waveform. Applies /scale - bias then acoustic_decoder."""
|
| 390 |
lat = (np.asarray(latents, dtype=np.float32) / (scale if scale else 1.0)) - bias
|
cpu_fp32/acoustic_encoder.onnx
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7223ac37cbf144296dbbcd3298e18cb1a30d44b4d5689d84892fe3bb4458f61c
|
| 3 |
+
size 1376385625
|
cpu_int4/acoustic_encoder.onnx
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7223ac37cbf144296dbbcd3298e18cb1a30d44b4d5689d84892fe3bb4458f61c
|
| 3 |
+
size 1376385625
|
fp32.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fd78b7f3e8851ed034f203d4647743fbd348b8555ccc0bcb8464cdc8cdb978c7
|
| 3 |
+
size 1280044
|
inference.py
CHANGED
|
@@ -1,96 +1,130 @@
|
|
| 1 |
-
"""VibeVoice-1.5B TTS inference over the exported ONNX sub-parts.
|
| 2 |
-
|
| 3 |
-
Pipeline (reference algorithm, driven on ONNX via common):
|
| 4 |
-
text → tokenize → embed_tokens → OnnxLLM.prefill → then per acoustic frame:
|
| 5 |
-
last hidden state → DiffusionSampler (diffusion_head.onnx, DDPM/CFG) → acoustic latent
|
| 6 |
-
→ acoustic_decoder.onnx → waveform chunk
|
| 7 |
-
→ acoustic_connector.onnx (latent → LLM-space embed) → OnnxLLM.step (feeds next frame)
|
| 8 |
-
concatenate chunks → 24 kHz wav.
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
import
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
ap
|
| 40 |
-
ap.add_argument("
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
#
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
print(f"
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""VibeVoice-1.5B TTS inference over the exported ONNX sub-parts.
|
| 2 |
+
|
| 3 |
+
Pipeline (reference algorithm, driven on ONNX via common):
|
| 4 |
+
text → tokenize → embed_tokens → OnnxLLM.prefill → then per acoustic frame:
|
| 5 |
+
last hidden state → DiffusionSampler (diffusion_head.onnx, DDPM/CFG) → acoustic latent
|
| 6 |
+
→ acoustic_decoder.onnx → waveform chunk
|
| 7 |
+
→ acoustic_connector.onnx (latent → LLM-space embed) → OnnxLLM.step (feeds next frame)
|
| 8 |
+
concatenate chunks → 24 kHz wav.
|
| 9 |
+
|
| 10 |
+
Voice-cloning: the VibeVoice processor (vendored vibevoice/) builds the system + Speaker prompt and marks speech
|
| 11 |
+
placeholders; the reference voice is acoustic-encoded and spliced there (replicates
|
| 12 |
+
forward_speech_features). CFG uses a static "<|image_pad|>" negative. Single-shot (whole prompt in
|
| 13 |
+
one context) — chunking split phrasing and regressed some inputs.
|
| 14 |
+
|
| 15 |
+
Learned EOS: the 1.5B lm_head is TIED to embed_tokens, so logits = hidden @ embedᵀ (no lm_head
|
| 16 |
+
export needed). VibeVoice reuses vision tokens for speech, so generation stops when the model
|
| 17 |
+
predicts speech-end (<|vision_end|>) or EOS (<|endoftext|>). --max-frames is just a safety cap.
|
| 18 |
+
|
| 19 |
+
Usage — the ONLY model input is the built ONNX dir; model_id/device/precision derive from the path:
|
| 20 |
+
uv run inference.py --text "Hello world." --voice samples/voices/en-Alice_woman.wav onnx/1.5b/cpu_fp32
|
| 21 |
+
uv run inference.py --text "..." --cfg-scale 1.3 --max-frames 0 onnx/1.5b/cpu_fp32 # 0 = auto cap
|
| 22 |
+
|
| 23 |
+
Notes:
|
| 24 |
+
* Use **fp32** — int4 is lossy in raw-hidden space (conditions the diffusion head) → audible hiss.
|
| 25 |
+
* timbre-cloning is approximate and digits read weakly (1.5B model quality). See STATUS.md.
|
| 26 |
+
"""
|
| 27 |
+
import argparse
|
| 28 |
+
import json
|
| 29 |
+
import sys
|
| 30 |
+
from pathlib import Path
|
| 31 |
+
|
| 32 |
+
import numpy as np
|
| 33 |
+
|
| 34 |
+
sys.path.insert(0, str(Path(__file__).parent))
|
| 35 |
+
import common
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def main():
|
| 39 |
+
ap = argparse.ArgumentParser(description="VibeVoice-1.5B TTS (ONNX)")
|
| 40 |
+
ap.add_argument("model_path", help="built ONNX dir, e.g. model/cpu_int4/models")
|
| 41 |
+
ap.add_argument("--text", required=True, help="text to synthesize")
|
| 42 |
+
ap.add_argument("--out", default="tts_out.wav", help="output wav path")
|
| 43 |
+
ap.add_argument("--max-frames", type=int, default=0,
|
| 44 |
+
help="safety cap on acoustic frames (7.5/sec); generation normally stops earlier "
|
| 45 |
+
"via the learned EOS. 0 = auto-cap from text length (~5/word)")
|
| 46 |
+
ap.add_argument("--cfg-scale", type=float, default=1.3)
|
| 47 |
+
ap.add_argument("--voice", default="samples/voices/en-Alice_woman.wav",
|
| 48 |
+
help="reference voice wav for cloning (VibeVoice is voice-conditioned); "
|
| 49 |
+
"'' = text-only smoke-test (unconditioned, not intelligible)")
|
| 50 |
+
args = ap.parse_args()
|
| 51 |
+
|
| 52 |
+
key, src, onnx_dir, device, precision, model_id = common.resolve_from_path(args.model_path)
|
| 53 |
+
print(f"=== TTS | model_id={model_id} device={device} precision={precision} ===")
|
| 54 |
+
if key != "1.5b":
|
| 55 |
+
sys.exit(f"[error] {model_id} ({key}) is not the 1.5B TTS model — "
|
| 56 |
+
f"use inference_asr.py (asr/asr-hf) or inference_realtime.py (realtime)")
|
| 57 |
+
for need in ("llm_decoder", "diffusion_head", "acoustic_decoder", "acoustic_connector"):
|
| 58 |
+
if not (onnx_dir / f"{need}.onnx").exists():
|
| 59 |
+
sys.exit(f"missing {need}.onnx in {onnx_dir} — build it: uv run optimize.py {key}")
|
| 60 |
+
|
| 61 |
+
cfg = json.loads((src / "config.json").read_text())
|
| 62 |
+
dcfg = cfg["diffusion_head_config"]
|
| 63 |
+
scale, bias = common.load_scaling(src)
|
| 64 |
+
print(f" scale={scale:.4f} bias={bias:.4f}")
|
| 65 |
+
|
| 66 |
+
tok = common.load_tokenizer(onnx_dir, src)
|
| 67 |
+
llm = common.OnnxLLM(onnx_dir / "llm_decoder.onnx", device)
|
| 68 |
+
head = common.OnnxOp(onnx_dir / "diffusion_head.onnx", device)
|
| 69 |
+
dec = common.OnnxOp(onnx_dir / "acoustic_decoder.onnx", device)
|
| 70 |
+
conn = common.OnnxOp(onnx_dir / "acoustic_connector.onnx", device)
|
| 71 |
+
sampler = common.DiffusionSampler(head, dcfg, device)
|
| 72 |
+
|
| 73 |
+
# CFG negative (static "<|image_pad|>" — see below), computed once and reused for every chunk.
|
| 74 |
+
neg_vec = None
|
| 75 |
+
if args.cfg_scale != 1.0:
|
| 76 |
+
neg_id = tok.convert_tokens_to_ids("<|image_pad|>")
|
| 77 |
+
if neg_id is None or neg_id < 0:
|
| 78 |
+
print(" [warn] no <|image_pad|> token — CFG disabled")
|
| 79 |
+
else:
|
| 80 |
+
nh = llm.prefill(common.embed_tokens(src, [neg_id], key))
|
| 81 |
+
neg_vec = nh[0, -1, :].copy(); llm._reset()
|
| 82 |
+
print(f" CFG negative: static '<|image_pad|>' (id={neg_id}), scale={args.cfg_scale}")
|
| 83 |
+
|
| 84 |
+
# Single-shot generation (chunking regressed some inputs — the whole prompt in one context is
|
| 85 |
+
# what works). Voice-conditioned prefill, then the AR diffusion frame loop.
|
| 86 |
+
from pathlib import Path as _P
|
| 87 |
+
if args.voice and _P(args.voice).exists():
|
| 88 |
+
print(f" voice-cloning prompt from {args.voice}")
|
| 89 |
+
embeds, ids = common.voice_prompt_embeds(src, onnx_dir, args.text, args.voice, scale, bias, device)
|
| 90 |
+
else:
|
| 91 |
+
if args.voice:
|
| 92 |
+
print(f" [warn] voice '{args.voice}' not found — text-only (unconditioned)")
|
| 93 |
+
embeds = common.embed_tokens(src, tok.encode(args.text), key)
|
| 94 |
+
llm._reset()
|
| 95 |
+
hidden = llm.prefill(embeds)
|
| 96 |
+
H = hidden.shape[-1]
|
| 97 |
+
|
| 98 |
+
# Safety cap only — the learned EOS below normally stops first. Auto-estimated from text (~5/word).
|
| 99 |
+
n_frames = args.max_frames if args.max_frames > 0 else min(400, 12 + len(args.text.split()) * 5)
|
| 100 |
+
if args.max_frames <= 0:
|
| 101 |
+
print(f" frame cap={n_frames} ({len(args.text.split())} words, ~{n_frames/7.5:.1f}s) — EOS stops earlier")
|
| 102 |
+
|
| 103 |
+
# Learned EOS: lm_head is TIED to embed_tokens, so logits = hidden @ embedᵀ. VibeVoice reuses
|
| 104 |
+
# vision tokens for speech — stop when the model predicts speech-end (<|vision_end|>) or EOS
|
| 105 |
+
# (<|endoftext|>). This removes the no-EOS overshoot ("jargon tail") instead of guessing a budget.
|
| 106 |
+
emb_W = common.load_embed_matrix(src, key) # [vocab, H]
|
| 107 |
+
end_ids = {tok.convert_tokens_to_ids(t) for t in ("<|vision_end|>", "<|endoftext|>")}
|
| 108 |
+
end_ids = {i for i in end_ids if isinstance(i, int) and i >= 0}
|
| 109 |
+
|
| 110 |
+
latents = []
|
| 111 |
+
for f in range(n_frames):
|
| 112 |
+
cond = hidden[0, -1, :]
|
| 113 |
+
neg = neg_vec if neg_vec is not None else np.zeros(H, dtype=np.float32)
|
| 114 |
+
latent = sampler.sample(cond, neg, cfg_scale=args.cfg_scale, n_frames=1, seed=f)
|
| 115 |
+
latents.append(latent[0])
|
| 116 |
+
hidden = llm.step(conn.run(features=latent[None]).astype(np.float32))
|
| 117 |
+
if end_ids and int((hidden[0, -1] @ emb_W.T).argmax()) in end_ids: # tied-lm_head EOS
|
| 118 |
+
print(f" EOS predicted at frame {f+1}/{n_frames} — stop")
|
| 119 |
+
break
|
| 120 |
+
if (f + 1) % 25 == 0:
|
| 121 |
+
print(f" frame {f+1}/{n_frames}")
|
| 122 |
+
|
| 123 |
+
lat_seq = np.stack(latents)[None].astype(np.float32)
|
| 124 |
+
audio = np.asarray(common.acoustic_decode_to_wav(dec, lat_seq, scale, bias)).ravel()
|
| 125 |
+
common.save_wav(args.out, audio)
|
| 126 |
+
print(f"wrote {args.out} ({len(audio)} samples, {len(audio)/common.SR:.2f}s, {len(latents)} frames)")
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
if __name__ == "__main__":
|
| 130 |
+
main()
|
inference_asr.py
CHANGED
|
@@ -1,123 +1,123 @@
|
|
| 1 |
-
"""VibeVoice ASR inference (audio → text) over the exported ONNX sub-parts.
|
| 2 |
-
|
| 3 |
-
Handles BOTH ASR checkpoints (choose via the positional model — key or path):
|
| 4 |
-
asr (VibeVoice-ASR,
|
| 5 |
-
asr-hf (VibeVoice-ASR-HF, transformers) : multi_modal_projector fuses acoustic+semantic
|
| 6 |
-
|
| 7 |
-
Pipeline (reference algorithm on ONNX via common):
|
| 8 |
-
audio → acoustic_encoder + semantic_encoder → (connectors | projector) → speech features
|
| 9 |
-
→ build [system + <speech_start> <speech_pad>*N <speech_end> + instruction] embeds, inject the
|
| 10 |
-
speech features at the <speech_pad> positions → OnnxLLM.prefill (decoder KEEPS lm_head → logits)
|
| 11 |
-
→ greedy-decode tokens until EOS → tokenizer.decode → transcript.
|
| 12 |
-
|
| 13 |
-
Usage (prefer uv run) — the ONLY model input is the built ONNX dir; model_id/device/precision are
|
| 14 |
-
derived from the path and printed:
|
| 15 |
-
uv run inference_asr.py --audio speech.wav asr-hf/cpu_int4/models
|
| 16 |
-
uv run inference_asr.py --audio speech.wav asr/cuda_fp16/models
|
| 17 |
-
|
| 18 |
-
Note: the ASR LLM is Qwen2.5-7B; its int4 serialize is RAM-bound (see STATUS.md), so
|
| 19 |
-
llm_decoder.onnx may not be built. Without it this still runs the audio front-end and reports the
|
| 20 |
-
fused-feature shape, then explains that the 7B decoder must be built to produce text.
|
| 21 |
-
"""
|
| 22 |
-
import argparse
|
| 23 |
-
import json
|
| 24 |
-
import sys
|
| 25 |
-
from pathlib import Path
|
| 26 |
-
|
| 27 |
-
import numpy as np
|
| 28 |
-
|
| 29 |
-
sys.path.insert(0, str(Path(__file__).parent))
|
| 30 |
-
import common as C
|
| 31 |
-
|
| 32 |
-
SPEECH_TOKENS = ["<|speech_start|>", "<|speech_pad|>", "<|speech_end|>"]
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
def _resolve_speech_ids(tok):
|
| 36 |
-
ids = {}
|
| 37 |
-
for t in SPEECH_TOKENS:
|
| 38 |
-
i = tok.convert_tokens_to_ids(t)
|
| 39 |
-
ids[t] = i if isinstance(i, int) and i >= 0 else None
|
| 40 |
-
return ids
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
def encode_audio_features(key, src, onnx_dir, device, wav):
|
| 44 |
-
"""Return fused speech features [N, H] and frame count N from the ONNX front-end."""
|
| 45 |
-
a_enc = C.OnnxOp(onnx_dir / "acoustic_encoder.onnx", device)
|
| 46 |
-
s_enc = C.OnnxOp(onnx_dir / "semantic_encoder.onnx", device)
|
| 47 |
-
x = wav[None, None, :].astype(np.float32) # [1,1,T]
|
| 48 |
-
al = a_enc.run(audio=x) # [1,fa,64]
|
| 49 |
-
sl = s_enc.run(audio=x) # [1,fs,128]
|
| 50 |
-
n = min(al.shape[1], sl.shape[1])
|
| 51 |
-
al, sl = al[:, :n].astype(np.float32), sl[:, :n].astype(np.float32)
|
| 52 |
-
proj = C.component_path(onnx_dir, "multi_modal_projector")
|
| 53 |
-
if proj: # asr-hf
|
| 54 |
-
fused = C.OnnxOp(proj, device).run(acoustic_latents=al, semantic_latents=sl) # [1,n,H]
|
| 55 |
-
else: # asr: sum of the two connectors
|
| 56 |
-
ac = C.OnnxOp(onnx_dir / "acoustic_connector.onnx", device).run(features=al)
|
| 57 |
-
sc = C.OnnxOp(onnx_dir / "semantic_connector.onnx", device).run(features=sl)
|
| 58 |
-
fused = ac + sc
|
| 59 |
-
return fused[0].astype(np.float32), n # [n,H], n
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
def main():
|
| 63 |
-
ap = argparse.ArgumentParser(description="VibeVoice ASR (ONNX)")
|
| 64 |
-
ap.add_argument("model_path", help="built ONNX dir, e.g. asr-hf/cpu_int4/models")
|
| 65 |
-
ap.add_argument("--audio", required=True, help="input wav")
|
| 66 |
-
ap.add_argument("--max-new-tokens", type=int, default=256)
|
| 67 |
-
ap.add_argument("--prompt", default="Please transcribe the audio.")
|
| 68 |
-
args = ap.parse_args()
|
| 69 |
-
|
| 70 |
-
key, src, onnx_dir, device, precision, model_id = C.resolve_from_path(args.model_path)
|
| 71 |
-
print(f"=== ASR | model_id={model_id} device={device} precision={precision} ===")
|
| 72 |
-
if key not in ("asr", "asr-hf"):
|
| 73 |
-
sys.exit(f"[error] {model_id} ({key}) is not an ASR model — "
|
| 74 |
-
f"use inference.py (1.5b) or inference_realtime.py (realtime)")
|
| 75 |
-
for need in ("acoustic_encoder", "semantic_encoder"):
|
| 76 |
-
if not (onnx_dir / f"{need}.onnx").exists():
|
| 77 |
-
sys.exit(f"missing {need}.onnx in {onnx_dir} — build: uv run optimize.py {key}")
|
| 78 |
-
|
| 79 |
-
wav = C.normalize_audio(C.load_audio(args.audio))
|
| 80 |
-
feats, n = encode_audio_features(key, src, onnx_dir, device, wav)
|
| 81 |
-
H = feats.shape[-1]
|
| 82 |
-
print(f" audio {len(wav)/C.SR:.2f}s → {n} speech frames → fused features [{n},{H}]")
|
| 83 |
-
|
| 84 |
-
llm_path = onnx_dir / "llm_decoder.onnx"
|
| 85 |
-
if not llm_path.exists():
|
| 86 |
-
print(f"\n[front-end OK] llm_decoder.onnx not built (Qwen2.5-7B int4 is RAM-bound — see STATUS.md).")
|
| 87 |
-
print(f" Build it after freeing RAM: uv run optimize.py --components llm {key}")
|
| 88 |
-
print(f" Then re-run for the transcript.")
|
| 89 |
-
return
|
| 90 |
-
|
| 91 |
-
tok = C.load_tokenizer(onnx_dir, src)
|
| 92 |
-
sid = _resolve_speech_ids(tok)
|
| 93 |
-
# Prompt: system + user(<speech_start> <speech_pad>*n <speech_end> + instruction).
|
| 94 |
-
pad = sid["<|speech_pad|>"]
|
| 95 |
-
if pad is None:
|
| 96 |
-
# tokenizer lacks the speech tokens (repo shipped none) — add them, then use a pad id.
|
| 97 |
-
tok.add_special_tokens({"additional_special_tokens": SPEECH_TOKENS})
|
| 98 |
-
sid = _resolve_speech_ids(tok); pad = sid["<|speech_pad|>"]
|
| 99 |
-
start = sid["<|speech_start|>"] if sid["<|speech_start|>"] is not None else pad
|
| 100 |
-
end = sid["<|speech_end|>"] if sid["<|speech_end|>"] is not None else pad
|
| 101 |
-
pre = tok.encode(f"{args.prompt}\n")
|
| 102 |
-
ids = pre + [start] + [pad] * n + [end]
|
| 103 |
-
embeds = C.embed_tokens(src, ids, key) # [1,S,H]
|
| 104 |
-
# inject at the n dedicated speech-pad slots (the [pad]*n block), not any incidental pad id
|
| 105 |
-
pad_pos = list(range(len(pre) + 1, len(pre) + 1 + n))
|
| 106 |
-
embeds[0, pad_pos, :] = feats[:len(pad_pos)] # inject speech features
|
| 107 |
-
|
| 108 |
-
llm = C.OnnxLLM(llm_path, device)
|
| 109 |
-
logits = llm.prefill(embeds) # [1,S,V]
|
| 110 |
-
eos = getattr(tok, "eos_token_id", None)
|
| 111 |
-
out = []
|
| 112 |
-
for _ in range(args.max_new_tokens):
|
| 113 |
-
nxt = int(np.asarray(logits)[0, -1].argmax())
|
| 114 |
-
if eos is not None and nxt == eos:
|
| 115 |
-
break
|
| 116 |
-
out.append(nxt)
|
| 117 |
-
logits = llm.step(C.embed_tokens(src, [nxt], key))
|
| 118 |
-
text = tok.decode(out, skip_special_tokens=True)
|
| 119 |
-
print(f"\nTRANSCRIPT:\n{text}")
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
if __name__ == "__main__":
|
| 123 |
-
main()
|
|
|
|
| 1 |
+
"""VibeVoice ASR inference (audio → text) over the exported ONNX sub-parts.
|
| 2 |
+
|
| 3 |
+
Handles BOTH ASR checkpoints (choose via the positional model — key or path):
|
| 4 |
+
asr (VibeVoice-ASR, vibevoice/ family) : acoustic_connector + semantic_connector, fused = sum
|
| 5 |
+
asr-hf (VibeVoice-ASR-HF, transformers) : multi_modal_projector fuses acoustic+semantic
|
| 6 |
+
|
| 7 |
+
Pipeline (reference algorithm on ONNX via common):
|
| 8 |
+
audio → acoustic_encoder + semantic_encoder → (connectors | projector) → speech features
|
| 9 |
+
→ build [system + <speech_start> <speech_pad>*N <speech_end> + instruction] embeds, inject the
|
| 10 |
+
speech features at the <speech_pad> positions → OnnxLLM.prefill (decoder KEEPS lm_head → logits)
|
| 11 |
+
→ greedy-decode tokens until EOS → tokenizer.decode → transcript.
|
| 12 |
+
|
| 13 |
+
Usage (prefer uv run) — the ONLY model input is the built ONNX dir; model_id/device/precision are
|
| 14 |
+
derived from the path and printed:
|
| 15 |
+
uv run inference_asr.py --audio speech.wav asr-hf/cpu_int4/models
|
| 16 |
+
uv run inference_asr.py --audio speech.wav asr/cuda_fp16/models
|
| 17 |
+
|
| 18 |
+
Note: the ASR LLM is Qwen2.5-7B; its int4 serialize is RAM-bound (see STATUS.md), so
|
| 19 |
+
llm_decoder.onnx may not be built. Without it this still runs the audio front-end and reports the
|
| 20 |
+
fused-feature shape, then explains that the 7B decoder must be built to produce text.
|
| 21 |
+
"""
|
| 22 |
+
import argparse
|
| 23 |
+
import json
|
| 24 |
+
import sys
|
| 25 |
+
from pathlib import Path
|
| 26 |
+
|
| 27 |
+
import numpy as np
|
| 28 |
+
|
| 29 |
+
sys.path.insert(0, str(Path(__file__).parent))
|
| 30 |
+
import common as C
|
| 31 |
+
|
| 32 |
+
SPEECH_TOKENS = ["<|speech_start|>", "<|speech_pad|>", "<|speech_end|>"]
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def _resolve_speech_ids(tok):
|
| 36 |
+
ids = {}
|
| 37 |
+
for t in SPEECH_TOKENS:
|
| 38 |
+
i = tok.convert_tokens_to_ids(t)
|
| 39 |
+
ids[t] = i if isinstance(i, int) and i >= 0 else None
|
| 40 |
+
return ids
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def encode_audio_features(key, src, onnx_dir, device, wav):
|
| 44 |
+
"""Return fused speech features [N, H] and frame count N from the ONNX front-end."""
|
| 45 |
+
a_enc = C.OnnxOp(onnx_dir / "acoustic_encoder.onnx", device)
|
| 46 |
+
s_enc = C.OnnxOp(onnx_dir / "semantic_encoder.onnx", device)
|
| 47 |
+
x = wav[None, None, :].astype(np.float32) # [1,1,T]
|
| 48 |
+
al = a_enc.run(audio=x) # [1,fa,64]
|
| 49 |
+
sl = s_enc.run(audio=x) # [1,fs,128]
|
| 50 |
+
n = min(al.shape[1], sl.shape[1])
|
| 51 |
+
al, sl = al[:, :n].astype(np.float32), sl[:, :n].astype(np.float32)
|
| 52 |
+
proj = C.component_path(onnx_dir, "multi_modal_projector")
|
| 53 |
+
if proj: # asr-hf
|
| 54 |
+
fused = C.OnnxOp(proj, device).run(acoustic_latents=al, semantic_latents=sl) # [1,n,H]
|
| 55 |
+
else: # asr: sum of the two connectors
|
| 56 |
+
ac = C.OnnxOp(onnx_dir / "acoustic_connector.onnx", device).run(features=al)
|
| 57 |
+
sc = C.OnnxOp(onnx_dir / "semantic_connector.onnx", device).run(features=sl)
|
| 58 |
+
fused = ac + sc
|
| 59 |
+
return fused[0].astype(np.float32), n # [n,H], n
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def main():
|
| 63 |
+
ap = argparse.ArgumentParser(description="VibeVoice ASR (ONNX)")
|
| 64 |
+
ap.add_argument("model_path", help="built ONNX dir, e.g. asr-hf/cpu_int4/models")
|
| 65 |
+
ap.add_argument("--audio", required=True, help="input wav")
|
| 66 |
+
ap.add_argument("--max-new-tokens", type=int, default=256)
|
| 67 |
+
ap.add_argument("--prompt", default="Please transcribe the audio.")
|
| 68 |
+
args = ap.parse_args()
|
| 69 |
+
|
| 70 |
+
key, src, onnx_dir, device, precision, model_id = C.resolve_from_path(args.model_path)
|
| 71 |
+
print(f"=== ASR | model_id={model_id} device={device} precision={precision} ===")
|
| 72 |
+
if key not in ("asr", "asr-hf"):
|
| 73 |
+
sys.exit(f"[error] {model_id} ({key}) is not an ASR model — "
|
| 74 |
+
f"use inference.py (1.5b) or inference_realtime.py (realtime)")
|
| 75 |
+
for need in ("acoustic_encoder", "semantic_encoder"):
|
| 76 |
+
if not (onnx_dir / f"{need}.onnx").exists():
|
| 77 |
+
sys.exit(f"missing {need}.onnx in {onnx_dir} — build: uv run optimize.py {key}")
|
| 78 |
+
|
| 79 |
+
wav = C.normalize_audio(C.load_audio(args.audio))
|
| 80 |
+
feats, n = encode_audio_features(key, src, onnx_dir, device, wav)
|
| 81 |
+
H = feats.shape[-1]
|
| 82 |
+
print(f" audio {len(wav)/C.SR:.2f}s → {n} speech frames → fused features [{n},{H}]")
|
| 83 |
+
|
| 84 |
+
llm_path = onnx_dir / "llm_decoder.onnx"
|
| 85 |
+
if not llm_path.exists():
|
| 86 |
+
print(f"\n[front-end OK] llm_decoder.onnx not built (Qwen2.5-7B int4 is RAM-bound — see STATUS.md).")
|
| 87 |
+
print(f" Build it after freeing RAM: uv run optimize.py --components llm {key}")
|
| 88 |
+
print(f" Then re-run for the transcript.")
|
| 89 |
+
return
|
| 90 |
+
|
| 91 |
+
tok = C.load_tokenizer(onnx_dir, src)
|
| 92 |
+
sid = _resolve_speech_ids(tok)
|
| 93 |
+
# Prompt: system + user(<speech_start> <speech_pad>*n <speech_end> + instruction).
|
| 94 |
+
pad = sid["<|speech_pad|>"]
|
| 95 |
+
if pad is None:
|
| 96 |
+
# tokenizer lacks the speech tokens (repo shipped none) — add them, then use a pad id.
|
| 97 |
+
tok.add_special_tokens({"additional_special_tokens": SPEECH_TOKENS})
|
| 98 |
+
sid = _resolve_speech_ids(tok); pad = sid["<|speech_pad|>"]
|
| 99 |
+
start = sid["<|speech_start|>"] if sid["<|speech_start|>"] is not None else pad
|
| 100 |
+
end = sid["<|speech_end|>"] if sid["<|speech_end|>"] is not None else pad
|
| 101 |
+
pre = tok.encode(f"{args.prompt}\n")
|
| 102 |
+
ids = pre + [start] + [pad] * n + [end]
|
| 103 |
+
embeds = C.embed_tokens(src, ids, key) # [1,S,H]
|
| 104 |
+
# inject at the n dedicated speech-pad slots (the [pad]*n block), not any incidental pad id
|
| 105 |
+
pad_pos = list(range(len(pre) + 1, len(pre) + 1 + n))
|
| 106 |
+
embeds[0, pad_pos, :] = feats[:len(pad_pos)] # inject speech features
|
| 107 |
+
|
| 108 |
+
llm = C.OnnxLLM(llm_path, device)
|
| 109 |
+
logits = llm.prefill(embeds) # [1,S,V]
|
| 110 |
+
eos = getattr(tok, "eos_token_id", None)
|
| 111 |
+
out = []
|
| 112 |
+
for _ in range(args.max_new_tokens):
|
| 113 |
+
nxt = int(np.asarray(logits)[0, -1].argmax())
|
| 114 |
+
if eos is not None and nxt == eos:
|
| 115 |
+
break
|
| 116 |
+
out.append(nxt)
|
| 117 |
+
logits = llm.step(C.embed_tokens(src, [nxt], key))
|
| 118 |
+
text = tok.decode(out, skip_special_tokens=True)
|
| 119 |
+
print(f"\nTRANSCRIPT:\n{text}")
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
if __name__ == "__main__":
|
| 123 |
+
main()
|
inference_tokenizer.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""VibeVoice Acoustic Tokenizer — ONNX encode/decode driver (the shared 7.5 Hz speech codec used by
|
| 2 |
+
the VibeVoice TTS and ASR models). onnxruntime + numpy only (+ soundfile/librosa for audio I/O).
|
| 3 |
+
|
| 4 |
+
Flow (hop = 3200 samples/frame @ 24 kHz):
|
| 5 |
+
|
| 6 |
+
audio ─[acoustic_encoder]→ latents [1, frames, 64] ─[acoustic_decoder]→ audio
|
| 7 |
+
(encoder fixed at 24000 samples/call → 7 frames; long audio is encoded in 1 s chunks)
|
| 8 |
+
|
| 9 |
+
Usage:
|
| 10 |
+
python inference_tokenizer.py --models-dir fp32 --input in.wav --output recon.wav # round-trip
|
| 11 |
+
python inference_tokenizer.py --models-dir fp32 --input in.wav --latents-out z.npy --encode-only
|
| 12 |
+
python inference_tokenizer.py --models-dir fp32 --latents-in z.npy --output out.wav --decode-only
|
| 13 |
+
"""
|
| 14 |
+
import argparse
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
|
| 17 |
+
import numpy as np
|
| 18 |
+
|
| 19 |
+
SR = 24_000 # sampling rate
|
| 20 |
+
CHUNK = 24_000 # encoder's baked input length (1 s)
|
| 21 |
+
HOP = 3_200 # samples per latent frame
|
| 22 |
+
VAE_DIM = 64
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _session(path, provider):
|
| 26 |
+
import onnxruntime as ort
|
| 27 |
+
so = ort.SessionOptions(); so.log_severity_level = 3
|
| 28 |
+
return ort.InferenceSession(str(path), sess_options=so, providers=[provider])
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _npdtype(sess):
|
| 32 |
+
return np.float16 if "float16" in sess.get_inputs()[0].type else np.float32
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def load_audio(path):
|
| 36 |
+
import soundfile as sf
|
| 37 |
+
wav, sr = sf.read(path, dtype="float32", always_2d=False)
|
| 38 |
+
if wav.ndim > 1:
|
| 39 |
+
wav = wav.mean(1)
|
| 40 |
+
if sr != SR:
|
| 41 |
+
import librosa
|
| 42 |
+
wav = librosa.resample(wav, orig_sr=sr, target_sr=SR)
|
| 43 |
+
return wav.astype(np.float32)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def save_audio(path, wav):
|
| 47 |
+
import soundfile as sf
|
| 48 |
+
sf.write(path, np.asarray(wav, np.float32).ravel(), SR, subtype="PCM_16")
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def encode(enc, wav):
|
| 52 |
+
"""wav [T] → latents [1, frames, 64], printing the encoder flow. Chunks into 1 s windows."""
|
| 53 |
+
npdt = _npdtype(enc)
|
| 54 |
+
n = int(np.ceil(len(wav) / CHUNK))
|
| 55 |
+
padded = np.pad(wav, (0, n * CHUNK - len(wav)))
|
| 56 |
+
dt = "fp16" if npdt == np.float16 else "fp32"
|
| 57 |
+
print(f"[encode] {len(wav)} samples ({len(wav)/SR:.2f}s) @ {SR} Hz -> {n} x {CHUNK} chunk(s) "
|
| 58 |
+
f"(padded to {len(padded)}), {dt}")
|
| 59 |
+
lat = []
|
| 60 |
+
for i in range(n):
|
| 61 |
+
chunk = padded[i * CHUNK:(i + 1) * CHUNK][None, None, :].astype(npdt)
|
| 62 |
+
z = enc.run(["latents"], {"audio": chunk})[0]
|
| 63 |
+
lat.append(z)
|
| 64 |
+
print(f" encoder: audio[1,1,{CHUNK}] -> latents{list(z.shape)} (chunk {i+1}/{n})")
|
| 65 |
+
latents = np.concatenate(lat, axis=1)
|
| 66 |
+
print(f"[encode] done: latents {list(latents.shape)} (7.5 Hz, {VAE_DIM}-dim per frame)")
|
| 67 |
+
return latents
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def decode(dec, latents):
|
| 71 |
+
"""latents [1, frames, 64] → audio [samples], printing the decoder flow."""
|
| 72 |
+
npdt = _npdtype(dec)
|
| 73 |
+
frames = latents.shape[1]
|
| 74 |
+
audio = dec.run(["audio"], {"latents": latents.astype(npdt)})[0].ravel()
|
| 75 |
+
print(f"[decode] decoder: latents[1,{frames},{VAE_DIM}] -> audio[1,1,{len(audio)}] "
|
| 76 |
+
f"({len(audio)/SR:.2f}s, {HOP}/frame)")
|
| 77 |
+
return audio
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def main():
|
| 81 |
+
ap = argparse.ArgumentParser(description="VibeVoice Acoustic Tokenizer — ONNX encode/decode")
|
| 82 |
+
ap.add_argument("--models-dir", default=".", help="dir with acoustic_encoder/decoder.onnx (fp32|fp16)")
|
| 83 |
+
ap.add_argument("--input", "-i", default=None)
|
| 84 |
+
ap.add_argument("--output", "-o", default="reconstructed.wav")
|
| 85 |
+
ap.add_argument("--latents-out", default=None)
|
| 86 |
+
ap.add_argument("--latents-in", default=None)
|
| 87 |
+
ap.add_argument("--encode-only", action="store_true")
|
| 88 |
+
ap.add_argument("--decode-only", action="store_true")
|
| 89 |
+
ap.add_argument("--cuda", action="store_true")
|
| 90 |
+
args = ap.parse_args()
|
| 91 |
+
prov = "CUDAExecutionProvider" if args.cuda else "CPUExecutionProvider"
|
| 92 |
+
d = Path(args.models_dir)
|
| 93 |
+
print(f"=== VibeVoice Acoustic Tokenizer (ONNX) | models={d} | {prov} ===")
|
| 94 |
+
|
| 95 |
+
if args.decode_only:
|
| 96 |
+
latents = np.load(args.latents_in)
|
| 97 |
+
wav = decode(_session(d / "acoustic_decoder.onnx", prov), latents)
|
| 98 |
+
save_audio(args.output, wav)
|
| 99 |
+
print(f"saved -> {args.output}")
|
| 100 |
+
return
|
| 101 |
+
|
| 102 |
+
wav = load_audio(args.input)
|
| 103 |
+
latents = encode(_session(d / "acoustic_encoder.onnx", prov), wav)
|
| 104 |
+
if args.latents_out:
|
| 105 |
+
np.save(args.latents_out, latents); print(f"latents -> {args.latents_out}")
|
| 106 |
+
if args.encode_only:
|
| 107 |
+
return
|
| 108 |
+
recon = decode(_session(d / "acoustic_decoder.onnx", prov), latents)
|
| 109 |
+
save_audio(args.output, recon)
|
| 110 |
+
n = min(len(recon), len(wav))
|
| 111 |
+
corr = float(np.corrcoef(wav[:n], recon[:n])[0, 1]) if n > 1 else float("nan")
|
| 112 |
+
print(f"saved -> {args.output} round-trip corr={corr:+.3f}")
|
| 113 |
+
if len(wav) > CHUNK:
|
| 114 |
+
print(" note: multi-chunk round-trip loses ~1600 samples/chunk at the fixed-encoder edge "
|
| 115 |
+
"(cumulative time offset lowers corr); per-1s-window parity is corr ~0.999.")
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
if __name__ == "__main__":
|
| 119 |
+
main()
|
int4.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:dcf76d2f23faf13e367b2b09cbef16b597849163ff74fad3b66a7619a6828bc2
|
| 3 |
+
size 1280044
|
optimize.py
CHANGED
|
@@ -211,7 +211,11 @@ def build_olive(model: str, component: str, model_src: Path, target: Path, preci
|
|
| 211 |
tmp = (target / f"_{component}_tmp").resolve()
|
| 212 |
passes = {"c": {"type": "OnnxConversion", "use_dynamo_exporter": True}}
|
| 213 |
if precision == "fp16":
|
| 214 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
cfg = {
|
| 216 |
"input_model": {"type": "PyTorchModel", "model_path": str(model_src),
|
| 217 |
"model_loader": loader, "model_script": str(HERE / "user_script.py"),
|
|
@@ -257,6 +261,17 @@ def build_model(model: str, model_src: Path, device: str, precision: str, compon
|
|
| 257 |
comps = [c for c in comps if c != "llm"]
|
| 258 |
print(f"[{model}] --exclude-llm: skipping the LLM decoder build")
|
| 259 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
print(f"\n=== {model} src={model_src} device={device} precision={precision} ===")
|
| 261 |
print(f" target={target} components={comps}")
|
| 262 |
for comp in comps:
|
|
|
|
| 211 |
tmp = (target / f"_{component}_tmp").resolve()
|
| 212 |
passes = {"c": {"type": "OnnxConversion", "use_dynamo_exporter": True}}
|
| 213 |
if precision == "fp16":
|
| 214 |
+
# keep shape/resample ops in fp32 — converting them yields invalid graphs (e.g. the acoustic
|
| 215 |
+
# tokenizer's ConstantOfShape emitted fp16 where a float32 consumer expects it) and they gain
|
| 216 |
+
# nothing from fp16. Same class of block-list the Higgs encoders needed.
|
| 217 |
+
passes["f16"] = {"type": "OnnxFloatToFloat16",
|
| 218 |
+
"op_block_list": ["ConstantOfShape", "ConvTranspose", "Resize", "Range"]}
|
| 219 |
cfg = {
|
| 220 |
"input_model": {"type": "PyTorchModel", "model_path": str(model_src),
|
| 221 |
"model_loader": loader, "model_script": str(HERE / "user_script.py"),
|
|
|
|
| 261 |
comps = [c for c in comps if c != "llm"]
|
| 262 |
print(f"[{model}] --exclude-llm: skipping the LLM decoder build")
|
| 263 |
|
| 264 |
+
# int4 only quantizes the LLM (MatMulNBits). Keys with no LLM (e.g. acoustic — a conv VAE) have
|
| 265 |
+
# no quantizable MatMul weights, so int4 is a no-op that just re-emits fp32. Warn + downgrade so
|
| 266 |
+
# the output isn't a misleadingly-named fp32 copy.
|
| 267 |
+
if precision == "int4" and "llm" not in comps:
|
| 268 |
+
print(f"[{model}] WARNING: int4 is a no-op here (no LLM / conv-VAE components have no "
|
| 269 |
+
f"quantizable MatMul weights) — building fp32 instead.")
|
| 270 |
+
precision = "fp32"
|
| 271 |
+
if not output_dir:
|
| 272 |
+
target = HERE / "onnx" / model / f"{device}_fp32"
|
| 273 |
+
target.mkdir(parents=True, exist_ok=True)
|
| 274 |
+
|
| 275 |
print(f"\n=== {model} src={model_src} device={device} precision={precision} ===")
|
| 276 |
print(f" target={target} components={comps}")
|
| 277 |
for comp in comps:
|
pyproject.toml
CHANGED
|
@@ -14,13 +14,11 @@ dependencies = [
|
|
| 14 |
"safetensors>=0.4",
|
| 15 |
"soundfile>=0.12",
|
| 16 |
"librosa>=0.10",
|
| 17 |
-
# VibeVoice source
|
| 18 |
-
#
|
| 19 |
-
#
|
| 20 |
-
#
|
| 21 |
-
#
|
| 22 |
-
# executing that __init__.
|
| 23 |
-
"vibevoice @ git+https://github.com/microsoft/VibeVoice.git@main",
|
| 24 |
]
|
| 25 |
|
| 26 |
# Build the ONNX sub-parts (needs the source checkpoint + onnxruntime-genai ModelBuilder):
|
|
|
|
| 14 |
"safetensors>=0.4",
|
| 15 |
"soundfile>=0.12",
|
| 16 |
"librosa>=0.10",
|
| 17 |
+
# The VibeVoice source (DPMSolverMultistepScheduler + modular tokenizer/diffusion-head classes)
|
| 18 |
+
# is VENDORED — it ships as the `vibevoice/` directory alongside these drivers, so there is NO
|
| 19 |
+
# pip/git dependency on it. The drivers import individual leaf modules from that tree by path
|
| 20 |
+
# (isolated-import shim), never `import vibevoice` (HANDOFF trap 1: its package __init__ collides
|
| 21 |
+
# with transformers' native `vibevoice` registration and pulls diffusers + a renamed qwen2 tokenizer).
|
|
|
|
|
|
|
| 22 |
]
|
| 23 |
|
| 24 |
# Build the ONNX sub-parts (needs the source checkpoint + onnxruntime-genai ModelBuilder):
|
samples/text_examples/1p_abs.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Speaker 1: Generating long-form, multi-speaker conversational audio like podcasts poses significant challenges for traditional Text-to-Speech (TTS) systems, particularly in scalability, speaker consistency, and natural turn-taking. This report presents VibeVoice, a novel model designed to synthesize long-form speech with multiple speakers by employing the next-token diffusion framework, a unified method for modeling continuous data by autoregressively generating latent vectors via diffusion.
|
| 2 |
+
|
| 3 |
+
Speaker 1: A core component of our approach is the continuous speech tokenizers operating at an ultra-low frame rate of 7.5. This tokenizer effectively preserves audio fidelity while significantly boosting computational efficiency for processing long sequences. This enables VibeVoice to synthesize long-form speech for up to 90 minutes (in a 64K context window length) with up to 4 speakers, capturing the authentic conversational "vibe" and surpassing all known open-source and closed-source dialogue models (for example, Gemini 2.5 Pro Preview TTS). Code and checkpoint are available now.
|
samples/voices/en-Alice_woman.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c27ae47421436287a6bd2c3062de2dc2a2855b78c0bb626d472202c359704203
|
| 3 |
+
size 296684
|
tts_out.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fcf07f8a8a0de7126190b7f32672cd137d9d4ada42bd548915efb6a724246b5e
|
| 3 |
+
size 1100844
|
user_script.py
CHANGED
|
@@ -126,7 +126,7 @@ def extract_qwen2_asrhf(model_path: str, output_dir: str) -> str:
|
|
| 126 |
|
| 127 |
|
| 128 |
# =============================================================================
|
| 129 |
-
# Acoustic tokenizer (VAE codec) — vendored
|
| 130 |
# (552 weights, 0 missing). We import ONLY the tokenizer module in isolation (the
|
| 131 |
# package __init__ pulls the streaming/diffusion chain → diffusers + a qwen2-tokenizer
|
| 132 |
# import that transformers 5.10.2 renamed), and shim Auto*.register so it coexists with
|
|
@@ -135,27 +135,21 @@ def extract_qwen2_asrhf(model_path: str, output_dir: str) -> str:
|
|
| 135 |
# stages/head; ASR-HF: conv_layers) — this loader targets the 1.5B one.
|
| 136 |
# =============================================================================
|
| 137 |
import sys as _sys
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
| 139 |
|
| 140 |
|
| 141 |
def _vibevoice_dir():
|
| 142 |
-
"""
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
return local
|
| 147 |
-
import importlib.util
|
| 148 |
-
spec = importlib.util.find_spec("vibevoice")
|
| 149 |
-
if spec and spec.submodule_search_locations:
|
| 150 |
-
return list(spec.submodule_search_locations)[0]
|
| 151 |
-
raise ModuleNotFoundError(
|
| 152 |
-
"vibevoice source not found. Either populate the codes/ submodule\n"
|
| 153 |
-
" (git submodule update --init --force VibeVoice/codes), or install the git dependency:\n"
|
| 154 |
-
" uv pip install 'vibevoice @ git+https://github.com/microsoft/VibeVoice.git@303b283'")
|
| 155 |
|
| 156 |
|
| 157 |
def _codes_import(submodule):
|
| 158 |
-
"""Isolated import of a single
|
| 159 |
Auto*.register (coexist with transformers) and injects empty `vibevoice[.modular]`
|
| 160 |
parent packages so the package __init__ (diffusers + renamed qwen2-tokenizer) never runs."""
|
| 161 |
import types, importlib
|
|
@@ -166,18 +160,17 @@ def _codes_import(submodule):
|
|
| 166 |
try: __r(*a, **k)
|
| 167 |
except Exception: pass
|
| 168 |
cls.register = staticmethod(_safe)
|
| 169 |
-
# status: make the vibevoice source
|
| 170 |
base = _vibevoice_dir()
|
| 171 |
target = os.path.join(base, "modular", submodule + ".py")
|
| 172 |
-
|
| 173 |
-
print(f"[codes] isolated-import vibevoice.modular.{submodule} <- {target} ({src})")
|
| 174 |
if not os.path.exists(target):
|
| 175 |
-
raise ModuleNotFoundError(f"{target} missing in the
|
| 176 |
for name, sub in [("vibevoice", ""), ("vibevoice.modular", "modular")]: # empty parent pkgs
|
| 177 |
m = types.ModuleType(name); m.__path__ = [os.path.join(base, sub)]
|
| 178 |
_sys.modules[name] = m
|
| 179 |
mod = importlib.import_module("vibevoice.modular." + submodule)
|
| 180 |
-
print(f"[
|
| 181 |
return mod
|
| 182 |
|
| 183 |
|
|
@@ -188,7 +181,7 @@ def _codes_tokenizer():
|
|
| 188 |
|
| 189 |
|
| 190 |
def _load_acoustic(model_path):
|
| 191 |
-
"""Load VibeVoice-1.5B's acoustic tokenizer (VAE) via vendored
|
| 192 |
import glob
|
| 193 |
from safetensors.torch import load_file
|
| 194 |
tok, ACfg = _codes_tokenizer()
|
|
@@ -223,15 +216,17 @@ def get_acoustic_encoder_model(model_path=None):
|
|
| 223 |
|
| 224 |
|
| 225 |
def get_acoustic_encoder_io_config(model=None):
|
|
|
|
|
|
|
|
|
|
| 226 |
return {"input_names": ["audio"], "output_names": ["latents"],
|
| 227 |
-
"input_shapes": [[1, 1,
|
| 228 |
-
"
|
| 229 |
-
"latents": {0: "batch", 2: "frames"}}}
|
| 230 |
|
| 231 |
|
| 232 |
def get_acoustic_encoder_dummy_inputs(model=None):
|
| 233 |
-
import torch
|
| 234 |
-
return {"audio": torch.randn(1, 1,
|
| 235 |
|
| 236 |
|
| 237 |
def get_acoustic_decoder_model(model_path=None):
|
|
@@ -298,7 +293,7 @@ def get_acoustic_std_decoder_model(model_path=None):
|
|
| 298 |
|
| 299 |
# =============================================================================
|
| 300 |
# ASR-HF acoustic encoder — transformers-NATIVE (VibeVoiceAcousticTokenizerEncoderModel),
|
| 301 |
-
# different arch than 1.5B (conv_layers). No
|
| 302 |
# `acoustic_tokenizer_encoder.*` weights (not the 7B LLM) so it fits in memory.
|
| 303 |
# =============================================================================
|
| 304 |
|
|
@@ -441,12 +436,12 @@ def get_asrhf_projector_dummy_inputs(model=None):
|
|
| 441 |
|
| 442 |
|
| 443 |
# =============================================================================
|
| 444 |
-
# Realtime-0.5B (`vibevoice_streaming`, auto_map null →
|
| 445 |
# checkpoint. Key groups: model.tts_language_model.* (Qwen2.5-0.5B backbone, 20 layers,
|
| 446 |
# no lm_head), model.acoustic_tokenizer.* (DECODER-ONLY, 276 — no encoder shipped, since
|
| 447 |
# inference only DECODES generated latents → audio), model.language_model.* (4-layer base),
|
| 448 |
# model.prediction_head.* (diffusion), model.acoustic_connector.*, tts_eos_classifier.*.
|
| 449 |
-
# The acoustic decoder matches the
|
| 450 |
# missing / 0 unexpected) — NOT transformers-native (conv_layers/convtr naming).
|
| 451 |
# =============================================================================
|
| 452 |
RT_TTS_LM_PREFIX = "model.tts_language_model."
|
|
@@ -495,8 +490,8 @@ def extract_qwen2_realtime(model_path: str, output_dir: str) -> str:
|
|
| 495 |
|
| 496 |
|
| 497 |
def _load_realtime_acoustic_decoder(model_path):
|
| 498 |
-
"""Realtime acoustic tokenizer (DECODER-ONLY) via
|
| 499 |
-
(encoder absent from the checkpoint), drops the encoder module.
|
| 500 |
import glob
|
| 501 |
from safetensors.torch import load_file
|
| 502 |
tok, ACfg = _codes_tokenizer()
|
|
@@ -538,7 +533,7 @@ def get_realtime_acoustic_decoder_dummy_inputs(model=None):
|
|
| 538 |
|
| 539 |
|
| 540 |
# =============================================================================
|
| 541 |
-
# Diffusion prediction_head + speech connectors (shared 1.5B / Realtime, via
|
| 542 |
# diffusion_head: ONE denoise step (noisy_images[B,64], timesteps[B], condition[B,H]) → pred[B,64].
|
| 543 |
# The ~20-step DDPM sampling loop stays in the pipeline; ONNX = one step.
|
| 544 |
# connector: SpeechConnector fc1(in→H) → RMSNorm(H) → fc2(H→H). 1.5B: acoustic 64→1536,
|
|
@@ -653,16 +648,16 @@ def get_semantic_connector_dummy_inputs(model=None):
|
|
| 653 |
|
| 654 |
|
| 655 |
# =============================================================================
|
| 656 |
-
# VibeVoice-ASR (`vibevoice`, VibeVoiceForASRTraining,
|
| 657 |
-
# same
|
| 658 |
# there is NO prediction_head (no audio generation). Front-end = full acoustic tokenizer (552,
|
| 659 |
# enc+dec) + semantic tokenizer (276, ENCODE-only) + acoustic/semantic connectors — all load via
|
| 660 |
-
# the existing
|
| 661 |
# Weight layout: model.language_model.* (338) + top-level lm_head.weight (unlike ASR-HF).
|
| 662 |
# =============================================================================
|
| 663 |
|
| 664 |
def _load_semantic(model_path):
|
| 665 |
-
"""Semantic tokenizer (ENCODE-only, deterministic latent = encode().mean) via
|
| 666 |
import glob
|
| 667 |
from safetensors.torch import load_file
|
| 668 |
tok = _codes_import("modular_vibevoice_tokenizer")
|
|
|
|
| 126 |
|
| 127 |
|
| 128 |
# =============================================================================
|
| 129 |
+
# Acoustic tokenizer (VAE codec) — the vendored vibevoice/ source matches VibeVoice-1.5B EXACTLY
|
| 130 |
# (552 weights, 0 missing). We import ONLY the tokenizer module in isolation (the
|
| 131 |
# package __init__ pulls the streaming/diffusion chain → diffusers + a qwen2-tokenizer
|
| 132 |
# import that transformers 5.10.2 renamed), and shim Auto*.register so it coexists with
|
|
|
|
| 135 |
# stages/head; ASR-HF: conv_layers) — this loader targets the 1.5B one.
|
| 136 |
# =============================================================================
|
| 137 |
import sys as _sys
|
| 138 |
+
# The required VibeVoice source is VENDORED here at VibeVoice/vibevoice/ (no submodule, no git
|
| 139 |
+
# dependency) — see VIBEVOICE_LICENSE. We still import it in isolation (below) because its package
|
| 140 |
+
# __init__ collides with transformers' native registration.
|
| 141 |
+
_VENDORED = str(Path(__file__).parent / "vibevoice")
|
| 142 |
|
| 143 |
|
| 144 |
def _vibevoice_dir():
|
| 145 |
+
"""The vendored vibevoice source tree shipped alongside this code."""
|
| 146 |
+
if os.path.isdir(_VENDORED):
|
| 147 |
+
return _VENDORED
|
| 148 |
+
raise ModuleNotFoundError(f"vendored vibevoice source missing at {_VENDORED}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
|
| 150 |
|
| 151 |
def _codes_import(submodule):
|
| 152 |
+
"""Isolated import of a single vibevoice/modular/<submodule> module. Shims
|
| 153 |
Auto*.register (coexist with transformers) and injects empty `vibevoice[.modular]`
|
| 154 |
parent packages so the package __init__ (diffusers + renamed qwen2-tokenizer) never runs."""
|
| 155 |
import types, importlib
|
|
|
|
| 160 |
try: __r(*a, **k)
|
| 161 |
except Exception: pass
|
| 162 |
cls.register = staticmethod(_safe)
|
| 163 |
+
# status: make the vendored vibevoice source (and where it resolves from) visible in the log
|
| 164 |
base = _vibevoice_dir()
|
| 165 |
target = os.path.join(base, "modular", submodule + ".py")
|
| 166 |
+
print(f"[vibevoice] isolated-import vibevoice.modular.{submodule} <- {target} (vendored)")
|
|
|
|
| 167 |
if not os.path.exists(target):
|
| 168 |
+
raise ModuleNotFoundError(f"{target} missing in the vendored vibevoice source ({base}).")
|
| 169 |
for name, sub in [("vibevoice", ""), ("vibevoice.modular", "modular")]: # empty parent pkgs
|
| 170 |
m = types.ModuleType(name); m.__path__ = [os.path.join(base, sub)]
|
| 171 |
_sys.modules[name] = m
|
| 172 |
mod = importlib.import_module("vibevoice.modular." + submodule)
|
| 173 |
+
print(f"[vibevoice] loaded {submodule} OK (vendored)")
|
| 174 |
return mod
|
| 175 |
|
| 176 |
|
|
|
|
| 181 |
|
| 182 |
|
| 183 |
def _load_acoustic(model_path):
|
| 184 |
+
"""Load VibeVoice-1.5B's acoustic tokenizer (VAE) via the vendored vibevoice/ source, weights loaded."""
|
| 185 |
import glob
|
| 186 |
from safetensors.torch import load_file
|
| 187 |
tok, ACfg = _codes_tokenizer()
|
|
|
|
| 216 |
|
| 217 |
|
| 218 |
def get_acoustic_encoder_io_config(model=None):
|
| 219 |
+
# dynamo IGNORES dynamic_axes (trap #2) — use dynamic_shapes so the audio length is variable,
|
| 220 |
+
# else it bakes 24000 (which isn't a multiple of the 3200 hop → 7 vs 7.5 frame drift that
|
| 221 |
+
# breaks alignment with the processor's speech-token count). frames = samples / 3200.
|
| 222 |
return {"input_names": ["audio"], "output_names": ["latents"],
|
| 223 |
+
"input_shapes": [[1, 1, 25600]], "input_types": ["float32"],
|
| 224 |
+
"dynamic_shapes": {"audio": {0: "batch", 2: "samples"}}}
|
|
|
|
| 225 |
|
| 226 |
|
| 227 |
def get_acoustic_encoder_dummy_inputs(model=None):
|
| 228 |
+
import torch # 25600 = 8 * 3200 hop → 8 frames (3200-aligned trace sample)
|
| 229 |
+
return {"audio": torch.randn(1, 1, 25600, dtype=torch.float32)}
|
| 230 |
|
| 231 |
|
| 232 |
def get_acoustic_decoder_model(model_path=None):
|
|
|
|
| 293 |
|
| 294 |
# =============================================================================
|
| 295 |
# ASR-HF acoustic encoder — transformers-NATIVE (VibeVoiceAcousticTokenizerEncoderModel),
|
| 296 |
+
# different arch than 1.5B (conv_layers). No vibevoice/ shim needed. Loads only the
|
| 297 |
# `acoustic_tokenizer_encoder.*` weights (not the 7B LLM) so it fits in memory.
|
| 298 |
# =============================================================================
|
| 299 |
|
|
|
|
| 436 |
|
| 437 |
|
| 438 |
# =============================================================================
|
| 439 |
+
# Realtime-0.5B (`vibevoice_streaming`, auto_map null → vibevoice/) — a streaming TTS
|
| 440 |
# checkpoint. Key groups: model.tts_language_model.* (Qwen2.5-0.5B backbone, 20 layers,
|
| 441 |
# no lm_head), model.acoustic_tokenizer.* (DECODER-ONLY, 276 — no encoder shipped, since
|
| 442 |
# inference only DECODES generated latents → audio), model.language_model.* (4-layer base),
|
| 443 |
# model.prediction_head.* (diffusion), model.acoustic_connector.*, tts_eos_classifier.*.
|
| 444 |
+
# The acoustic decoder matches the vendored vibevoice/ class EXACTLY (stages/head naming; decoder 0
|
| 445 |
# missing / 0 unexpected) — NOT transformers-native (conv_layers/convtr naming).
|
| 446 |
# =============================================================================
|
| 447 |
RT_TTS_LM_PREFIX = "model.tts_language_model."
|
|
|
|
| 490 |
|
| 491 |
|
| 492 |
def _load_realtime_acoustic_decoder(model_path):
|
| 493 |
+
"""Realtime acoustic tokenizer (DECODER-ONLY) via the vendored vibevoice/ source. Loads only `decoder.*` weights
|
| 494 |
+
(encoder absent from the checkpoint), drops the encoder module. the vendored source matches exactly."""
|
| 495 |
import glob
|
| 496 |
from safetensors.torch import load_file
|
| 497 |
tok, ACfg = _codes_tokenizer()
|
|
|
|
| 533 |
|
| 534 |
|
| 535 |
# =============================================================================
|
| 536 |
+
# Diffusion prediction_head + speech connectors (shared 1.5B / Realtime, via the vendored vibevoice/ source).
|
| 537 |
# diffusion_head: ONE denoise step (noisy_images[B,64], timesteps[B], condition[B,H]) → pred[B,64].
|
| 538 |
# The ~20-step DDPM sampling loop stays in the pipeline; ONNX = one step.
|
| 539 |
# connector: SpeechConnector fc1(in→H) → RMSNorm(H) → fc2(H→H). 1.5B: acoustic 64→1536,
|
|
|
|
| 648 |
|
| 649 |
|
| 650 |
# =============================================================================
|
| 651 |
+
# VibeVoice-ASR (`vibevoice`, VibeVoiceForASRTraining, vendored vibevoice/) — an audio→text ASR model:
|
| 652 |
+
# same vibevoice/ family as 1.5B TTS but the LLM is Qwen2.5-7B WITH lm_head (generates text) and
|
| 653 |
# there is NO prediction_head (no audio generation). Front-end = full acoustic tokenizer (552,
|
| 654 |
# enc+dec) + semantic tokenizer (276, ENCODE-only) + acoustic/semantic connectors — all load via
|
| 655 |
+
# the existing vibevoice/ loaders (`_load_acoustic`, `_load_connector`, `_load_semantic`).
|
| 656 |
# Weight layout: model.language_model.* (338) + top-level lm_head.weight (unlike ASR-HF).
|
| 657 |
# =============================================================================
|
| 658 |
|
| 659 |
def _load_semantic(model_path):
|
| 660 |
+
"""Semantic tokenizer (ENCODE-only, deterministic latent = encode().mean) via the vendored vibevoice/ source."""
|
| 661 |
import glob
|
| 662 |
from safetensors.torch import load_file
|
| 663 |
tok = _codes_import("modular_vibevoice_tokenizer")
|
vibevoice/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# vibevoice/__init__.py
|
| 2 |
+
from vibevoice.modular import (
|
| 3 |
+
VibeVoiceStreamingForConditionalGenerationInference,
|
| 4 |
+
VibeVoiceStreamingConfig,
|
| 5 |
+
)
|
| 6 |
+
from vibevoice.processor import (
|
| 7 |
+
VibeVoiceStreamingProcessor,
|
| 8 |
+
VibeVoiceTokenizerProcessor,
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
__all__ = [
|
| 12 |
+
"VibeVoiceStreamingForConditionalGenerationInference",
|
| 13 |
+
"VibeVoiceStreamingConfig",
|
| 14 |
+
"VibeVoiceStreamingProcessor",
|
| 15 |
+
"VibeVoiceTokenizerProcessor",
|
| 16 |
+
]
|
vibevoice/configs/qwen2.5_1.5b_64k.json
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_attn_implementation_autoset": true,
|
| 3 |
+
"acoustic_vae_dim": 64,
|
| 4 |
+
"acoustic_tokenizer_config": {
|
| 5 |
+
"causal": true,
|
| 6 |
+
"channels": 1,
|
| 7 |
+
"conv_bias": true,
|
| 8 |
+
"conv_norm": "none",
|
| 9 |
+
"corpus_normalize": 0.0,
|
| 10 |
+
"decoder_depths": null,
|
| 11 |
+
"decoder_n_filters": 32,
|
| 12 |
+
"decoder_ratios": [
|
| 13 |
+
8,
|
| 14 |
+
5,
|
| 15 |
+
5,
|
| 16 |
+
4,
|
| 17 |
+
2,
|
| 18 |
+
2
|
| 19 |
+
],
|
| 20 |
+
"disable_last_norm": true,
|
| 21 |
+
"encoder_depths": "3-3-3-3-3-3-8",
|
| 22 |
+
"encoder_n_filters": 32,
|
| 23 |
+
"encoder_ratios": [
|
| 24 |
+
8,
|
| 25 |
+
5,
|
| 26 |
+
5,
|
| 27 |
+
4,
|
| 28 |
+
2,
|
| 29 |
+
2
|
| 30 |
+
],
|
| 31 |
+
"fix_std": 0.5,
|
| 32 |
+
"layer_scale_init_value": 1e-06,
|
| 33 |
+
"layernorm": "RMSNorm",
|
| 34 |
+
"layernorm_elementwise_affine": true,
|
| 35 |
+
"layernorm_eps": 1e-05,
|
| 36 |
+
"mixer_layer": "depthwise_conv",
|
| 37 |
+
"model_type": "vibepod_acoustic_tokenizer",
|
| 38 |
+
"pad_mode": "constant",
|
| 39 |
+
"std_dist_type": "gaussian",
|
| 40 |
+
"vae_dim": 64,
|
| 41 |
+
"weight_init_value": 0.01
|
| 42 |
+
},
|
| 43 |
+
"decoder_config": {
|
| 44 |
+
"attention_dropout": 0.0,
|
| 45 |
+
"hidden_act": "silu",
|
| 46 |
+
"hidden_size": 1536,
|
| 47 |
+
"initializer_range": 0.02,
|
| 48 |
+
"intermediate_size": 8960,
|
| 49 |
+
"max_position_embeddings": 65536,
|
| 50 |
+
"max_window_layers": 28,
|
| 51 |
+
"model_type": "qwen2",
|
| 52 |
+
"num_attention_heads": 12,
|
| 53 |
+
"num_hidden_layers": 28,
|
| 54 |
+
"num_key_value_heads": 2,
|
| 55 |
+
"rms_norm_eps": 1e-06,
|
| 56 |
+
"rope_scaling": null,
|
| 57 |
+
"rope_theta": 1000000.0,
|
| 58 |
+
"sliding_window": null,
|
| 59 |
+
"tie_word_embeddings": true,
|
| 60 |
+
"torch_dtype": "bfloat16",
|
| 61 |
+
"use_cache": true,
|
| 62 |
+
"use_sliding_window": false,
|
| 63 |
+
"vocab_size": 151936
|
| 64 |
+
},
|
| 65 |
+
"diffusion_head_config": {
|
| 66 |
+
"ddpm_batch_mul": 4,
|
| 67 |
+
"ddpm_beta_schedule": "cosine",
|
| 68 |
+
"ddpm_num_inference_steps": 20,
|
| 69 |
+
"ddpm_num_steps": 1000,
|
| 70 |
+
"diffusion_type": "ddpm",
|
| 71 |
+
"head_ffn_ratio": 3.0,
|
| 72 |
+
"head_layers": 4,
|
| 73 |
+
"hidden_size": 1536,
|
| 74 |
+
"latent_size": 64,
|
| 75 |
+
"model_type": "vibepod_diffusion_head",
|
| 76 |
+
"prediction_type": "v_prediction",
|
| 77 |
+
"rms_norm_eps": 1e-05,
|
| 78 |
+
"speech_vae_dim": 64
|
| 79 |
+
},
|
| 80 |
+
"model_type": "vibepod",
|
| 81 |
+
"semantic_tokenizer_config": {
|
| 82 |
+
"causal": true,
|
| 83 |
+
"channels": 1,
|
| 84 |
+
"conv_bias": true,
|
| 85 |
+
"conv_norm": "none",
|
| 86 |
+
"corpus_normalize": 0.0,
|
| 87 |
+
"disable_last_norm": true,
|
| 88 |
+
"encoder_depths": "3-3-3-3-3-3-8",
|
| 89 |
+
"encoder_n_filters": 32,
|
| 90 |
+
"encoder_ratios": [
|
| 91 |
+
8,
|
| 92 |
+
5,
|
| 93 |
+
5,
|
| 94 |
+
4,
|
| 95 |
+
2,
|
| 96 |
+
2
|
| 97 |
+
],
|
| 98 |
+
"fix_std": 0,
|
| 99 |
+
"layer_scale_init_value": 1e-06,
|
| 100 |
+
"layernorm": "RMSNorm",
|
| 101 |
+
"layernorm_elementwise_affine": true,
|
| 102 |
+
"layernorm_eps": 1e-05,
|
| 103 |
+
"mixer_layer": "depthwise_conv",
|
| 104 |
+
"model_type": "vibepod_semantic_tokenizer",
|
| 105 |
+
"pad_mode": "constant",
|
| 106 |
+
"std_dist_type": "none",
|
| 107 |
+
"vae_dim": 128,
|
| 108 |
+
"weight_init_value": 0.01
|
| 109 |
+
},
|
| 110 |
+
"semantic_vae_dim": 128,
|
| 111 |
+
"torch_dtype": "bfloat16"
|
| 112 |
+
}
|
vibevoice/configs/qwen2.5_7b_32k.json
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_attn_implementation_autoset": true,
|
| 3 |
+
"acoustic_vae_dim": 64,
|
| 4 |
+
"acoustic_tokenizer_config": {
|
| 5 |
+
"causal": true,
|
| 6 |
+
"channels": 1,
|
| 7 |
+
"conv_bias": true,
|
| 8 |
+
"conv_norm": "none",
|
| 9 |
+
"corpus_normalize": 0.0,
|
| 10 |
+
"decoder_depths": null,
|
| 11 |
+
"decoder_n_filters": 32,
|
| 12 |
+
"decoder_ratios": [
|
| 13 |
+
8,
|
| 14 |
+
5,
|
| 15 |
+
5,
|
| 16 |
+
4,
|
| 17 |
+
2,
|
| 18 |
+
2
|
| 19 |
+
],
|
| 20 |
+
"disable_last_norm": true,
|
| 21 |
+
"encoder_depths": "3-3-3-3-3-3-8",
|
| 22 |
+
"encoder_n_filters": 32,
|
| 23 |
+
"encoder_ratios": [
|
| 24 |
+
8,
|
| 25 |
+
5,
|
| 26 |
+
5,
|
| 27 |
+
4,
|
| 28 |
+
2,
|
| 29 |
+
2
|
| 30 |
+
],
|
| 31 |
+
"fix_std": 0.5,
|
| 32 |
+
"layer_scale_init_value": 1e-06,
|
| 33 |
+
"layernorm": "RMSNorm",
|
| 34 |
+
"layernorm_elementwise_affine": true,
|
| 35 |
+
"layernorm_eps": 1e-05,
|
| 36 |
+
"mixer_layer": "depthwise_conv",
|
| 37 |
+
"model_type": "vibepod_acoustic_tokenizer",
|
| 38 |
+
"pad_mode": "constant",
|
| 39 |
+
"std_dist_type": "gaussian",
|
| 40 |
+
"vae_dim": 64,
|
| 41 |
+
"weight_init_value": 0.01
|
| 42 |
+
},
|
| 43 |
+
"decoder_config": {
|
| 44 |
+
"attention_dropout": 0.0,
|
| 45 |
+
"hidden_act": "silu",
|
| 46 |
+
"hidden_size": 3584,
|
| 47 |
+
"initializer_range": 0.02,
|
| 48 |
+
"intermediate_size": 18944,
|
| 49 |
+
"max_position_embeddings": 32768,
|
| 50 |
+
"max_window_layers": 28,
|
| 51 |
+
"model_type": "qwen2",
|
| 52 |
+
"num_attention_heads": 28,
|
| 53 |
+
"num_hidden_layers": 28,
|
| 54 |
+
"num_key_value_heads": 4,
|
| 55 |
+
"rms_norm_eps": 1e-06,
|
| 56 |
+
"rope_theta": 1000000.0,
|
| 57 |
+
"sliding_window": null,
|
| 58 |
+
"tie_word_embeddings": false,
|
| 59 |
+
"torch_dtype": "bfloat16",
|
| 60 |
+
"transformers_version": "4.40.1",
|
| 61 |
+
"use_cache": true,
|
| 62 |
+
"use_mrope": false,
|
| 63 |
+
"use_sliding_window": false,
|
| 64 |
+
"vocab_size": 152064
|
| 65 |
+
},
|
| 66 |
+
"diffusion_head_config": {
|
| 67 |
+
"ddpm_batch_mul": 4,
|
| 68 |
+
"ddpm_beta_schedule": "cosine",
|
| 69 |
+
"ddpm_num_inference_steps": 20,
|
| 70 |
+
"ddpm_num_steps": 1000,
|
| 71 |
+
"diffusion_type": "ddpm",
|
| 72 |
+
"head_ffn_ratio": 3.0,
|
| 73 |
+
"head_layers": 4,
|
| 74 |
+
"hidden_size": 3584,
|
| 75 |
+
"latent_size": 64,
|
| 76 |
+
"model_type": "vibepod_diffusion_head",
|
| 77 |
+
"prediction_type": "v_prediction",
|
| 78 |
+
"rms_norm_eps": 1e-05,
|
| 79 |
+
"speech_vae_dim": 64
|
| 80 |
+
},
|
| 81 |
+
"model_type": "vibepod",
|
| 82 |
+
"semantic_tokenizer_config": {
|
| 83 |
+
"causal": true,
|
| 84 |
+
"channels": 1,
|
| 85 |
+
"conv_bias": true,
|
| 86 |
+
"conv_norm": "none",
|
| 87 |
+
"corpus_normalize": 0.0,
|
| 88 |
+
"disable_last_norm": true,
|
| 89 |
+
"encoder_depths": "3-3-3-3-3-3-8",
|
| 90 |
+
"encoder_n_filters": 32,
|
| 91 |
+
"encoder_ratios": [
|
| 92 |
+
8,
|
| 93 |
+
5,
|
| 94 |
+
5,
|
| 95 |
+
4,
|
| 96 |
+
2,
|
| 97 |
+
2
|
| 98 |
+
],
|
| 99 |
+
"fix_std": 0,
|
| 100 |
+
"layer_scale_init_value": 1e-06,
|
| 101 |
+
"layernorm": "RMSNorm",
|
| 102 |
+
"layernorm_elementwise_affine": true,
|
| 103 |
+
"layernorm_eps": 1e-05,
|
| 104 |
+
"mixer_layer": "depthwise_conv",
|
| 105 |
+
"model_type": "vibepod_semantic_tokenizer",
|
| 106 |
+
"pad_mode": "constant",
|
| 107 |
+
"std_dist_type": "none",
|
| 108 |
+
"vae_dim": 128,
|
| 109 |
+
"weight_init_value": 0.01
|
| 110 |
+
},
|
| 111 |
+
"semantic_vae_dim": 128,
|
| 112 |
+
"torch_dtype": "bfloat16"
|
| 113 |
+
}
|
vibevoice/modular/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# vibevoice/modular/__init__.py
|
| 2 |
+
from .modeling_vibevoice_streaming_inference import VibeVoiceStreamingForConditionalGenerationInference
|
| 3 |
+
from .configuration_vibevoice_streaming import VibeVoiceStreamingConfig
|
| 4 |
+
from .modeling_vibevoice_streaming import VibeVoiceStreamingModel, VibeVoiceStreamingPreTrainedModel
|
| 5 |
+
from .streamer import AudioStreamer, AsyncAudioStreamer
|
| 6 |
+
|
| 7 |
+
__all__ = [
|
| 8 |
+
"VibeVoiceStreamingForConditionalGenerationInference",
|
| 9 |
+
"VibeVoiceStreamingConfig",
|
| 10 |
+
"VibeVoiceStreamingModel",
|
| 11 |
+
"VibeVoiceStreamingPreTrainedModel",
|
| 12 |
+
"AudioStreamer",
|
| 13 |
+
"AsyncAudioStreamer",
|
| 14 |
+
]
|
vibevoice/modular/configuration_vibevoice.py
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
""" VibeVoice_AcousticTokenizer model configuration"""
|
| 2 |
+
|
| 3 |
+
from typing import Dict, List, Optional, Tuple
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
from transformers.configuration_utils import PretrainedConfig
|
| 7 |
+
from transformers.utils import logging
|
| 8 |
+
|
| 9 |
+
from transformers.models.qwen2.configuration_qwen2 import Qwen2Config
|
| 10 |
+
|
| 11 |
+
logger = logging.get_logger(__name__)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def _convert_dtype_to_string(config_dict: dict) -> dict:
|
| 15 |
+
"""
|
| 16 |
+
Convert torch.dtype objects to their string representation for JSON serialization.
|
| 17 |
+
|
| 18 |
+
This fixes the "Object of type dtype is not JSON serializable" error that occurs
|
| 19 |
+
when transformers tries to log/serialize the config with torch_dtype as a torch.dtype object.
|
| 20 |
+
|
| 21 |
+
See: https://github.com/microsoft/VibeVoice/issues/199
|
| 22 |
+
"""
|
| 23 |
+
if "torch_dtype" in config_dict and config_dict["torch_dtype"] is not None:
|
| 24 |
+
dtype = config_dict["torch_dtype"]
|
| 25 |
+
if isinstance(dtype, torch.dtype):
|
| 26 |
+
# Convert torch.dtype to string (e.g., torch.bfloat16 -> "bfloat16")
|
| 27 |
+
config_dict["torch_dtype"] = str(dtype).replace("torch.", "")
|
| 28 |
+
return config_dict
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class VibeVoiceAcousticTokenizerConfig(PretrainedConfig):
|
| 32 |
+
model_type = "vibevoice_acoustic_tokenizer"
|
| 33 |
+
|
| 34 |
+
def __init__(
|
| 35 |
+
self,
|
| 36 |
+
channels: int = 1,
|
| 37 |
+
corpus_normalize: float = 0.0,
|
| 38 |
+
causal: bool = True,
|
| 39 |
+
vae_dim: int = 64,
|
| 40 |
+
fix_std: float = 0.5,
|
| 41 |
+
std_dist_type: str = 'gaussian',
|
| 42 |
+
# common
|
| 43 |
+
mixer_layer: str = 'depthwise_conv',
|
| 44 |
+
conv_norm: str = 'none',
|
| 45 |
+
pad_mode: str = 'constant',
|
| 46 |
+
disable_last_norm: bool = True,
|
| 47 |
+
layernorm: str = 'RMSNorm',
|
| 48 |
+
layernorm_eps: float = 1e-5,
|
| 49 |
+
layernorm_elementwise_affine: bool = True,
|
| 50 |
+
conv_bias: bool = True,
|
| 51 |
+
layer_scale_init_value: float = 1e-6,
|
| 52 |
+
weight_init_value: float = 1e-2,
|
| 53 |
+
# encoder specific
|
| 54 |
+
encoder_n_filters: int = 32,
|
| 55 |
+
encoder_ratios: Optional[List[int]] = [8,5,5,4,2,2],
|
| 56 |
+
encoder_depths: str = "3-3-3-3-3-3-8",
|
| 57 |
+
# decoder specific
|
| 58 |
+
decoder_n_filters: int = 32,
|
| 59 |
+
decoder_ratios: Optional[List[int]] = None, # if None, same as encoder
|
| 60 |
+
decoder_depths: Optional[str] = None,
|
| 61 |
+
**kwargs
|
| 62 |
+
):
|
| 63 |
+
super().__init__(**kwargs)
|
| 64 |
+
self.channels = channels
|
| 65 |
+
self.corpus_normalize = corpus_normalize
|
| 66 |
+
self.causal = causal
|
| 67 |
+
self.vae_dim = vae_dim
|
| 68 |
+
self.fix_std = fix_std
|
| 69 |
+
self.std_dist_type = std_dist_type
|
| 70 |
+
|
| 71 |
+
# common parameters
|
| 72 |
+
self.conv_norm = conv_norm
|
| 73 |
+
self.pad_mode = pad_mode
|
| 74 |
+
self.layernorm_eps = layernorm_eps
|
| 75 |
+
self.disable_last_norm = disable_last_norm
|
| 76 |
+
self.layernorm = layernorm
|
| 77 |
+
self.layernorm_elementwise_affine = layernorm_elementwise_affine
|
| 78 |
+
self.conv_bias = conv_bias
|
| 79 |
+
self.layer_scale_init_value = layer_scale_init_value
|
| 80 |
+
self.weight_init_value = weight_init_value
|
| 81 |
+
self.mixer_layer = mixer_layer
|
| 82 |
+
|
| 83 |
+
# encoder specific parameters
|
| 84 |
+
self.encoder_n_filters = encoder_n_filters
|
| 85 |
+
self.encoder_ratios = encoder_ratios
|
| 86 |
+
self.encoder_depths = encoder_depths
|
| 87 |
+
|
| 88 |
+
# decoder specific parameters
|
| 89 |
+
self.decoder_ratios = decoder_ratios if decoder_ratios is not None else encoder_ratios
|
| 90 |
+
self.decoder_n_filters = decoder_n_filters
|
| 91 |
+
self.decoder_depths = decoder_depths
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
class VibeVoiceSemanticTokenizerConfig(PretrainedConfig):
|
| 95 |
+
model_type = "vibevoice_semantic_tokenizer"
|
| 96 |
+
|
| 97 |
+
def __init__(
|
| 98 |
+
self,
|
| 99 |
+
channels: int = 1,
|
| 100 |
+
corpus_normalize: float = 0.0,
|
| 101 |
+
causal: bool = True,
|
| 102 |
+
vae_dim: int = 64,
|
| 103 |
+
fix_std: float = 0,
|
| 104 |
+
std_dist_type: str = 'none',
|
| 105 |
+
# common
|
| 106 |
+
mixer_layer: str = 'depthwise_conv',
|
| 107 |
+
conv_norm: str = 'none',
|
| 108 |
+
pad_mode: str = 'constant',
|
| 109 |
+
disable_last_norm: bool = True,
|
| 110 |
+
layernorm: str = 'RMSNorm',
|
| 111 |
+
layernorm_eps: float = 1e-5,
|
| 112 |
+
layernorm_elementwise_affine: bool = True,
|
| 113 |
+
conv_bias: bool = True,
|
| 114 |
+
layer_scale_init_value: float = 1e-6,
|
| 115 |
+
weight_init_value: float = 1e-2,
|
| 116 |
+
# encoder specific
|
| 117 |
+
encoder_n_filters: int = 32,
|
| 118 |
+
encoder_ratios: Optional[List[int]] = [8,5,5,4,2,2],
|
| 119 |
+
encoder_depths: str = "3-3-3-3-3-3-8",
|
| 120 |
+
**kwargs
|
| 121 |
+
):
|
| 122 |
+
super().__init__(**kwargs)
|
| 123 |
+
self.channels = channels
|
| 124 |
+
self.corpus_normalize = corpus_normalize
|
| 125 |
+
self.causal = causal
|
| 126 |
+
self.vae_dim = vae_dim
|
| 127 |
+
self.fix_std = fix_std
|
| 128 |
+
self.std_dist_type = std_dist_type
|
| 129 |
+
|
| 130 |
+
# common parameters
|
| 131 |
+
self.conv_norm = conv_norm
|
| 132 |
+
self.pad_mode = pad_mode
|
| 133 |
+
self.layernorm_eps = layernorm_eps
|
| 134 |
+
self.disable_last_norm = disable_last_norm
|
| 135 |
+
self.layernorm = layernorm
|
| 136 |
+
self.layernorm_elementwise_affine = layernorm_elementwise_affine
|
| 137 |
+
self.conv_bias = conv_bias
|
| 138 |
+
self.layer_scale_init_value = layer_scale_init_value
|
| 139 |
+
self.weight_init_value = weight_init_value
|
| 140 |
+
self.mixer_layer = mixer_layer
|
| 141 |
+
|
| 142 |
+
# encoder specific parameters
|
| 143 |
+
self.encoder_n_filters = encoder_n_filters
|
| 144 |
+
self.encoder_ratios = encoder_ratios
|
| 145 |
+
self.encoder_depths = encoder_depths
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
class VibeVoiceDiffusionHeadConfig(PretrainedConfig):
|
| 149 |
+
model_type = "vibevoice_diffusion_head"
|
| 150 |
+
|
| 151 |
+
def __init__(
|
| 152 |
+
self,
|
| 153 |
+
hidden_size=768,
|
| 154 |
+
head_layers=4,
|
| 155 |
+
head_ffn_ratio=3.0,
|
| 156 |
+
rms_norm_eps=1e-5,
|
| 157 |
+
latent_size=64,
|
| 158 |
+
speech_vae_dim=None,
|
| 159 |
+
prediction_type="v_prediction",
|
| 160 |
+
diffusion_type="ddpm",
|
| 161 |
+
ddpm_num_steps=1000,
|
| 162 |
+
ddpm_num_inference_steps=20,
|
| 163 |
+
ddpm_beta_schedule="cosine",
|
| 164 |
+
ddpm_batch_mul=4,
|
| 165 |
+
**kwargs
|
| 166 |
+
):
|
| 167 |
+
self.hidden_size = hidden_size
|
| 168 |
+
self.head_layers = head_layers
|
| 169 |
+
self.head_ffn_ratio = head_ffn_ratio
|
| 170 |
+
self.rms_norm_eps = rms_norm_eps
|
| 171 |
+
self.latent_size = latent_size
|
| 172 |
+
self.speech_vae_dim = speech_vae_dim
|
| 173 |
+
self.prediction_type = prediction_type
|
| 174 |
+
self.diffusion_type = diffusion_type
|
| 175 |
+
self.ddpm_num_steps = ddpm_num_steps
|
| 176 |
+
self.ddpm_num_inference_steps = ddpm_num_inference_steps
|
| 177 |
+
self.ddpm_beta_schedule = ddpm_beta_schedule
|
| 178 |
+
self.ddpm_batch_mul = ddpm_batch_mul
|
| 179 |
+
|
| 180 |
+
super().__init__(**kwargs)
|
| 181 |
+
|
| 182 |
+
class VibeVoiceConfig(PretrainedConfig):
|
| 183 |
+
model_type = "vibevoice"
|
| 184 |
+
is_composition = True
|
| 185 |
+
sub_configs = {
|
| 186 |
+
"acoustic_tokenizer_config": VibeVoiceAcousticTokenizerConfig,
|
| 187 |
+
"semantic_tokenizer_config": VibeVoiceSemanticTokenizerConfig,
|
| 188 |
+
"decoder_config": Qwen2Config,
|
| 189 |
+
"diffusion_head_config": VibeVoiceDiffusionHeadConfig,
|
| 190 |
+
}
|
| 191 |
+
# keys_to_ignore_at_inference = ["past_key_values"]
|
| 192 |
+
# Default tensor parallel plan for base model `Qwen2`
|
| 193 |
+
base_model_tp_plan = {
|
| 194 |
+
"layers.*.self_attn.q_proj": "colwise",
|
| 195 |
+
"layers.*.self_attn.k_proj": "colwise",
|
| 196 |
+
"layers.*.self_attn.v_proj": "colwise",
|
| 197 |
+
"layers.*.self_attn.o_proj": "rowwise",
|
| 198 |
+
"layers.*.mlp.gate_proj": "colwise",
|
| 199 |
+
"layers.*.mlp.up_proj": "colwise",
|
| 200 |
+
"layers.*.mlp.down_proj": "rowwise",
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
def __init__(
|
| 204 |
+
self,
|
| 205 |
+
acoustic_tokenizer_config=None,
|
| 206 |
+
semantic_tokenizer_config=None,
|
| 207 |
+
decoder_config=None,
|
| 208 |
+
diffusion_head_config=None,
|
| 209 |
+
**kwargs
|
| 210 |
+
):
|
| 211 |
+
|
| 212 |
+
# kwargs["_attn_implementation"] = "flash_attention_2"
|
| 213 |
+
kwargs["_attn_implementation_autoset"] = False
|
| 214 |
+
|
| 215 |
+
if acoustic_tokenizer_config is None:
|
| 216 |
+
self.acoustic_tokenizer_config = self.sub_configs["acoustic_tokenizer_config"]()
|
| 217 |
+
elif isinstance(acoustic_tokenizer_config, dict):
|
| 218 |
+
acoustic_tokenizer_config["model_type"] = "vibevoice_acoustic_tokenizer"
|
| 219 |
+
self.acoustic_tokenizer_config = self.sub_configs["acoustic_tokenizer_config"](**acoustic_tokenizer_config)
|
| 220 |
+
elif isinstance(acoustic_tokenizer_config, VibeVoiceAcousticTokenizerConfig):
|
| 221 |
+
# If an instance of the config class is provided
|
| 222 |
+
self.acoustic_tokenizer_config = acoustic_tokenizer_config
|
| 223 |
+
|
| 224 |
+
if semantic_tokenizer_config is None:
|
| 225 |
+
self.semantic_tokenizer_config = self.sub_configs["semantic_tokenizer_config"]()
|
| 226 |
+
elif isinstance(semantic_tokenizer_config, dict):
|
| 227 |
+
semantic_tokenizer_config["model_type"] = "vibevoice_semantic_tokenizer"
|
| 228 |
+
self.semantic_tokenizer_config = self.sub_configs["semantic_tokenizer_config"](**semantic_tokenizer_config)
|
| 229 |
+
elif isinstance(semantic_tokenizer_config, VibeVoiceSemanticTokenizerConfig):
|
| 230 |
+
# If an instance of the config class is provided
|
| 231 |
+
self.semantic_tokenizer_config = semantic_tokenizer_config
|
| 232 |
+
|
| 233 |
+
if decoder_config is None:
|
| 234 |
+
self.decoder_config = self.sub_configs["decoder_config"]()
|
| 235 |
+
elif isinstance(decoder_config, dict):
|
| 236 |
+
# If a dictionary is provided, instantiate the config class with it
|
| 237 |
+
# self.decoder_config = self.sub_configs["decoder_config"](**decoder_config)
|
| 238 |
+
if decoder_config.get("model_type", '') == "qwen2":
|
| 239 |
+
self.decoder_config = Qwen2Config(**decoder_config)
|
| 240 |
+
else:
|
| 241 |
+
raise ValueError(f"Unsupported decoder model type: {decoder_config.get('model_type', '')}")
|
| 242 |
+
elif isinstance(decoder_config, (Qwen2Config,)):
|
| 243 |
+
# If an instance of the config class is provided
|
| 244 |
+
self.decoder_config = decoder_config
|
| 245 |
+
|
| 246 |
+
if diffusion_head_config is None:
|
| 247 |
+
self.diffusion_head_config = self.sub_configs["diffusion_head_config"]()
|
| 248 |
+
elif isinstance(diffusion_head_config, dict):
|
| 249 |
+
diffusion_head_config["model_type"] = "vibevoice_diffusion_head"
|
| 250 |
+
self.diffusion_head_config = self.sub_configs["diffusion_head_config"](**diffusion_head_config)
|
| 251 |
+
elif isinstance(diffusion_head_config, VibeVoiceDiffusionHeadConfig):
|
| 252 |
+
# If an instance of the config class is provided
|
| 253 |
+
self.diffusion_head_config = diffusion_head_config
|
| 254 |
+
|
| 255 |
+
# other parameters
|
| 256 |
+
self.acoustic_vae_dim = getattr(self.acoustic_tokenizer_config, 'vae_dim', 64)
|
| 257 |
+
self.semantic_vae_dim = getattr(self.semantic_tokenizer_config, 'vae_dim', 128)
|
| 258 |
+
|
| 259 |
+
super().__init__(**kwargs)
|
| 260 |
+
|
| 261 |
+
def get_text_config(self, decoder=False):
|
| 262 |
+
"""
|
| 263 |
+
Returns the text config for this model.
|
| 264 |
+
|
| 265 |
+
vLLM uses this method to get the text configuration from multimodal models.
|
| 266 |
+
This allows vLLM to correctly determine hidden_size, num_attention_heads,
|
| 267 |
+
and other properties needed for memory profiling and model execution.
|
| 268 |
+
|
| 269 |
+
For VibeVoice, the "text config" is the decoder_config (Qwen2Config).
|
| 270 |
+
|
| 271 |
+
Args:
|
| 272 |
+
decoder: If True, return the decoder config (for encoder-decoder models).
|
| 273 |
+
For VibeVoice, this is always the decoder_config.
|
| 274 |
+
|
| 275 |
+
Returns:
|
| 276 |
+
The decoder configuration (Qwen2Config) which contains hidden_size, etc.
|
| 277 |
+
"""
|
| 278 |
+
return self.decoder_config
|
| 279 |
+
|
| 280 |
+
def to_dict(self):
|
| 281 |
+
"""
|
| 282 |
+
Override to_dict to handle torch.dtype serialization.
|
| 283 |
+
|
| 284 |
+
Fixes: https://github.com/microsoft/VibeVoice/issues/199
|
| 285 |
+
"""
|
| 286 |
+
output = super().to_dict()
|
| 287 |
+
return _convert_dtype_to_string(output)
|
| 288 |
+
|
| 289 |
+
class VibeVoiceASRConfig(PretrainedConfig):
|
| 290 |
+
model_type = "vibevoice"
|
| 291 |
+
is_composition = True
|
| 292 |
+
sub_configs = {
|
| 293 |
+
"acoustic_tokenizer_config": VibeVoiceAcousticTokenizerConfig,
|
| 294 |
+
"semantic_tokenizer_config": VibeVoiceSemanticTokenizerConfig,
|
| 295 |
+
"decoder_config": Qwen2Config,
|
| 296 |
+
}
|
| 297 |
+
# keys_to_ignore_at_inference = ["past_key_values"]
|
| 298 |
+
# Default tensor parallel plan for base model `Qwen2`
|
| 299 |
+
base_model_tp_plan = {
|
| 300 |
+
"layers.*.self_attn.q_proj": "colwise",
|
| 301 |
+
"layers.*.self_attn.k_proj": "colwise",
|
| 302 |
+
"layers.*.self_attn.v_proj": "colwise",
|
| 303 |
+
"layers.*.self_attn.o_proj": "rowwise",
|
| 304 |
+
"layers.*.mlp.gate_proj": "colwise",
|
| 305 |
+
"layers.*.mlp.up_proj": "colwise",
|
| 306 |
+
"layers.*.mlp.down_proj": "rowwise",
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
def __init__(
|
| 310 |
+
self,
|
| 311 |
+
acoustic_tokenizer_config=None,
|
| 312 |
+
semantic_tokenizer_config=None,
|
| 313 |
+
decoder_config=None,
|
| 314 |
+
**kwargs
|
| 315 |
+
):
|
| 316 |
+
|
| 317 |
+
# kwargs["_attn_implementation"] = "flash_attention_2"
|
| 318 |
+
kwargs["_attn_implementation_autoset"] = False
|
| 319 |
+
|
| 320 |
+
if acoustic_tokenizer_config is None:
|
| 321 |
+
self.acoustic_tokenizer_config = self.sub_configs["acoustic_tokenizer_config"]()
|
| 322 |
+
elif isinstance(acoustic_tokenizer_config, dict):
|
| 323 |
+
acoustic_tokenizer_config["model_type"] = "vibevoice_acoustic_tokenizer"
|
| 324 |
+
self.acoustic_tokenizer_config = self.sub_configs["acoustic_tokenizer_config"](**acoustic_tokenizer_config)
|
| 325 |
+
elif isinstance(acoustic_tokenizer_config, VibeVoiceAcousticTokenizerConfig):
|
| 326 |
+
# If an instance of the config class is provided
|
| 327 |
+
self.acoustic_tokenizer_config = acoustic_tokenizer_config
|
| 328 |
+
|
| 329 |
+
if semantic_tokenizer_config is None:
|
| 330 |
+
self.semantic_tokenizer_config = self.sub_configs["semantic_tokenizer_config"]()
|
| 331 |
+
elif isinstance(semantic_tokenizer_config, dict):
|
| 332 |
+
semantic_tokenizer_config["model_type"] = "vibevoice_semantic_tokenizer"
|
| 333 |
+
self.semantic_tokenizer_config = self.sub_configs["semantic_tokenizer_config"](**semantic_tokenizer_config)
|
| 334 |
+
elif isinstance(semantic_tokenizer_config, VibeVoiceSemanticTokenizerConfig):
|
| 335 |
+
# If an instance of the config class is provided
|
| 336 |
+
self.semantic_tokenizer_config = semantic_tokenizer_config
|
| 337 |
+
|
| 338 |
+
if decoder_config is None:
|
| 339 |
+
self.decoder_config = self.sub_configs["decoder_config"]()
|
| 340 |
+
elif isinstance(decoder_config, dict):
|
| 341 |
+
# If a dictionary is provided, instantiate the config class with it
|
| 342 |
+
# self.decoder_config = self.sub_configs["decoder_config"](**decoder_config)
|
| 343 |
+
if decoder_config.get("model_type", '') == "qwen2":
|
| 344 |
+
self.decoder_config = Qwen2Config(**decoder_config)
|
| 345 |
+
else:
|
| 346 |
+
raise ValueError(f"Unsupported decoder model type: {decoder_config.get('model_type', '')}")
|
| 347 |
+
elif isinstance(decoder_config, Qwen2Config):
|
| 348 |
+
# If an instance of the config class is provided
|
| 349 |
+
self.decoder_config = decoder_config
|
| 350 |
+
|
| 351 |
+
# other parameters
|
| 352 |
+
self.acoustic_vae_dim = getattr(self.acoustic_tokenizer_config, 'vae_dim', 64)
|
| 353 |
+
self.semantic_vae_dim = getattr(self.semantic_tokenizer_config, 'vae_dim', 128)
|
| 354 |
+
|
| 355 |
+
super().__init__(**kwargs)
|
| 356 |
+
|
| 357 |
+
def to_dict(self):
|
| 358 |
+
"""
|
| 359 |
+
Override to_dict to handle torch.dtype serialization.
|
| 360 |
+
|
| 361 |
+
Fixes: https://github.com/microsoft/VibeVoice/issues/199
|
| 362 |
+
"""
|
| 363 |
+
output = super().to_dict()
|
| 364 |
+
return _convert_dtype_to_string(output)
|
| 365 |
+
|
| 366 |
+
def get_text_config(self, decoder: bool = False):
|
| 367 |
+
"""Return the text (decoder) config for generation."""
|
| 368 |
+
return self.decoder_config
|
| 369 |
+
|
| 370 |
+
@property
|
| 371 |
+
def vocab_size(self):
|
| 372 |
+
"""Return vocab_size from decoder config for generation compatibility."""
|
| 373 |
+
return self.decoder_config.vocab_size
|
| 374 |
+
|
| 375 |
+
@property
|
| 376 |
+
def num_attention_heads(self):
|
| 377 |
+
"""Return num_attention_heads from decoder config for Ulysses SP compatibility."""
|
| 378 |
+
return self.decoder_config.num_attention_heads
|
| 379 |
+
|
| 380 |
+
@property
|
| 381 |
+
def num_key_value_heads(self):
|
| 382 |
+
"""Return num_key_value_heads from decoder config for Ulysses SP compatibility."""
|
| 383 |
+
return self.decoder_config.num_key_value_heads
|
| 384 |
+
|
| 385 |
+
@property
|
| 386 |
+
def hidden_size(self):
|
| 387 |
+
"""Return hidden_size from decoder config for model compatibility."""
|
| 388 |
+
return self.decoder_config.hidden_size
|
| 389 |
+
|
| 390 |
+
@property
|
| 391 |
+
def num_hidden_layers(self):
|
| 392 |
+
"""Return num_hidden_layers from decoder config for Ulysses SP compatibility."""
|
| 393 |
+
return self.decoder_config.num_hidden_layers
|
| 394 |
+
|
| 395 |
+
@property
|
| 396 |
+
def head_dim(self):
|
| 397 |
+
"""Return head_dim from decoder config for Ulysses SP compatibility."""
|
| 398 |
+
return getattr(self.decoder_config, 'head_dim', self.hidden_size // self.num_attention_heads)
|
| 399 |
+
|
| 400 |
+
__all__ = [
|
| 401 |
+
"VibeVoiceAcousticTokenizerConfig",
|
| 402 |
+
"VibeVoiceSemanticTokenizerConfig",
|
| 403 |
+
"VibeVoiceDiffusionHeadConfig",
|
| 404 |
+
"VibeVoiceConfig",
|
| 405 |
+
"VibeVoiceASRConfig"
|
| 406 |
+
]
|
vibevoice/modular/configuration_vibevoice_streaming.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
""" VibeVoice Streaming model configuration"""
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
from transformers.configuration_utils import PretrainedConfig
|
| 5 |
+
from transformers.utils import logging
|
| 6 |
+
|
| 7 |
+
from transformers.models.qwen2.configuration_qwen2 import Qwen2Config
|
| 8 |
+
|
| 9 |
+
from .configuration_vibevoice import VibeVoiceAcousticTokenizerConfig, VibeVoiceDiffusionHeadConfig, _convert_dtype_to_string
|
| 10 |
+
|
| 11 |
+
logger = logging.get_logger(__name__)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class VibeVoiceStreamingConfig(PretrainedConfig):
|
| 15 |
+
model_type = "vibevoice_streaming"
|
| 16 |
+
is_composition = True
|
| 17 |
+
sub_configs = {
|
| 18 |
+
"acoustic_tokenizer_config": VibeVoiceAcousticTokenizerConfig,
|
| 19 |
+
"decoder_config": Qwen2Config,
|
| 20 |
+
"diffusion_head_config": VibeVoiceDiffusionHeadConfig,
|
| 21 |
+
}
|
| 22 |
+
# keys_to_ignore_at_inference = ["past_key_values"]
|
| 23 |
+
# Default tensor parallel plan for base model `Qwen2`
|
| 24 |
+
base_model_tp_plan = {
|
| 25 |
+
"layers.*.self_attn.q_proj": "colwise",
|
| 26 |
+
"layers.*.self_attn.k_proj": "colwise",
|
| 27 |
+
"layers.*.self_attn.v_proj": "colwise",
|
| 28 |
+
"layers.*.self_attn.o_proj": "rowwise",
|
| 29 |
+
"layers.*.mlp.gate_proj": "colwise",
|
| 30 |
+
"layers.*.mlp.up_proj": "colwise",
|
| 31 |
+
"layers.*.mlp.down_proj": "rowwise",
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
def __init__(
|
| 35 |
+
self,
|
| 36 |
+
acoustic_tokenizer_config=None,
|
| 37 |
+
decoder_config=None,
|
| 38 |
+
diffusion_head_config=None,
|
| 39 |
+
tts_backbone_num_hidden_layers=20,
|
| 40 |
+
**kwargs
|
| 41 |
+
):
|
| 42 |
+
|
| 43 |
+
# kwargs["_attn_implementation"] = "flash_attention_2"
|
| 44 |
+
kwargs["_attn_implementation_autoset"] = False
|
| 45 |
+
|
| 46 |
+
if acoustic_tokenizer_config is None:
|
| 47 |
+
self.acoustic_tokenizer_config = self.sub_configs["acoustic_tokenizer_config"]()
|
| 48 |
+
elif isinstance(acoustic_tokenizer_config, dict):
|
| 49 |
+
acoustic_tokenizer_config["model_type"] = "vibevoice_acoustic_tokenizer"
|
| 50 |
+
self.acoustic_tokenizer_config = self.sub_configs["acoustic_tokenizer_config"](**acoustic_tokenizer_config)
|
| 51 |
+
elif isinstance(acoustic_tokenizer_config, VibeVoiceAcousticTokenizerConfig):
|
| 52 |
+
# If an instance of the config class is provided
|
| 53 |
+
self.acoustic_tokenizer_config = acoustic_tokenizer_config
|
| 54 |
+
|
| 55 |
+
if decoder_config is None:
|
| 56 |
+
self.decoder_config = self.sub_configs["decoder_config"]()
|
| 57 |
+
elif isinstance(decoder_config, dict):
|
| 58 |
+
# If a dictionary is provided, instantiate the config class with it
|
| 59 |
+
# self.decoder_config = self.sub_configs["decoder_config"](**decoder_config)
|
| 60 |
+
if decoder_config.get("model_type", '') == "qwen2":
|
| 61 |
+
self.decoder_config = Qwen2Config(**decoder_config)
|
| 62 |
+
else:
|
| 63 |
+
raise ValueError(f"Unsupported decoder model type: {decoder_config.get('model_type', '')}")
|
| 64 |
+
elif isinstance(decoder_config, (Qwen2Config,)):
|
| 65 |
+
# If an instance of the config class is provided
|
| 66 |
+
self.decoder_config = decoder_config
|
| 67 |
+
|
| 68 |
+
if diffusion_head_config is None:
|
| 69 |
+
self.diffusion_head_config = self.sub_configs["diffusion_head_config"]()
|
| 70 |
+
elif isinstance(diffusion_head_config, dict):
|
| 71 |
+
diffusion_head_config["model_type"] = "vibevoice_diffusion_head"
|
| 72 |
+
self.diffusion_head_config = self.sub_configs["diffusion_head_config"](**diffusion_head_config)
|
| 73 |
+
elif isinstance(diffusion_head_config, VibeVoiceDiffusionHeadConfig):
|
| 74 |
+
# If an instance of the config class is provided
|
| 75 |
+
self.diffusion_head_config = diffusion_head_config
|
| 76 |
+
|
| 77 |
+
# other parameters
|
| 78 |
+
self.acoustic_vae_dim = getattr(self.acoustic_tokenizer_config, 'vae_dim', 64)
|
| 79 |
+
# The decoder of the model is divided into two components. The lower Transformer layers are only used for encoding text, while the upper Transformer layers are used for encoding text and generating speech. `tts_backbone_num_hidden_layers` indicates the number of upper layers used for TTS.
|
| 80 |
+
self.tts_backbone_num_hidden_layers = tts_backbone_num_hidden_layers
|
| 81 |
+
|
| 82 |
+
super().__init__(**kwargs)
|
| 83 |
+
|
| 84 |
+
def get_text_config(self, decoder=False):
|
| 85 |
+
"""Returns the decoder config (required for transformers >= 4.57 cache compatibility)."""
|
| 86 |
+
return self.decoder_config
|
| 87 |
+
|
| 88 |
+
@property
|
| 89 |
+
def num_hidden_layers(self):
|
| 90 |
+
"""Proxy to decoder_config.num_hidden_layers (required for transformers >= 4.57)."""
|
| 91 |
+
return self.decoder_config.num_hidden_layers
|
| 92 |
+
|
| 93 |
+
def to_dict(self):
|
| 94 |
+
"""
|
| 95 |
+
Override to_dict to handle torch.dtype serialization.
|
| 96 |
+
|
| 97 |
+
Fixes: https://github.com/microsoft/VibeVoice/issues/199
|
| 98 |
+
"""
|
| 99 |
+
output = super().to_dict()
|
| 100 |
+
return _convert_dtype_to_string(output)
|
| 101 |
+
|
| 102 |
+
__all__ = [
|
| 103 |
+
"VibeVoiceStreamingConfig"
|
| 104 |
+
]
|
vibevoice/modular/modeling_vibevoice.py
ADDED
|
@@ -0,0 +1,496 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# copied from https://github.com/vibevoice-community/VibeVoice/blob/main/vibevoice/modular/modeling_vibevoice.py
|
| 2 |
+
from dataclasses import dataclass
|
| 3 |
+
from typing import Dict, List, Optional, Tuple, Union, Callable
|
| 4 |
+
from tqdm import tqdm
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn as nn
|
| 7 |
+
import torch.nn.functional as F
|
| 8 |
+
import torch.distributed as dist
|
| 9 |
+
|
| 10 |
+
from transformers.models.auto import AutoModel, AutoModelForCausalLM
|
| 11 |
+
|
| 12 |
+
from transformers.activations import ACT2FN
|
| 13 |
+
from transformers.modeling_outputs import CausalLMOutput, BaseModelOutputWithPast, ModelOutput
|
| 14 |
+
from transformers.models.llama.modeling_llama import LlamaRMSNorm
|
| 15 |
+
from transformers import modeling_utils
|
| 16 |
+
from transformers.modeling_utils import PreTrainedModel
|
| 17 |
+
from transformers.modeling_flash_attention_utils import FlashAttentionKwargs
|
| 18 |
+
from transformers.utils import logging
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
from .modular_vibevoice_tokenizer import VibeVoiceTokenizerStreamingCache, VibeVoiceAcousticTokenizerModel, VibeVoiceSemanticTokenizerModel
|
| 22 |
+
from .modular_vibevoice_diffusion_head import VibeVoiceDiffusionHead
|
| 23 |
+
from vibevoice.schedule.dpm_solver import DPMSolverMultistepScheduler
|
| 24 |
+
|
| 25 |
+
from .configuration_vibevoice import VibeVoiceConfig
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
logger = logging.get_logger(__name__)
|
| 29 |
+
|
| 30 |
+
if not hasattr(modeling_utils, "ALL_PARALLEL_STYLES") or modeling_utils.ALL_PARALLEL_STYLES is None:
|
| 31 |
+
modeling_utils.ALL_PARALLEL_STYLES = ["tp", "none", "colwise", "rowwise"]
|
| 32 |
+
|
| 33 |
+
@dataclass
|
| 34 |
+
class VibeVoiceCausalLMOutputWithPast(ModelOutput):
|
| 35 |
+
loss: Optional[torch.FloatTensor] = None
|
| 36 |
+
diffusion_loss: Optional[torch.FloatTensor] = None
|
| 37 |
+
speech_token_num: Optional[int] = None
|
| 38 |
+
logits: torch.FloatTensor = None
|
| 39 |
+
past_key_values: Optional[Tuple[Tuple[torch.FloatTensor]]] = None
|
| 40 |
+
hidden_states: Optional[Tuple[torch.FloatTensor, ...]] = None
|
| 41 |
+
attentions: Optional[Tuple[torch.FloatTensor, ...]] = None
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
@dataclass
|
| 45 |
+
class VibeVoiceGenerationOutput(ModelOutput):
|
| 46 |
+
"""
|
| 47 |
+
Output type for VibeVoice generation.
|
| 48 |
+
|
| 49 |
+
Args:
|
| 50 |
+
sequences (`torch.LongTensor` of shape `(batch_size, sequence_length)`):
|
| 51 |
+
The generated sequences.
|
| 52 |
+
speech_outputs (`List[torch.FloatTensor]`, *optional*):
|
| 53 |
+
List of generated speech waveforms or latents for each speech segment.
|
| 54 |
+
"""
|
| 55 |
+
sequences: torch.LongTensor = None
|
| 56 |
+
speech_outputs: Optional[List[torch.FloatTensor]] = None
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
class SpeechConnector(nn.Module):
|
| 60 |
+
def __init__(self, input_dim, output_dim):
|
| 61 |
+
super().__init__()
|
| 62 |
+
self.fc1 = nn.Linear(input_dim, output_dim)
|
| 63 |
+
self.norm = LlamaRMSNorm(output_dim, eps=1e-6)
|
| 64 |
+
self.fc2 = nn.Linear(output_dim, output_dim)
|
| 65 |
+
|
| 66 |
+
def forward(self, features, **kwargs):
|
| 67 |
+
x = self.fc1(features)
|
| 68 |
+
x = self.norm(x)
|
| 69 |
+
x = self.fc2(x)
|
| 70 |
+
return x
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
# @auto_docstring
|
| 74 |
+
class VibeVoicePreTrainedModel(PreTrainedModel):
|
| 75 |
+
config_class = VibeVoiceConfig
|
| 76 |
+
base_model_prefix = "model"
|
| 77 |
+
supports_gradient_checkpointing = True
|
| 78 |
+
_skip_keys_device_placement = "past_key_values"
|
| 79 |
+
_supports_cache_class = True
|
| 80 |
+
_supports_flash_attn_2 = True
|
| 81 |
+
_supports_sdpa = True
|
| 82 |
+
_supports_quantized_cache = True
|
| 83 |
+
_supports_static_cache = True
|
| 84 |
+
_supports_attention_backend = True
|
| 85 |
+
|
| 86 |
+
def _init_weights(self, module):
|
| 87 |
+
if isinstance(module, VibeVoiceDiffusionHead):
|
| 88 |
+
module.initialize_weights()
|
| 89 |
+
return
|
| 90 |
+
|
| 91 |
+
# Use the language model's initializer_range if available
|
| 92 |
+
if hasattr(self.config, 'language_model_config') and hasattr(self.config.language_model_config, 'initializer_range'):
|
| 93 |
+
std = self.config.language_model_config.initializer_range
|
| 94 |
+
elif hasattr(self.config, 'decoder_config') and hasattr(self.config.decoder_config, 'initializer_range'):
|
| 95 |
+
std = self.config.decoder_config.initializer_range
|
| 96 |
+
else:
|
| 97 |
+
std = 0.02 # Default value
|
| 98 |
+
|
| 99 |
+
if isinstance(module, nn.Linear):
|
| 100 |
+
module.weight.data.normal_(mean=0.0, std=std)
|
| 101 |
+
if module.bias is not None:
|
| 102 |
+
module.bias.data.zero_()
|
| 103 |
+
elif isinstance(module, nn.LayerNorm):
|
| 104 |
+
module.weight.data.fill_(1.0)
|
| 105 |
+
module.bias.data.zero_()
|
| 106 |
+
|
| 107 |
+
# @auto_docstring
|
| 108 |
+
class VibeVoiceModel(VibeVoicePreTrainedModel):
|
| 109 |
+
def __init__(self, config):
|
| 110 |
+
super().__init__(config)
|
| 111 |
+
|
| 112 |
+
if hasattr(config, 'torch_dtype') and config.torch_dtype is not None:
|
| 113 |
+
if isinstance(config.torch_dtype, str):
|
| 114 |
+
dtype = getattr(torch, config.torch_dtype)
|
| 115 |
+
else:
|
| 116 |
+
dtype = config.torch_dtype
|
| 117 |
+
else:
|
| 118 |
+
dtype = torch.float32
|
| 119 |
+
|
| 120 |
+
# Initialize Qwen2 model for language modeling
|
| 121 |
+
lm_config = config.decoder_config
|
| 122 |
+
self.language_model = AutoModel.from_config(lm_config)
|
| 123 |
+
|
| 124 |
+
# Initialize speech components if needed
|
| 125 |
+
self.acoustic_tokenizer = AutoModel.from_config(config.acoustic_tokenizer_config).to(dtype)
|
| 126 |
+
self.semantic_tokenizer = AutoModel.from_config(config.semantic_tokenizer_config).to(dtype)
|
| 127 |
+
|
| 128 |
+
self.acoustic_connector = SpeechConnector(config.acoustic_vae_dim, lm_config.hidden_size).to(dtype)
|
| 129 |
+
self.semantic_connector = SpeechConnector(config.semantic_vae_dim, lm_config.hidden_size).to(dtype)
|
| 130 |
+
|
| 131 |
+
# Register scaling factors as buffers - use 1D tensors for FSDP compatibility
|
| 132 |
+
self.register_buffer('speech_scaling_factor', torch.tensor(float('nan')))
|
| 133 |
+
self.register_buffer('speech_bias_factor', torch.tensor(float('nan')))
|
| 134 |
+
|
| 135 |
+
# Initialize prediction head for speech generation
|
| 136 |
+
self.prediction_head = AutoModel.from_config(config.diffusion_head_config).to(dtype)
|
| 137 |
+
|
| 138 |
+
# Initialize noise scheduler
|
| 139 |
+
self.noise_scheduler = DPMSolverMultistepScheduler(
|
| 140 |
+
num_train_timesteps=config.diffusion_head_config.ddpm_num_steps,
|
| 141 |
+
beta_schedule=config.diffusion_head_config.ddpm_beta_schedule,
|
| 142 |
+
prediction_type=config.diffusion_head_config.prediction_type
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
def get_input_embeddings(self):
|
| 146 |
+
if hasattr(self.language_model, 'embed_tokens'):
|
| 147 |
+
# If the language model has an embed_tokens attribute, return it
|
| 148 |
+
return self.language_model.embed_tokens
|
| 149 |
+
|
| 150 |
+
for name, attr in self.language_model.fullmap.items(): # parallel by nnscaler, the name is changed
|
| 151 |
+
if attr.orig_name == 'embed_tokens.weight':
|
| 152 |
+
return getattr(self.language_model, name)
|
| 153 |
+
assert False, 'should not arrive here'
|
| 154 |
+
|
| 155 |
+
def set_input_embeddings(self, value):
|
| 156 |
+
self.language_model.embed_tokens = value
|
| 157 |
+
|
| 158 |
+
def set_speech_tokenizers(self, acoustic_tokenizer=None, semantic_tokenizer=None):
|
| 159 |
+
"""Set the speech tokenizers used for encoding and decoding speech."""
|
| 160 |
+
self.acoustic_tokenizer = acoustic_tokenizer
|
| 161 |
+
self.semantic_tokenizer = semantic_tokenizer
|
| 162 |
+
|
| 163 |
+
# Reset the encoder to evaluation mode
|
| 164 |
+
if self.acoustic_tokenizer is not None:
|
| 165 |
+
self.acoustic_tokenizer.eval()
|
| 166 |
+
|
| 167 |
+
if self.semantic_tokenizer is not None:
|
| 168 |
+
self.semantic_tokenizer.eval()
|
| 169 |
+
|
| 170 |
+
def forward(
|
| 171 |
+
self,
|
| 172 |
+
input_ids: torch.LongTensor = None,
|
| 173 |
+
attention_mask: Optional[torch.Tensor] = None,
|
| 174 |
+
position_ids: Optional[torch.LongTensor] = None,
|
| 175 |
+
past_key_values: Optional[Tuple[Tuple[torch.FloatTensor]]] = None,
|
| 176 |
+
inputs_embeds: Optional[torch.FloatTensor] = None,
|
| 177 |
+
use_cache: Optional[bool] = None,
|
| 178 |
+
output_attentions: Optional[bool] = None,
|
| 179 |
+
output_hidden_states: Optional[bool] = None,
|
| 180 |
+
return_dict: Optional[bool] = None,
|
| 181 |
+
cache_position: Optional[torch.LongTensor] = None,
|
| 182 |
+
**kwargs,
|
| 183 |
+
) -> Union[Tuple, BaseModelOutputWithPast]:
|
| 184 |
+
|
| 185 |
+
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
|
| 186 |
+
|
| 187 |
+
# Forward through language model
|
| 188 |
+
outputs = self.language_model(
|
| 189 |
+
input_ids=input_ids,
|
| 190 |
+
attention_mask=attention_mask,
|
| 191 |
+
position_ids=position_ids,
|
| 192 |
+
past_key_values=past_key_values,
|
| 193 |
+
inputs_embeds=inputs_embeds,
|
| 194 |
+
use_cache=use_cache,
|
| 195 |
+
output_attentions=output_attentions,
|
| 196 |
+
output_hidden_states=output_hidden_states,
|
| 197 |
+
return_dict=return_dict,
|
| 198 |
+
cache_position=cache_position,
|
| 199 |
+
**kwargs,
|
| 200 |
+
)
|
| 201 |
+
|
| 202 |
+
if not return_dict:
|
| 203 |
+
return outputs
|
| 204 |
+
|
| 205 |
+
return BaseModelOutputWithPast(
|
| 206 |
+
last_hidden_state=outputs.last_hidden_state,
|
| 207 |
+
past_key_values=outputs.past_key_values,
|
| 208 |
+
hidden_states=outputs.hidden_states,
|
| 209 |
+
attentions=outputs.attentions,
|
| 210 |
+
)
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
class VibeVoiceForConditionalGeneration(VibeVoicePreTrainedModel):
|
| 214 |
+
_tied_weights_keys = ["lm_head.weight"]
|
| 215 |
+
_tp_plan = {"lm_head": "colwise_rep"}
|
| 216 |
+
|
| 217 |
+
def __init__(self, config):
|
| 218 |
+
super().__init__(config)
|
| 219 |
+
self.model = VibeVoiceModel(config)
|
| 220 |
+
self.vocab_size = config.decoder_config.vocab_size
|
| 221 |
+
self.lm_head = nn.Linear(config.decoder_config.hidden_size, self.vocab_size, bias=False)
|
| 222 |
+
|
| 223 |
+
self.post_init()
|
| 224 |
+
|
| 225 |
+
def get_input_embeddings(self):
|
| 226 |
+
return self.model.get_input_embeddings()
|
| 227 |
+
|
| 228 |
+
def set_input_embeddings(self, value):
|
| 229 |
+
self.model.set_input_embeddings(value)
|
| 230 |
+
|
| 231 |
+
def get_output_embeddings(self):
|
| 232 |
+
return self.lm_head
|
| 233 |
+
|
| 234 |
+
def set_decoder(self, decoder):
|
| 235 |
+
self.model.language_model = decoder
|
| 236 |
+
|
| 237 |
+
def get_decoder(self):
|
| 238 |
+
return self.model.language_model
|
| 239 |
+
|
| 240 |
+
def tie_weights(self):
|
| 241 |
+
"""
|
| 242 |
+
Tie the weights between the input embeddings and the output embeddings.
|
| 243 |
+
"""
|
| 244 |
+
if getattr(self.config.decoder_config, 'tie_word_embeddings', False):
|
| 245 |
+
# The standard PreTrainedModel method will handle the tying.
|
| 246 |
+
# It typically does a simple parameter object assignment, which is
|
| 247 |
+
# CORRECT to do BEFORE FSDP wraps the model.
|
| 248 |
+
output_embeddings = self.get_output_embeddings()
|
| 249 |
+
input_embeddings = self.get_input_embeddings()
|
| 250 |
+
if hasattr(input_embeddings, 'weight'):
|
| 251 |
+
output_embeddings.weight = input_embeddings.weight
|
| 252 |
+
else:
|
| 253 |
+
# maybe returned input_embeddings a tensor directly
|
| 254 |
+
output_embeddings.weight = input_embeddings
|
| 255 |
+
|
| 256 |
+
if getattr(output_embeddings, "bias", None) is not None:
|
| 257 |
+
output_embeddings.bias.data = nn.functional.pad(
|
| 258 |
+
output_embeddings.bias.data,
|
| 259 |
+
(0, output_embeddings.weight.shape[0] - output_embeddings.bias.shape[0]),
|
| 260 |
+
"constant",
|
| 261 |
+
0,
|
| 262 |
+
)
|
| 263 |
+
print("Tied input and output embeddings using standard assignment.")
|
| 264 |
+
else:
|
| 265 |
+
print("tie_word_embeddings is False, not tying weights.")
|
| 266 |
+
|
| 267 |
+
# Also, ensure set_output_embeddings is safe, though your implementation looks okay.
|
| 268 |
+
# The key is to avoid calling it after accelerator.prepare().
|
| 269 |
+
def set_output_embeddings(self, new_embeddings):
|
| 270 |
+
# Your current implementation using data.copy_ is good practice,
|
| 271 |
+
# but the best way is to not call this after prepare().
|
| 272 |
+
self.lm_head = new_embeddings
|
| 273 |
+
|
| 274 |
+
def forward_speech_features(
|
| 275 |
+
self,
|
| 276 |
+
speech_tensors=None,
|
| 277 |
+
speech_masks=None,
|
| 278 |
+
speech_type="audio",
|
| 279 |
+
return_unmask=False
|
| 280 |
+
):
|
| 281 |
+
if speech_tensors is None:
|
| 282 |
+
# Use config to get vae_dim instead of non-existent self.args
|
| 283 |
+
vae_dim = self.config.acoustic_tokenizer_config.vae_dim
|
| 284 |
+
audio_features = torch.zeros(1, 1, vae_dim).to(self.get_input_embeddings().weight)
|
| 285 |
+
connect_features = self.model.acoustic_connector(audio_features)
|
| 286 |
+
return audio_features, connect_features
|
| 287 |
+
else:
|
| 288 |
+
with torch.no_grad():
|
| 289 |
+
if speech_type == "audio":
|
| 290 |
+
with torch.no_grad():
|
| 291 |
+
frames = self.model.acoustic_tokenizer.encode(speech_tensors.unsqueeze(1))[0][0]
|
| 292 |
+
audio_tokens = frames.sample(self.model.acoustic_tokenizer.std_dist_type)[0]
|
| 293 |
+
|
| 294 |
+
elif speech_type == "vae":
|
| 295 |
+
# Use config to get vae_dim instead of non-existent self.args
|
| 296 |
+
vae_dim = self.config.acoustic_tokenizer_config.vae_dim
|
| 297 |
+
speech_mode = speech_tensors.reshape(speech_tensors.size(0), -1, vae_dim)
|
| 298 |
+
|
| 299 |
+
# gaussian sample from the speech_mode
|
| 300 |
+
batch_size = speech_mode.size(0)
|
| 301 |
+
value = self.model.acoustic_tokenizer.fix_std / 0.8
|
| 302 |
+
std = torch.randn(batch_size, dtype=speech_mode.dtype, device=speech_mode.device) * value
|
| 303 |
+
std = std.view(-1, *[1] * (speech_mode.dim() - 1))
|
| 304 |
+
audio_tokens = speech_mode + std * torch.randn(speech_mode.shape).to(speech_mode)
|
| 305 |
+
else:
|
| 306 |
+
raise NotImplementedError(f"Speech type {speech_type} not implemented")
|
| 307 |
+
|
| 308 |
+
if torch.isnan(self.model.speech_scaling_factor) or torch.isnan(self.model.speech_bias_factor):
|
| 309 |
+
scaling_factor = 1. / audio_tokens[speech_masks].flatten().std()
|
| 310 |
+
bias_factor = -audio_tokens[speech_masks].flatten().mean()
|
| 311 |
+
|
| 312 |
+
# Only use distributed operations if the process group is initialized
|
| 313 |
+
if dist.is_available() and dist.is_initialized():
|
| 314 |
+
dist.all_reduce(scaling_factor, op=dist.ReduceOp.SUM)
|
| 315 |
+
dist.all_reduce(bias_factor, op=dist.ReduceOp.SUM)
|
| 316 |
+
world_size = dist.get_world_size()
|
| 317 |
+
self.model.speech_scaling_factor.copy_(scaling_factor / world_size)
|
| 318 |
+
self.model.speech_bias_factor.copy_(bias_factor / world_size)
|
| 319 |
+
print(f"Speech scaling factor (distributed): {self.model.speech_scaling_factor}, bias factor: {self.model.speech_bias_factor}", flush=True)
|
| 320 |
+
else:
|
| 321 |
+
# Single process case
|
| 322 |
+
self.model.speech_scaling_factor.copy_(scaling_factor)
|
| 323 |
+
self.model.speech_bias_factor.copy_(bias_factor)
|
| 324 |
+
print(f"Speech scaling factor (single process): {self.model.speech_scaling_factor}, bias factor: {self.model.speech_bias_factor}", flush=True)
|
| 325 |
+
|
| 326 |
+
audio_features = (audio_tokens + self.model.speech_bias_factor) * self.model.speech_scaling_factor
|
| 327 |
+
|
| 328 |
+
connect_features = self.model.acoustic_connector(audio_features)
|
| 329 |
+
if return_unmask:
|
| 330 |
+
return audio_features, connect_features
|
| 331 |
+
return audio_features[speech_masks], connect_features[speech_masks]
|
| 332 |
+
|
| 333 |
+
def forward(
|
| 334 |
+
self,
|
| 335 |
+
input_ids: torch.LongTensor = None,
|
| 336 |
+
attention_mask: Optional[torch.Tensor] = None,
|
| 337 |
+
position_ids: Optional[torch.LongTensor] = None,
|
| 338 |
+
past_key_values: Optional[List[torch.FloatTensor]] = None,
|
| 339 |
+
inputs_embeds: Optional[torch.FloatTensor] = None,
|
| 340 |
+
labels: Optional[torch.LongTensor] = None,
|
| 341 |
+
use_cache: Optional[bool] = False,
|
| 342 |
+
output_attentions: Optional[bool] = None,
|
| 343 |
+
output_hidden_states: Optional[bool] = None,
|
| 344 |
+
return_dict: Optional[bool] = None,
|
| 345 |
+
cache_position: Optional[torch.LongTensor] = None,
|
| 346 |
+
# New arguments for speech processing and loss calculation
|
| 347 |
+
speech_tensors: Optional[torch.FloatTensor] = None,
|
| 348 |
+
speech_masks: Optional[torch.BoolTensor] = None,
|
| 349 |
+
speeches_loss_input: Optional[torch.FloatTensor] = None,
|
| 350 |
+
speech_semantic_tensors: Optional[torch.FloatTensor] = None,
|
| 351 |
+
acoustic_input_mask: Optional[torch.BoolTensor] = None,
|
| 352 |
+
acoustic_loss_mask: Optional[torch.BoolTensor] = None,
|
| 353 |
+
ddpm_batch_mul: int = 1,
|
| 354 |
+
**kwargs: Optional[Dict[str, Union[torch.Tensor, str]]],
|
| 355 |
+
) -> Union[Tuple, VibeVoiceCausalLMOutputWithPast]:
|
| 356 |
+
|
| 357 |
+
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
|
| 358 |
+
|
| 359 |
+
x = self.get_input_embeddings()(input_ids)
|
| 360 |
+
|
| 361 |
+
semantic_speech_all_connect_features = self.model.semantic_connector(speech_semantic_tensors)
|
| 362 |
+
if speeches_loss_input is not None:
|
| 363 |
+
# only part audio need diffuse
|
| 364 |
+
speech_all_features, speech_all_connect_features = self.forward_speech_features(
|
| 365 |
+
speech_tensors=speech_tensors.type_as(x) if speech_tensors is not None else None,
|
| 366 |
+
speech_masks=speech_masks,
|
| 367 |
+
speech_type=kwargs.get("speech_type", "audio"),
|
| 368 |
+
return_unmask=True
|
| 369 |
+
)
|
| 370 |
+
if speech_tensors is not None:
|
| 371 |
+
if semantic_speech_all_connect_features is not None:
|
| 372 |
+
x[acoustic_input_mask] = (
|
| 373 |
+
speech_all_connect_features[speech_masks]
|
| 374 |
+
+ semantic_speech_all_connect_features[speech_masks]
|
| 375 |
+
)
|
| 376 |
+
else:
|
| 377 |
+
x[acoustic_input_mask] = speech_all_connect_features[speech_masks]
|
| 378 |
+
|
| 379 |
+
# Select only the target segments' latents for diffusion loss.
|
| 380 |
+
# Both masks are [num_segments, max_latent_len]; using 2D mask on [B,T,D] selects [N_true, D].
|
| 381 |
+
target_latent_mask = speeches_loss_input & speech_masks
|
| 382 |
+
speech_features = speech_all_features[target_latent_mask]
|
| 383 |
+
speech_connect_features = speech_all_connect_features[target_latent_mask]
|
| 384 |
+
else:
|
| 385 |
+
speech_features, speech_connect_features = self.forward_speech_features(
|
| 386 |
+
speech_tensors=speech_tensors.type_as(x) if speech_tensors is not None else None,
|
| 387 |
+
speech_masks=speech_masks,
|
| 388 |
+
speech_type=kwargs.get("speech_type", "audio"),
|
| 389 |
+
)
|
| 390 |
+
if speech_tensors is not None:
|
| 391 |
+
x[acoustic_input_mask] = speech_connect_features
|
| 392 |
+
|
| 393 |
+
outputs = self.model(
|
| 394 |
+
input_ids=None,
|
| 395 |
+
attention_mask=attention_mask,
|
| 396 |
+
position_ids=position_ids,
|
| 397 |
+
past_key_values=past_key_values,
|
| 398 |
+
inputs_embeds=x,
|
| 399 |
+
use_cache=use_cache,
|
| 400 |
+
output_attentions=output_attentions,
|
| 401 |
+
output_hidden_states=False,
|
| 402 |
+
return_dict=return_dict,
|
| 403 |
+
cache_position=cache_position,
|
| 404 |
+
)
|
| 405 |
+
|
| 406 |
+
hidden_states = outputs.last_hidden_state
|
| 407 |
+
logits = self.lm_head(hidden_states)
|
| 408 |
+
# logits = logits.float()
|
| 409 |
+
|
| 410 |
+
loss = None
|
| 411 |
+
if labels is not None:
|
| 412 |
+
# The custom CE loss with masking is calculated in the training script.
|
| 413 |
+
# We leave the standard loss calculation here as None.
|
| 414 |
+
pass
|
| 415 |
+
|
| 416 |
+
# --- Diffusion Loss Calculation ---
|
| 417 |
+
diffusion_loss = None
|
| 418 |
+
# This block is executed only if we are in a context that involves speech.
|
| 419 |
+
if speech_tensors is not None and acoustic_loss_mask.sum().item() > 0:
|
| 420 |
+
condition_features = hidden_states[acoustic_loss_mask]
|
| 421 |
+
|
| 422 |
+
speech_len, latent_size = speech_features.shape
|
| 423 |
+
|
| 424 |
+
noise = torch.randn(
|
| 425 |
+
(speech_len * ddpm_batch_mul, latent_size),
|
| 426 |
+
device=hidden_states.device,
|
| 427 |
+
dtype=hidden_states.dtype
|
| 428 |
+
)
|
| 429 |
+
|
| 430 |
+
timesteps = torch.multinomial(
|
| 431 |
+
torch.ones(self.config.diffusion_head_config.ddpm_num_steps),
|
| 432 |
+
speech_len * ddpm_batch_mul,
|
| 433 |
+
replacement=True,
|
| 434 |
+
).to(hidden_states.device)
|
| 435 |
+
|
| 436 |
+
speech_features_repeated = speech_features.repeat_interleave(ddpm_batch_mul, dim=0)
|
| 437 |
+
condition_features_repeated = condition_features.repeat_interleave(ddpm_batch_mul, dim=0)
|
| 438 |
+
|
| 439 |
+
noisy_speech_features = self.model.noise_scheduler.add_noise(
|
| 440 |
+
speech_features_repeated, noise, timesteps
|
| 441 |
+
)
|
| 442 |
+
|
| 443 |
+
model_output = self.model.prediction_head(
|
| 444 |
+
noisy_speech_features,
|
| 445 |
+
timesteps.type_as(x),
|
| 446 |
+
condition_features_repeated
|
| 447 |
+
)
|
| 448 |
+
|
| 449 |
+
prediction_type = self.config.diffusion_head_config.prediction_type
|
| 450 |
+
if prediction_type == "epsilon":
|
| 451 |
+
target_for_loss = noise
|
| 452 |
+
elif prediction_type == "v_prediction":
|
| 453 |
+
target_for_loss = self.model.noise_scheduler.get_velocity(
|
| 454 |
+
speech_features_repeated, noise, timesteps
|
| 455 |
+
)
|
| 456 |
+
else:
|
| 457 |
+
raise NotImplementedError(f"Prediction type {prediction_type} not implemented")
|
| 458 |
+
|
| 459 |
+
diffusion_loss = F.mse_loss(model_output.float(), target_for_loss.float(), reduction='sum')
|
| 460 |
+
if latent_size > 0 and ddpm_batch_mul > 0:
|
| 461 |
+
diffusion_loss = diffusion_loss / latent_size / ddpm_batch_mul
|
| 462 |
+
else:
|
| 463 |
+
diffusion_loss = torch.tensor(0.0, device=diffusion_loss.device)
|
| 464 |
+
|
| 465 |
+
else:
|
| 466 |
+
# Dummy loss for DDP to work when there are no speech samples in a batch,
|
| 467 |
+
# but we are in a speech context.
|
| 468 |
+
diffusion_loss = sum(p.sum() for p in self.model.prediction_head.parameters()) * 0.0
|
| 469 |
+
diffusion_loss += sum(p.sum() for p in self.model.acoustic_connector.parameters()) * 0.0
|
| 470 |
+
diffusion_loss += sum(p.sum() for p in self.model.semantic_connector.parameters()) * 0.0
|
| 471 |
+
# --- End Diffusion Loss Calculation ---
|
| 472 |
+
|
| 473 |
+
if not return_dict:
|
| 474 |
+
output = (logits, speech_len) + outputs.to_tuple()[1:]
|
| 475 |
+
return (loss, diffusion_loss) + output
|
| 476 |
+
|
| 477 |
+
return VibeVoiceCausalLMOutputWithPast(
|
| 478 |
+
loss=loss,
|
| 479 |
+
diffusion_loss=diffusion_loss,
|
| 480 |
+
speech_token_num=speech_len if speech_tensors is not None else 0,
|
| 481 |
+
logits=logits,
|
| 482 |
+
past_key_values=outputs.past_key_values,
|
| 483 |
+
hidden_states=outputs.hidden_states,
|
| 484 |
+
attentions=outputs.attentions,
|
| 485 |
+
)
|
| 486 |
+
|
| 487 |
+
AutoModel.register(VibeVoiceConfig, VibeVoiceModel)
|
| 488 |
+
AutoModelForCausalLM.register(VibeVoiceConfig, VibeVoiceForConditionalGeneration)
|
| 489 |
+
|
| 490 |
+
__all__ = [
|
| 491 |
+
"VibeVoiceModel",
|
| 492 |
+
"VibeVoicePreTrainedModel",
|
| 493 |
+
"VibeVoiceForConditionalGeneration",
|
| 494 |
+
"VibeVoiceCausalLMOutputWithPast",
|
| 495 |
+
"VibeVoiceGenerationOutput",
|
| 496 |
+
]
|
vibevoice/modular/modeling_vibevoice_asr.py
ADDED
|
@@ -0,0 +1,522 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Optional, Tuple, Union
|
| 2 |
+
import torch
|
| 3 |
+
import torch.nn as nn
|
| 4 |
+
|
| 5 |
+
from transformers.models.auto import AutoModel, AutoModelForCausalLM
|
| 6 |
+
|
| 7 |
+
from transformers.modeling_outputs import CausalLMOutput, BaseModelOutputWithPast
|
| 8 |
+
from transformers import modeling_utils
|
| 9 |
+
from transformers.modeling_utils import PreTrainedModel
|
| 10 |
+
from transformers.utils import logging
|
| 11 |
+
from transformers.generation import GenerationMixin
|
| 12 |
+
|
| 13 |
+
from .modular_vibevoice_tokenizer import (
|
| 14 |
+
VibeVoiceTokenizerStreamingCache,
|
| 15 |
+
VibeVoiceTokenizerEncoderOutput
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
from .configuration_vibevoice import VibeVoiceASRConfig
|
| 19 |
+
from .modeling_vibevoice import (
|
| 20 |
+
VibeVoiceCausalLMOutputWithPast,
|
| 21 |
+
SpeechConnector
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
logger = logging.get_logger(__name__)
|
| 25 |
+
|
| 26 |
+
if not hasattr(modeling_utils, "ALL_PARALLEL_STYLES") or modeling_utils.ALL_PARALLEL_STYLES is None:
|
| 27 |
+
modeling_utils.ALL_PARALLEL_STYLES = ["tp", "none", "colwise", "rowwise"]
|
| 28 |
+
|
| 29 |
+
# @auto_docstring
|
| 30 |
+
class VibeVoiceASRPreTrainedModel(PreTrainedModel):
|
| 31 |
+
config_class = VibeVoiceASRConfig
|
| 32 |
+
base_model_prefix = "model"
|
| 33 |
+
supports_gradient_checkpointing = True
|
| 34 |
+
_skip_keys_device_placement = "past_key_values"
|
| 35 |
+
_supports_cache_class = True
|
| 36 |
+
_supports_flash_attn = True
|
| 37 |
+
_supports_flash_attn_2 = True
|
| 38 |
+
_supports_sdpa = True
|
| 39 |
+
_supports_quantized_cache = True
|
| 40 |
+
_supports_static_cache = True
|
| 41 |
+
_supports_attention_backend = True
|
| 42 |
+
|
| 43 |
+
def _init_weights(self, module):
|
| 44 |
+
|
| 45 |
+
# Use the language model's initializer_range if available
|
| 46 |
+
if hasattr(self.config, 'language_model_config') and hasattr(self.config.language_model_config, 'initializer_range'):
|
| 47 |
+
std = self.config.language_model_config.initializer_range
|
| 48 |
+
elif hasattr(self.config, 'decoder_config') and hasattr(self.config.decoder_config, 'initializer_range'):
|
| 49 |
+
std = self.config.decoder_config.initializer_range
|
| 50 |
+
else:
|
| 51 |
+
std = 0.02 # Default value
|
| 52 |
+
|
| 53 |
+
if isinstance(module, nn.Linear):
|
| 54 |
+
module.weight.data.normal_(mean=0.0, std=std)
|
| 55 |
+
if module.bias is not None:
|
| 56 |
+
module.bias.data.zero_()
|
| 57 |
+
elif isinstance(module, nn.LayerNorm):
|
| 58 |
+
module.weight.data.fill_(1.0)
|
| 59 |
+
module.bias.data.zero_()
|
| 60 |
+
|
| 61 |
+
# @auto_docstring
|
| 62 |
+
class VibeVoiceASRModel(VibeVoiceASRPreTrainedModel):
|
| 63 |
+
def __init__(self, config):
|
| 64 |
+
super().__init__(config)
|
| 65 |
+
|
| 66 |
+
if hasattr(config, 'torch_dtype') and config.torch_dtype is not None:
|
| 67 |
+
if isinstance(config.torch_dtype, str):
|
| 68 |
+
dtype = getattr(torch, config.torch_dtype)
|
| 69 |
+
else:
|
| 70 |
+
dtype = config.torch_dtype
|
| 71 |
+
else:
|
| 72 |
+
dtype = torch.float32
|
| 73 |
+
|
| 74 |
+
# Initialize Qwen2 model for language modeling
|
| 75 |
+
lm_config = config.decoder_config
|
| 76 |
+
self.language_model = AutoModel.from_config(lm_config)
|
| 77 |
+
|
| 78 |
+
# Initialize speech components if needed
|
| 79 |
+
self.acoustic_tokenizer = AutoModel.from_config(config.acoustic_tokenizer_config).to(dtype)
|
| 80 |
+
self.semantic_tokenizer = AutoModel.from_config(config.semantic_tokenizer_config).to(dtype)
|
| 81 |
+
|
| 82 |
+
self.acoustic_connector = SpeechConnector(config.acoustic_vae_dim, lm_config.hidden_size).to(dtype)
|
| 83 |
+
self.semantic_connector = SpeechConnector(config.semantic_vae_dim, lm_config.hidden_size).to(dtype)
|
| 84 |
+
|
| 85 |
+
def get_input_embeddings(self):
|
| 86 |
+
if hasattr(self.language_model, 'embed_tokens'):
|
| 87 |
+
# If the language model has an embed_tokens attribute, return it
|
| 88 |
+
return self.language_model.embed_tokens
|
| 89 |
+
|
| 90 |
+
for name, attr in self.language_model.fullmap.items(): # parallel by nnscaler, the name is changed
|
| 91 |
+
if attr.orig_name == 'embed_tokens.weight':
|
| 92 |
+
return getattr(self.language_model, name)
|
| 93 |
+
assert False, 'should not arrive here'
|
| 94 |
+
|
| 95 |
+
def set_input_embeddings(self, value):
|
| 96 |
+
self.language_model.embed_tokens = value
|
| 97 |
+
|
| 98 |
+
def set_speech_tokenizers(self, acoustic_tokenizer=None, semantic_tokenizer=None):
|
| 99 |
+
"""Set the speech tokenizers used for encoding and decoding speech."""
|
| 100 |
+
self.acoustic_tokenizer = acoustic_tokenizer
|
| 101 |
+
self.semantic_tokenizer = semantic_tokenizer
|
| 102 |
+
|
| 103 |
+
# Reset the encoder to evaluation mode
|
| 104 |
+
if self.acoustic_tokenizer is not None:
|
| 105 |
+
self.acoustic_tokenizer.eval()
|
| 106 |
+
|
| 107 |
+
if self.semantic_tokenizer is not None:
|
| 108 |
+
self.semantic_tokenizer.eval()
|
| 109 |
+
|
| 110 |
+
def forward(
|
| 111 |
+
self,
|
| 112 |
+
input_ids: torch.LongTensor = None,
|
| 113 |
+
attention_mask: Optional[torch.Tensor] = None,
|
| 114 |
+
position_ids: Optional[torch.LongTensor] = None,
|
| 115 |
+
past_key_values: Optional[Tuple[Tuple[torch.FloatTensor]]] = None,
|
| 116 |
+
inputs_embeds: Optional[torch.FloatTensor] = None,
|
| 117 |
+
use_cache: Optional[bool] = None,
|
| 118 |
+
output_attentions: Optional[bool] = None,
|
| 119 |
+
output_hidden_states: Optional[bool] = None,
|
| 120 |
+
return_dict: Optional[bool] = None,
|
| 121 |
+
cache_position: Optional[torch.LongTensor] = None,
|
| 122 |
+
**kwargs,
|
| 123 |
+
) -> Union[Tuple, BaseModelOutputWithPast]:
|
| 124 |
+
|
| 125 |
+
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
|
| 126 |
+
|
| 127 |
+
# Forward through language model
|
| 128 |
+
outputs = self.language_model(
|
| 129 |
+
input_ids=input_ids,
|
| 130 |
+
attention_mask=attention_mask,
|
| 131 |
+
position_ids=position_ids,
|
| 132 |
+
past_key_values=past_key_values,
|
| 133 |
+
inputs_embeds=inputs_embeds,
|
| 134 |
+
use_cache=use_cache,
|
| 135 |
+
output_attentions=output_attentions,
|
| 136 |
+
output_hidden_states=output_hidden_states,
|
| 137 |
+
return_dict=return_dict,
|
| 138 |
+
cache_position=cache_position,
|
| 139 |
+
**kwargs,
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
if not return_dict:
|
| 143 |
+
return outputs
|
| 144 |
+
|
| 145 |
+
return BaseModelOutputWithPast(
|
| 146 |
+
last_hidden_state=outputs.last_hidden_state,
|
| 147 |
+
past_key_values=outputs.past_key_values,
|
| 148 |
+
hidden_states=outputs.hidden_states,
|
| 149 |
+
attentions=outputs.attentions,
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
+
class VibeVoiceASRForConditionalGeneration(VibeVoiceASRPreTrainedModel, GenerationMixin):
|
| 153 |
+
"""
|
| 154 |
+
VibeVoice model for Automatic Speech Recognition (ASR) with language modeling head for conditional generation.
|
| 155 |
+
This class is designed for inference and generation tasks.
|
| 156 |
+
"""
|
| 157 |
+
_tied_weights_keys = ["lm_head.weight"]
|
| 158 |
+
_tp_plan = {"lm_head": "colwise_rep"}
|
| 159 |
+
|
| 160 |
+
def __init__(self, config):
|
| 161 |
+
super().__init__(config)
|
| 162 |
+
self.model = VibeVoiceASRModel(config)
|
| 163 |
+
self.vocab_size = config.decoder_config.vocab_size
|
| 164 |
+
|
| 165 |
+
# Determine the dtype to use
|
| 166 |
+
if hasattr(config, 'torch_dtype') and config.torch_dtype is not None:
|
| 167 |
+
if isinstance(config.torch_dtype, str):
|
| 168 |
+
dtype = getattr(torch, config.torch_dtype)
|
| 169 |
+
else:
|
| 170 |
+
dtype = config.torch_dtype
|
| 171 |
+
else:
|
| 172 |
+
dtype = torch.float32
|
| 173 |
+
|
| 174 |
+
# Initialize lm_head with the correct dtype
|
| 175 |
+
self.lm_head = nn.Linear(config.decoder_config.hidden_size, self.vocab_size, bias=False).to(dtype)
|
| 176 |
+
|
| 177 |
+
# Initialize weights and apply final processing
|
| 178 |
+
self.post_init()
|
| 179 |
+
|
| 180 |
+
def get_input_embeddings(self):
|
| 181 |
+
return self.model.get_input_embeddings()
|
| 182 |
+
|
| 183 |
+
def set_input_embeddings(self, value):
|
| 184 |
+
self.model.set_input_embeddings(value)
|
| 185 |
+
|
| 186 |
+
def get_output_embeddings(self):
|
| 187 |
+
return self.lm_head
|
| 188 |
+
|
| 189 |
+
def set_output_embeddings(self, new_embeddings):
|
| 190 |
+
self.lm_head = new_embeddings
|
| 191 |
+
|
| 192 |
+
def set_decoder(self, decoder):
|
| 193 |
+
self.model.language_model = decoder
|
| 194 |
+
|
| 195 |
+
def get_decoder(self):
|
| 196 |
+
return self.model.language_model
|
| 197 |
+
|
| 198 |
+
def tie_weights(self):
|
| 199 |
+
"""Tie the weights between the input embeddings and the output embeddings."""
|
| 200 |
+
if getattr(self.config.decoder_config, 'tie_word_embeddings', False):
|
| 201 |
+
output_embeddings = self.get_output_embeddings()
|
| 202 |
+
input_embeddings = self.get_input_embeddings()
|
| 203 |
+
if hasattr(input_embeddings, 'weight'):
|
| 204 |
+
output_embeddings.weight = input_embeddings.weight
|
| 205 |
+
else:
|
| 206 |
+
output_embeddings.weight = input_embeddings
|
| 207 |
+
|
| 208 |
+
def encode_speech(
|
| 209 |
+
self,
|
| 210 |
+
speech_tensors: torch.FloatTensor,
|
| 211 |
+
speech_masks: Optional[torch.BoolTensor] = None,
|
| 212 |
+
speech_semantic_tensors: Optional[torch.FloatTensor] = None,
|
| 213 |
+
streaming_segment_duration: float = 60.0, # seconds
|
| 214 |
+
):
|
| 215 |
+
"""
|
| 216 |
+
Encode speech input into features that can be used by the language model.
|
| 217 |
+
This method is called once before generation to process the speech input.
|
| 218 |
+
|
| 219 |
+
For long audio (>600s by default), uses streaming processing to avoid conv overflow (>2^32).
|
| 220 |
+
Segments are processed independently, then concatenated before final sampling.
|
| 221 |
+
|
| 222 |
+
Args:
|
| 223 |
+
speech_tensors: Input audio tensor [batch_size, samples]
|
| 224 |
+
speech_masks: Optional mask for speech features
|
| 225 |
+
speech_semantic_tensors: Optional pre-computed semantic tokens
|
| 226 |
+
streaming_segment_duration: Segment duration in seconds for streaming processing (default: 60s)
|
| 227 |
+
"""
|
| 228 |
+
if hasattr(self.config, 'torch_dtype') and self.config.torch_dtype is not None:
|
| 229 |
+
if isinstance(self.config.torch_dtype, str):
|
| 230 |
+
dtype = getattr(torch, self.config.torch_dtype)
|
| 231 |
+
else:
|
| 232 |
+
dtype = self.config.torch_dtype
|
| 233 |
+
else:
|
| 234 |
+
dtype = torch.float32
|
| 235 |
+
|
| 236 |
+
speech_tensors = speech_tensors.to(dtype)
|
| 237 |
+
|
| 238 |
+
# Ensure proper shape: (batch, samples)
|
| 239 |
+
if speech_tensors.ndim == 1:
|
| 240 |
+
speech_tensors = speech_tensors.unsqueeze(0)
|
| 241 |
+
|
| 242 |
+
batch_size, total_samples = speech_tensors.shape
|
| 243 |
+
sample_rate = 24000 # fix 24kHz sample rate
|
| 244 |
+
|
| 245 |
+
# Calculate segment size in samples
|
| 246 |
+
segment_samples = int(streaming_segment_duration * sample_rate)
|
| 247 |
+
|
| 248 |
+
# Decide whether to use streaming based on audio length
|
| 249 |
+
use_streaming = total_samples > segment_samples
|
| 250 |
+
|
| 251 |
+
with torch.no_grad():
|
| 252 |
+
if not use_streaming:
|
| 253 |
+
# Short audio: direct processing (original behavior)
|
| 254 |
+
encoder_output = self.model.acoustic_tokenizer.encode(speech_tensors.unsqueeze(1))
|
| 255 |
+
audio_tokens = encoder_output.sample(dist_type=self.model.acoustic_tokenizer.std_dist_type)[0]
|
| 256 |
+
acoustic_features = self.model.acoustic_connector(audio_tokens)
|
| 257 |
+
|
| 258 |
+
# Encode semantic features
|
| 259 |
+
if speech_semantic_tensors is not None:
|
| 260 |
+
semantic_features = self.model.semantic_connector(speech_semantic_tensors)
|
| 261 |
+
else:
|
| 262 |
+
semantic_tokens = self.model.semantic_tokenizer.encode(speech_tensors.unsqueeze(1)).mean
|
| 263 |
+
semantic_features = self.model.semantic_connector(semantic_tokens)
|
| 264 |
+
else:
|
| 265 |
+
# Long audio: streaming processing
|
| 266 |
+
# print(f"Using streaming processing for long audio: {total_samples/sample_rate:.1f}s "
|
| 267 |
+
# f"(segment size: {streaming_segment_duration}s)")
|
| 268 |
+
|
| 269 |
+
# Initialize caches for both tokenizers
|
| 270 |
+
acoustic_encoder_cache = VibeVoiceTokenizerStreamingCache()
|
| 271 |
+
semantic_encoder_cache = VibeVoiceTokenizerStreamingCache()
|
| 272 |
+
acoustic_mean_segments = []
|
| 273 |
+
semantic_mean_segments = []
|
| 274 |
+
sample_indices = torch.arange(batch_size, device=speech_tensors.device)
|
| 275 |
+
|
| 276 |
+
# Helper function from batch_asr_sft_cache.py
|
| 277 |
+
def _iter_segments(total_length: int, segment_length: int):
|
| 278 |
+
"""Iterate over audio segments with a given segment length."""
|
| 279 |
+
if segment_length <= 0:
|
| 280 |
+
raise ValueError("segment_length must be positive")
|
| 281 |
+
for start in range(0, total_length, segment_length):
|
| 282 |
+
end = min(start + segment_length, total_length)
|
| 283 |
+
if end > start:
|
| 284 |
+
yield start, end
|
| 285 |
+
|
| 286 |
+
# Process each segment for both acoustic and semantic tokenizers
|
| 287 |
+
segments = list(_iter_segments(total_samples, segment_samples))
|
| 288 |
+
num_segments = len(segments)
|
| 289 |
+
for seg_idx, (start, end) in enumerate(segments):
|
| 290 |
+
chunk = speech_tensors[:, start:end].contiguous()
|
| 291 |
+
if chunk.numel() == 0:
|
| 292 |
+
continue
|
| 293 |
+
|
| 294 |
+
# Check if this is the final segment
|
| 295 |
+
is_final = (seg_idx == num_segments - 1)
|
| 296 |
+
|
| 297 |
+
# Encode chunk for acoustic tokenizer (don't sample yet)
|
| 298 |
+
acoustic_encoder_output = self.model.acoustic_tokenizer.encode(
|
| 299 |
+
chunk.unsqueeze(1),
|
| 300 |
+
cache=acoustic_encoder_cache,
|
| 301 |
+
sample_indices=sample_indices,
|
| 302 |
+
use_cache=True,
|
| 303 |
+
is_final_chunk=is_final,
|
| 304 |
+
)
|
| 305 |
+
acoustic_mean_segments.append(acoustic_encoder_output.mean)
|
| 306 |
+
|
| 307 |
+
# Encode chunk for semantic tokenizer (take mean directly)
|
| 308 |
+
semantic_encoder_output = self.model.semantic_tokenizer.encode(
|
| 309 |
+
chunk.unsqueeze(1),
|
| 310 |
+
cache=semantic_encoder_cache,
|
| 311 |
+
sample_indices=sample_indices,
|
| 312 |
+
use_cache=True,
|
| 313 |
+
is_final_chunk=is_final,
|
| 314 |
+
)
|
| 315 |
+
semantic_mean_segments.append(semantic_encoder_output.mean)
|
| 316 |
+
|
| 317 |
+
# print(f"Processed {len(acoustic_mean_segments)} segments.")
|
| 318 |
+
# Concatenate all acoustic means and sample once
|
| 319 |
+
acoustic_mean_full = torch.cat(acoustic_mean_segments, dim=1).contiguous()
|
| 320 |
+
acoustic_encoder_output = VibeVoiceTokenizerEncoderOutput(
|
| 321 |
+
mean=acoustic_mean_full,
|
| 322 |
+
std=self.model.acoustic_tokenizer.fix_std
|
| 323 |
+
)
|
| 324 |
+
audio_tokens = acoustic_encoder_output.sample(
|
| 325 |
+
dist_type=self.model.acoustic_tokenizer.std_dist_type
|
| 326 |
+
)[0]
|
| 327 |
+
acoustic_features = self.model.acoustic_connector(audio_tokens)
|
| 328 |
+
|
| 329 |
+
# Concatenate all semantic means
|
| 330 |
+
semantic_tokens = torch.cat(semantic_mean_segments, dim=1).contiguous()
|
| 331 |
+
semantic_features = self.model.semantic_connector(semantic_tokens)
|
| 332 |
+
|
| 333 |
+
# Combine acoustic and semantic features
|
| 334 |
+
if speech_masks is not None:
|
| 335 |
+
combined_features = acoustic_features[speech_masks] + semantic_features[speech_masks]
|
| 336 |
+
else:
|
| 337 |
+
combined_features = acoustic_features + semantic_features
|
| 338 |
+
|
| 339 |
+
return combined_features
|
| 340 |
+
|
| 341 |
+
def forward(
|
| 342 |
+
self,
|
| 343 |
+
input_ids: Optional[torch.LongTensor] = None,
|
| 344 |
+
attention_mask: Optional[torch.Tensor] = None,
|
| 345 |
+
position_ids: Optional[torch.LongTensor] = None,
|
| 346 |
+
past_key_values: Optional[List[torch.FloatTensor]] = None,
|
| 347 |
+
inputs_embeds: Optional[torch.FloatTensor] = None,
|
| 348 |
+
labels: Optional[torch.LongTensor] = None,
|
| 349 |
+
use_cache: Optional[bool] = None,
|
| 350 |
+
output_attentions: Optional[bool] = None,
|
| 351 |
+
output_hidden_states: Optional[bool] = None,
|
| 352 |
+
return_dict: Optional[bool] = None,
|
| 353 |
+
cache_position: Optional[torch.LongTensor] = None,
|
| 354 |
+
# Speech-specific arguments
|
| 355 |
+
speech_tensors: Optional[torch.FloatTensor] = None,
|
| 356 |
+
speech_masks: Optional[torch.BoolTensor] = None,
|
| 357 |
+
speech_semantic_tensors: Optional[torch.FloatTensor] = None,
|
| 358 |
+
acoustic_input_mask: Optional[torch.BoolTensor] = None,
|
| 359 |
+
**kwargs,
|
| 360 |
+
) -> Union[Tuple, CausalLMOutput]:
|
| 361 |
+
"""
|
| 362 |
+
Forward pass for the model. Handles both training and generation scenarios.
|
| 363 |
+
"""
|
| 364 |
+
output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions
|
| 365 |
+
output_hidden_states = output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
|
| 366 |
+
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
|
| 367 |
+
use_cache = use_cache if use_cache is not None else getattr(self.config, 'use_cache', False)
|
| 368 |
+
|
| 369 |
+
# Process inputs
|
| 370 |
+
if inputs_embeds is None and input_ids is not None:
|
| 371 |
+
inputs_embeds = self.get_input_embeddings()(input_ids)
|
| 372 |
+
|
| 373 |
+
# If we have speech input and acoustic_input_mask, encode and insert speech features
|
| 374 |
+
if speech_tensors is not None and acoustic_input_mask is not None:
|
| 375 |
+
speech_features = self.encode_speech(
|
| 376 |
+
speech_tensors=speech_tensors,
|
| 377 |
+
speech_masks=speech_masks,
|
| 378 |
+
speech_semantic_tensors=speech_semantic_tensors,
|
| 379 |
+
)
|
| 380 |
+
# Clone to avoid in-place operation on leaf variable during training
|
| 381 |
+
inputs_embeds = inputs_embeds.clone()
|
| 382 |
+
inputs_embeds[acoustic_input_mask] = speech_features
|
| 383 |
+
|
| 384 |
+
# Forward through the model
|
| 385 |
+
outputs = self.model(
|
| 386 |
+
input_ids=None,
|
| 387 |
+
attention_mask=attention_mask,
|
| 388 |
+
position_ids=position_ids,
|
| 389 |
+
past_key_values=past_key_values,
|
| 390 |
+
inputs_embeds=inputs_embeds,
|
| 391 |
+
use_cache=use_cache,
|
| 392 |
+
output_attentions=output_attentions,
|
| 393 |
+
output_hidden_states=output_hidden_states,
|
| 394 |
+
return_dict=return_dict,
|
| 395 |
+
cache_position=cache_position,
|
| 396 |
+
)
|
| 397 |
+
|
| 398 |
+
hidden_states = outputs[0] if not return_dict else outputs.last_hidden_state
|
| 399 |
+
logits = self.lm_head(hidden_states)
|
| 400 |
+
|
| 401 |
+
loss = None
|
| 402 |
+
if labels is not None:
|
| 403 |
+
# Shift so that tokens < n predict n
|
| 404 |
+
shift_logits = logits[..., :-1, :].contiguous()
|
| 405 |
+
shift_labels = labels[..., 1:].contiguous()
|
| 406 |
+
# Flatten the tokens
|
| 407 |
+
loss_fct = nn.CrossEntropyLoss(ignore_index=-100)
|
| 408 |
+
shift_logits = shift_logits.view(-1, self.vocab_size)
|
| 409 |
+
shift_labels = shift_labels.view(-1)
|
| 410 |
+
# Enable model parallelism
|
| 411 |
+
shift_labels = shift_labels.to(shift_logits.device)
|
| 412 |
+
loss = loss_fct(shift_logits, shift_labels)
|
| 413 |
+
|
| 414 |
+
if not return_dict:
|
| 415 |
+
output = (logits,) + outputs[1:]
|
| 416 |
+
return (loss,) + output if loss is not None else output
|
| 417 |
+
|
| 418 |
+
return VibeVoiceCausalLMOutputWithPast(
|
| 419 |
+
loss=loss,
|
| 420 |
+
logits=logits,
|
| 421 |
+
past_key_values=outputs.past_key_values,
|
| 422 |
+
hidden_states=outputs.hidden_states,
|
| 423 |
+
attentions=outputs.attentions,
|
| 424 |
+
)
|
| 425 |
+
|
| 426 |
+
def prepare_inputs_for_generation(
|
| 427 |
+
self,
|
| 428 |
+
input_ids,
|
| 429 |
+
past_key_values=None,
|
| 430 |
+
attention_mask=None,
|
| 431 |
+
inputs_embeds=None,
|
| 432 |
+
cache_position=None,
|
| 433 |
+
position_ids=None,
|
| 434 |
+
use_cache=True,
|
| 435 |
+
speech_tensors=None,
|
| 436 |
+
speech_masks=None,
|
| 437 |
+
speech_semantic_tensors=None,
|
| 438 |
+
acoustic_input_mask=None,
|
| 439 |
+
**kwargs,
|
| 440 |
+
):
|
| 441 |
+
"""
|
| 442 |
+
Prepare inputs for generation step. This method is called by generate()
|
| 443 |
+
for each token generation step.
|
| 444 |
+
|
| 445 |
+
Following Qwen2-VL's approach: speech inputs are only forwarded on the first pass
|
| 446 |
+
(when cache_position[0] == 0), and are excluded in subsequent generation steps.
|
| 447 |
+
"""
|
| 448 |
+
# If we have past key values, we only need to process the new tokens
|
| 449 |
+
if past_key_values is not None:
|
| 450 |
+
if isinstance(past_key_values, tuple):
|
| 451 |
+
past_length = past_key_values[0][0].shape[2]
|
| 452 |
+
else:
|
| 453 |
+
past_length = past_key_values.get_seq_length()
|
| 454 |
+
|
| 455 |
+
# Keep only the new tokens
|
| 456 |
+
if input_ids is not None and input_ids.shape[1] > past_length:
|
| 457 |
+
input_ids = input_ids[:, past_length:]
|
| 458 |
+
|
| 459 |
+
# Prepare position ids
|
| 460 |
+
if position_ids is None and attention_mask is not None:
|
| 461 |
+
position_ids = attention_mask.long().cumsum(-1) - 1
|
| 462 |
+
position_ids.masked_fill_(attention_mask == 0, 1)
|
| 463 |
+
if past_key_values is not None and input_ids is not None:
|
| 464 |
+
position_ids = position_ids[:, -input_ids.shape[1]:]
|
| 465 |
+
|
| 466 |
+
# Prepare cache position
|
| 467 |
+
if cache_position is None:
|
| 468 |
+
past_seen_tokens = past_key_values.get_seq_length() if past_key_values is not None else 0
|
| 469 |
+
cache_position = torch.arange(
|
| 470 |
+
past_seen_tokens,
|
| 471 |
+
past_seen_tokens + (input_ids.shape[1] if input_ids is not None else inputs_embeds.shape[1]),
|
| 472 |
+
device=input_ids.device if input_ids is not None else inputs_embeds.device
|
| 473 |
+
)
|
| 474 |
+
|
| 475 |
+
# Prepare model inputs
|
| 476 |
+
if inputs_embeds is not None and past_key_values is None:
|
| 477 |
+
model_inputs = {"inputs_embeds": inputs_embeds}
|
| 478 |
+
else:
|
| 479 |
+
model_inputs = {"input_ids": input_ids}
|
| 480 |
+
|
| 481 |
+
model_inputs.update(
|
| 482 |
+
{
|
| 483 |
+
"position_ids": position_ids,
|
| 484 |
+
"cache_position": cache_position,
|
| 485 |
+
"past_key_values": past_key_values,
|
| 486 |
+
"use_cache": use_cache,
|
| 487 |
+
"attention_mask": attention_mask,
|
| 488 |
+
}
|
| 489 |
+
)
|
| 490 |
+
|
| 491 |
+
# Following Qwen2-VL pattern: only include speech inputs on the first forward pass
|
| 492 |
+
# (when cache_position[0] == 0), exclude them in subsequent generation steps
|
| 493 |
+
if cache_position is not None and len(cache_position) > 0 and cache_position[0] == 0:
|
| 494 |
+
# First forward pass - include speech inputs if provided
|
| 495 |
+
model_inputs.update({
|
| 496 |
+
"speech_tensors": speech_tensors,
|
| 497 |
+
"speech_masks": speech_masks,
|
| 498 |
+
"speech_semantic_tensors": speech_semantic_tensors,
|
| 499 |
+
"acoustic_input_mask": acoustic_input_mask,
|
| 500 |
+
})
|
| 501 |
+
else:
|
| 502 |
+
# Subsequent generation steps - exclude speech inputs
|
| 503 |
+
model_inputs.update({
|
| 504 |
+
"speech_tensors": None,
|
| 505 |
+
"speech_masks": None,
|
| 506 |
+
"speech_semantic_tensors": None,
|
| 507 |
+
"acoustic_input_mask": None,
|
| 508 |
+
})
|
| 509 |
+
|
| 510 |
+
# Include any remaining kwargs that might be needed
|
| 511 |
+
model_inputs.update(kwargs)
|
| 512 |
+
|
| 513 |
+
return model_inputs
|
| 514 |
+
|
| 515 |
+
AutoModel.register(VibeVoiceASRConfig, VibeVoiceASRModel)
|
| 516 |
+
AutoModelForCausalLM.register(VibeVoiceASRConfig, VibeVoiceASRForConditionalGeneration)
|
| 517 |
+
|
| 518 |
+
__all__ = [
|
| 519 |
+
"VibeVoiceASRPreTrainedModel",
|
| 520 |
+
"VibeVoiceASRModel",
|
| 521 |
+
"VibeVoiceASRForConditionalGeneration",
|
| 522 |
+
]
|
vibevoice/modular/modeling_vibevoice_streaming.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass
|
| 2 |
+
from typing import Dict, List, Optional, Tuple, Union, Callable
|
| 3 |
+
from tqdm import tqdm
|
| 4 |
+
import copy
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn as nn
|
| 7 |
+
import torch.nn.functional as F
|
| 8 |
+
import torch.distributed as dist
|
| 9 |
+
|
| 10 |
+
from transformers.models.auto import AutoModel, AutoModelForCausalLM
|
| 11 |
+
|
| 12 |
+
from transformers.activations import ACT2FN
|
| 13 |
+
from transformers.modeling_outputs import CausalLMOutput, BaseModelOutputWithPast, ModelOutput
|
| 14 |
+
from transformers.models.llama.modeling_llama import LlamaRMSNorm
|
| 15 |
+
from transformers import modeling_utils
|
| 16 |
+
from transformers.modeling_utils import PreTrainedModel
|
| 17 |
+
from transformers.modeling_flash_attention_utils import FlashAttentionKwargs
|
| 18 |
+
from transformers.utils import logging
|
| 19 |
+
|
| 20 |
+
from .modular_vibevoice_diffusion_head import VibeVoiceDiffusionHead
|
| 21 |
+
from vibevoice.schedule.dpm_solver import DPMSolverMultistepScheduler
|
| 22 |
+
|
| 23 |
+
from .configuration_vibevoice_streaming import VibeVoiceStreamingConfig
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
logger = logging.get_logger(__name__)
|
| 27 |
+
|
| 28 |
+
if not hasattr(modeling_utils, "ALL_PARALLEL_STYLES") or modeling_utils.ALL_PARALLEL_STYLES is None:
|
| 29 |
+
modeling_utils.ALL_PARALLEL_STYLES = ["tp", "none", "colwise", "rowwise"]
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class BinaryClassifier(nn.Module):
|
| 33 |
+
def __init__(self, hidden_size):
|
| 34 |
+
super(BinaryClassifier, self).__init__()
|
| 35 |
+
self.fc1 = nn.Linear(hidden_size, hidden_size)
|
| 36 |
+
self.fc2 = nn.Linear(hidden_size, 1)
|
| 37 |
+
|
| 38 |
+
def forward(self, x):
|
| 39 |
+
x = torch.relu(self.fc1(x))
|
| 40 |
+
x = self.fc2(x)
|
| 41 |
+
return x
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class SpeechConnector(nn.Module):
|
| 45 |
+
def __init__(self, input_dim, output_dim):
|
| 46 |
+
super().__init__()
|
| 47 |
+
self.fc1 = nn.Linear(input_dim, output_dim)
|
| 48 |
+
self.norm = LlamaRMSNorm(output_dim, eps=1e-6)
|
| 49 |
+
self.fc2 = nn.Linear(output_dim, output_dim)
|
| 50 |
+
|
| 51 |
+
def forward(self, features, **kwargs):
|
| 52 |
+
x = self.fc1(features)
|
| 53 |
+
x = self.norm(x)
|
| 54 |
+
x = self.fc2(x)
|
| 55 |
+
return x
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
# @auto_docstring
|
| 59 |
+
class VibeVoiceStreamingPreTrainedModel(PreTrainedModel):
|
| 60 |
+
config_class = VibeVoiceStreamingConfig
|
| 61 |
+
base_model_prefix = "model"
|
| 62 |
+
supports_gradient_checkpointing = True
|
| 63 |
+
_skip_keys_device_placement = "past_key_values"
|
| 64 |
+
_supports_cache_class = True
|
| 65 |
+
_supports_flash_attn_2 = True
|
| 66 |
+
_supports_sdpa = True
|
| 67 |
+
_supports_quantized_cache = True
|
| 68 |
+
_supports_static_cache = True
|
| 69 |
+
_supports_attention_backend = True
|
| 70 |
+
|
| 71 |
+
def _init_weights(self, module):
|
| 72 |
+
if isinstance(module, VibeVoiceDiffusionHead):
|
| 73 |
+
module.initialize_weights()
|
| 74 |
+
return
|
| 75 |
+
|
| 76 |
+
# Use the language model's initializer_range if available
|
| 77 |
+
if hasattr(self.config, 'language_model_config') and hasattr(self.config.language_model_config, 'initializer_range'):
|
| 78 |
+
std = self.config.language_model_config.initializer_range
|
| 79 |
+
elif hasattr(self.config, 'decoder_config') and hasattr(self.config.decoder_config, 'initializer_range'):
|
| 80 |
+
std = self.config.decoder_config.initializer_range
|
| 81 |
+
else:
|
| 82 |
+
std = 0.02 # Default value
|
| 83 |
+
|
| 84 |
+
if isinstance(module, nn.Linear):
|
| 85 |
+
module.weight.data.normal_(mean=0.0, std=std)
|
| 86 |
+
if module.bias is not None:
|
| 87 |
+
module.bias.data.zero_()
|
| 88 |
+
elif isinstance(module, nn.LayerNorm):
|
| 89 |
+
module.weight.data.fill_(1.0)
|
| 90 |
+
module.bias.data.zero_()
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
# @auto_docstring
|
| 94 |
+
class VibeVoiceStreamingModel(VibeVoiceStreamingPreTrainedModel):
|
| 95 |
+
def __init__(self, config):
|
| 96 |
+
super().__init__(config)
|
| 97 |
+
|
| 98 |
+
if hasattr(config, 'torch_dtype') and config.torch_dtype is not None:
|
| 99 |
+
if isinstance(config.torch_dtype, str):
|
| 100 |
+
dtype = getattr(torch, config.torch_dtype)
|
| 101 |
+
else:
|
| 102 |
+
dtype = config.torch_dtype
|
| 103 |
+
else:
|
| 104 |
+
dtype = torch.float32
|
| 105 |
+
|
| 106 |
+
# Initialize Qwen2 model for language modeling.
|
| 107 |
+
# The lower Transformer layers are only used for encoding text, while the upper Transformer layers are used for encoding text and generating speech.
|
| 108 |
+
# To keep the code clean, we constructs two language models.
|
| 109 |
+
# The final norm layer of the first language_model is set to identity and will not be used in inference.
|
| 110 |
+
lm_config = copy.deepcopy(config.decoder_config)
|
| 111 |
+
lm_backbone_num_hidden_layers = getattr(lm_config, 'num_hidden_layers', 24) - config.tts_backbone_num_hidden_layers
|
| 112 |
+
lm_config.num_hidden_layers = lm_backbone_num_hidden_layers
|
| 113 |
+
self.language_model = AutoModel.from_config(lm_config)
|
| 114 |
+
self.language_model.norm = nn.Identity()
|
| 115 |
+
|
| 116 |
+
# We only need the Transformer layers here. Note that embed_tokens in tts_language_model is unused
|
| 117 |
+
tts_lm_config = copy.deepcopy(lm_config)
|
| 118 |
+
tts_lm_config.num_hidden_layers = config.tts_backbone_num_hidden_layers
|
| 119 |
+
self.tts_language_model = AutoModel.from_config(tts_lm_config)
|
| 120 |
+
|
| 121 |
+
# Marks the text that needs to be spoken by the TTS model.
|
| 122 |
+
self.tts_input_types = nn.Embedding(num_embeddings=2, embedding_dim=config.decoder_config.hidden_size)
|
| 123 |
+
|
| 124 |
+
# Initialize speech components if needed
|
| 125 |
+
self.acoustic_tokenizer = AutoModel.from_config(config.acoustic_tokenizer_config).to(dtype)
|
| 126 |
+
self.acoustic_connector = SpeechConnector(config.acoustic_vae_dim, lm_config.hidden_size).to(dtype)
|
| 127 |
+
|
| 128 |
+
# Register scaling factors as buffers - use 1D tensors for FSDP compatibility
|
| 129 |
+
self.register_buffer('speech_scaling_factor', torch.tensor(float('nan')))
|
| 130 |
+
self.register_buffer('speech_bias_factor', torch.tensor(float('nan')))
|
| 131 |
+
|
| 132 |
+
# Initialize prediction head for speech generation
|
| 133 |
+
self.prediction_head = AutoModel.from_config(config.diffusion_head_config).to(dtype)
|
| 134 |
+
|
| 135 |
+
# Initialize noise scheduler
|
| 136 |
+
self.noise_scheduler = DPMSolverMultistepScheduler(
|
| 137 |
+
num_train_timesteps=config.diffusion_head_config.ddpm_num_steps,
|
| 138 |
+
beta_schedule=config.diffusion_head_config.ddpm_beta_schedule,
|
| 139 |
+
prediction_type=config.diffusion_head_config.prediction_type
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
def get_input_embeddings(self):
|
| 143 |
+
if hasattr(self.language_model, 'embed_tokens'):
|
| 144 |
+
# If the language model has an embed_tokens attribute, return it
|
| 145 |
+
return self.language_model.embed_tokens
|
| 146 |
+
|
| 147 |
+
for name, attr in self.language_model.fullmap.items(): # parallel by nnscaler, the name is changed
|
| 148 |
+
if attr.orig_name == 'embed_tokens.weight':
|
| 149 |
+
return getattr(self.language_model, name)
|
| 150 |
+
assert False, 'should not arrive here'
|
| 151 |
+
|
| 152 |
+
def set_input_embeddings(self, value):
|
| 153 |
+
self.language_model.embed_tokens = value
|
| 154 |
+
|
| 155 |
+
def set_speech_tokenizers(self, acoustic_tokenizer=None):
|
| 156 |
+
"""Set the speech tokenizers used for encoding and decoding speech."""
|
| 157 |
+
self.acoustic_tokenizer = acoustic_tokenizer
|
| 158 |
+
|
| 159 |
+
# Reset the encoder to evaluation mode
|
| 160 |
+
if self.acoustic_tokenizer is not None:
|
| 161 |
+
self.acoustic_tokenizer.eval()
|
| 162 |
+
|
| 163 |
+
def forward(self, *args, **kwargs):
|
| 164 |
+
"""
|
| 165 |
+
Intentionally not implemented.
|
| 166 |
+
|
| 167 |
+
This streaming model is split into two explicit submodules:
|
| 168 |
+
- `language_model` for plain text processing (lower layers).
|
| 169 |
+
- `tts_language_model` for TTS-related upper layers.
|
| 170 |
+
|
| 171 |
+
We deliberately avoid a unified `forward` to prevent accidental calls
|
| 172 |
+
that mix responsibilities.
|
| 173 |
+
|
| 174 |
+
To use the model:
|
| 175 |
+
- Call `self.language_model(...)` for text embeddings / hidden states.
|
| 176 |
+
- Call `self.tts_language_model(...)` for the TTS portion.
|
| 177 |
+
- Use the dedicated inference class for combined generation logic.
|
| 178 |
+
"""
|
| 179 |
+
raise RuntimeError(
|
| 180 |
+
"VibeVoiceStreamingModel.forward is intentionally disabled. "
|
| 181 |
+
"Use `model.language_model(...)` or `model.tts_language_model(...)` instead."
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
AutoModel.register(VibeVoiceStreamingConfig, VibeVoiceStreamingModel)
|
| 186 |
+
|
| 187 |
+
__all__ = [
|
| 188 |
+
"VibeVoiceStreamingPreTrainedModel",
|
| 189 |
+
"VibeVoiceStreamingModel",
|
| 190 |
+
]
|
vibevoice/modular/modeling_vibevoice_streaming_inference.py
ADDED
|
@@ -0,0 +1,906 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass
|
| 2 |
+
from typing import Any, Dict, List, Optional, Tuple, Union, Callable
|
| 3 |
+
from tqdm import tqdm
|
| 4 |
+
import inspect
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn as nn
|
| 7 |
+
|
| 8 |
+
from transformers.models.auto import AutoModel, AutoModelForCausalLM
|
| 9 |
+
from transformers.generation import GenerationMixin, GenerationConfig, LogitsProcessor, LogitsProcessorList, StoppingCriteriaList
|
| 10 |
+
from transformers.modeling_outputs import BaseModelOutputWithPast, ModelOutput
|
| 11 |
+
from transformers import modeling_utils
|
| 12 |
+
from transformers.modeling_utils import PreTrainedModel
|
| 13 |
+
from transformers.modeling_flash_attention_utils import FlashAttentionKwargs
|
| 14 |
+
from transformers.utils import logging
|
| 15 |
+
|
| 16 |
+
from .modular_vibevoice_tokenizer import VibeVoiceTokenizerStreamingCache
|
| 17 |
+
from .modular_vibevoice_diffusion_head import VibeVoiceDiffusionHead
|
| 18 |
+
from vibevoice.schedule.dpm_solver import DPMSolverMultistepScheduler
|
| 19 |
+
from .configuration_vibevoice_streaming import VibeVoiceStreamingConfig
|
| 20 |
+
from .modular_vibevoice_text_tokenizer import VibeVoiceTextTokenizer, VibeVoiceTextTokenizerFast
|
| 21 |
+
from .modeling_vibevoice_streaming import VibeVoiceStreamingPreTrainedModel, VibeVoiceStreamingModel, BinaryClassifier
|
| 22 |
+
from .streamer import AudioStreamer, AsyncAudioStreamer
|
| 23 |
+
|
| 24 |
+
logger = logging.get_logger(__name__)
|
| 25 |
+
|
| 26 |
+
if not hasattr(modeling_utils, "ALL_PARALLEL_STYLES") or modeling_utils.ALL_PARALLEL_STYLES is None:
|
| 27 |
+
modeling_utils.ALL_PARALLEL_STYLES = ["tp", "none", "colwise", "rowwise"]
|
| 28 |
+
|
| 29 |
+
TTS_TEXT_WINDOW_SIZE = 5
|
| 30 |
+
TTS_SPEECH_WINDOW_SIZE = 6
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
# ============================================================================
|
| 34 |
+
# Transformers >= 4.57 Compatibility Layer
|
| 35 |
+
# The cache system was refactored in transformers 4.57, requiring these helpers.
|
| 36 |
+
# ============================================================================
|
| 37 |
+
|
| 38 |
+
class MockCacheLayer:
|
| 39 |
+
"""
|
| 40 |
+
Mock cache layer for transformers >= 4.57 compatibility.
|
| 41 |
+
Provides the `layers` interface expected by DynamicCache in newer versions.
|
| 42 |
+
"""
|
| 43 |
+
|
| 44 |
+
def __init__(self, key_cache, value_cache, parent_cache=None, layer_idx=0):
|
| 45 |
+
self.key_cache = key_cache
|
| 46 |
+
self.value_cache = value_cache
|
| 47 |
+
self._parent_cache = parent_cache
|
| 48 |
+
self._layer_idx = layer_idx
|
| 49 |
+
|
| 50 |
+
def get_mask_sizes(self, cache_position):
|
| 51 |
+
"""Return KV length and offset for mask creation."""
|
| 52 |
+
seq_length = self.key_cache.shape[2] if self.key_cache is not None else 0
|
| 53 |
+
query_length = cache_position.shape[0]
|
| 54 |
+
return seq_length + query_length, 0
|
| 55 |
+
|
| 56 |
+
def update(self, key_states, value_states, cache_kwargs=None):
|
| 57 |
+
"""Update the cache with new key/value states."""
|
| 58 |
+
if self._parent_cache is None:
|
| 59 |
+
return self.key_cache, self.value_cache
|
| 60 |
+
|
| 61 |
+
parent = self._parent_cache
|
| 62 |
+
idx = self._layer_idx
|
| 63 |
+
|
| 64 |
+
# Extend cache lists if needed
|
| 65 |
+
while len(parent.key_cache) <= idx:
|
| 66 |
+
parent.key_cache.append(None)
|
| 67 |
+
parent.value_cache.append(None)
|
| 68 |
+
|
| 69 |
+
# Concatenate or initialize cache
|
| 70 |
+
if parent.key_cache[idx] is not None:
|
| 71 |
+
parent.key_cache[idx] = torch.cat([parent.key_cache[idx], key_states], dim=2)
|
| 72 |
+
parent.value_cache[idx] = torch.cat([parent.value_cache[idx], value_states], dim=2)
|
| 73 |
+
else:
|
| 74 |
+
parent.key_cache[idx] = key_states
|
| 75 |
+
parent.value_cache[idx] = value_states
|
| 76 |
+
|
| 77 |
+
# Update local references
|
| 78 |
+
self.key_cache = parent.key_cache[idx]
|
| 79 |
+
self.value_cache = parent.value_cache[idx]
|
| 80 |
+
return self.key_cache, self.value_cache
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def _ensure_cache_has_layers(cache):
|
| 84 |
+
"""
|
| 85 |
+
Ensure the cache has all required attributes for transformers >= 4.57.
|
| 86 |
+
Creates MockCacheLayer wrappers to provide the expected `layers` interface.
|
| 87 |
+
"""
|
| 88 |
+
if cache is None:
|
| 89 |
+
return cache
|
| 90 |
+
|
| 91 |
+
# Add required attributes (skip if read-only)
|
| 92 |
+
for attr, default in [('layer_class_to_replicate', None), ('offloading', False), ('is_compileable', False)]:
|
| 93 |
+
if not hasattr(cache, attr):
|
| 94 |
+
try:
|
| 95 |
+
setattr(cache, attr, default)
|
| 96 |
+
except AttributeError:
|
| 97 |
+
pass
|
| 98 |
+
|
| 99 |
+
# Build layers list from key_cache/value_cache
|
| 100 |
+
if hasattr(cache, 'key_cache') and hasattr(cache, 'value_cache'):
|
| 101 |
+
try:
|
| 102 |
+
cache.layers = [
|
| 103 |
+
MockCacheLayer(cache.key_cache[i], cache.value_cache[i], parent_cache=cache, layer_idx=i)
|
| 104 |
+
for i in range(len(cache.key_cache))
|
| 105 |
+
]
|
| 106 |
+
except AttributeError:
|
| 107 |
+
pass
|
| 108 |
+
elif not hasattr(cache, 'layers'):
|
| 109 |
+
try:
|
| 110 |
+
cache.layers = []
|
| 111 |
+
except AttributeError:
|
| 112 |
+
pass
|
| 113 |
+
|
| 114 |
+
return cache
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def _update_model_kwargs_for_generation(
|
| 118 |
+
outputs: ModelOutput,
|
| 119 |
+
model_kwargs: Dict[str, Any],
|
| 120 |
+
num_new_tokens: int = 1,
|
| 121 |
+
) -> Dict[str, Any]:
|
| 122 |
+
"""
|
| 123 |
+
Update model_kwargs after adding new tokens (supports multi-token windows).
|
| 124 |
+
|
| 125 |
+
Updates past_key_values, attention_mask, and cache_position for the next forward pass.
|
| 126 |
+
"""
|
| 127 |
+
model_kwargs["past_key_values"] = _ensure_cache_has_layers(outputs.past_key_values)
|
| 128 |
+
|
| 129 |
+
attention_mask = model_kwargs["attention_mask"]
|
| 130 |
+
model_kwargs["attention_mask"] = torch.cat(
|
| 131 |
+
[attention_mask, attention_mask.new_ones((attention_mask.shape[0], num_new_tokens))], dim=-1
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
cache_pos = model_kwargs["cache_position"]
|
| 135 |
+
model_kwargs["cache_position"] = torch.arange(
|
| 136 |
+
cache_pos[-1] + 1, cache_pos[-1] + num_new_tokens + 1, device=cache_pos.device
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
return model_kwargs
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
@dataclass
|
| 143 |
+
class VibeVoiceCausalLMOutputWithPast(BaseModelOutputWithPast):
|
| 144 |
+
logits: Optional[torch.FloatTensor] = None
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
@dataclass
|
| 148 |
+
class VibeVoiceGenerationOutput(ModelOutput):
|
| 149 |
+
"""
|
| 150 |
+
Output type for VibeVoice generation.
|
| 151 |
+
|
| 152 |
+
Args:
|
| 153 |
+
sequences (`torch.LongTensor` of shape `(batch_size, sequence_length)`):
|
| 154 |
+
The generated sequences.
|
| 155 |
+
speech_outputs (`List[torch.FloatTensor]`, *optional*):
|
| 156 |
+
List of generated speech waveforms or latents for each speech segment.
|
| 157 |
+
"""
|
| 158 |
+
sequences: torch.LongTensor = None
|
| 159 |
+
speech_outputs: Optional[List[torch.FloatTensor]] = None
|
| 160 |
+
reach_max_step_sample: Optional[torch.BoolTensor] = None
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
class VibeVoiceStreamingForConditionalGenerationInference(VibeVoiceStreamingPreTrainedModel, GenerationMixin):
|
| 164 |
+
|
| 165 |
+
def __init__(self, config):
|
| 166 |
+
super().__init__(config)
|
| 167 |
+
|
| 168 |
+
# Initialize the base model
|
| 169 |
+
self.model = VibeVoiceStreamingModel(config)
|
| 170 |
+
|
| 171 |
+
# TTS generation EOS classifier
|
| 172 |
+
self.tts_eos_classifier = BinaryClassifier(config.decoder_config.hidden_size)
|
| 173 |
+
|
| 174 |
+
# inference configuration
|
| 175 |
+
self.ddpm_inference_steps = config.diffusion_head_config.ddpm_num_inference_steps
|
| 176 |
+
|
| 177 |
+
# Initialize weights and apply final processing
|
| 178 |
+
self.post_init()
|
| 179 |
+
|
| 180 |
+
@property
|
| 181 |
+
def noise_scheduler(self):
|
| 182 |
+
return self.model.noise_scheduler
|
| 183 |
+
|
| 184 |
+
@property
|
| 185 |
+
def prediction_head(self):
|
| 186 |
+
return self.model.prediction_head
|
| 187 |
+
|
| 188 |
+
@property
|
| 189 |
+
def speech_scaling_factor(self):
|
| 190 |
+
return self.model.speech_scaling_factor
|
| 191 |
+
|
| 192 |
+
@property
|
| 193 |
+
def speech_bias_factor(self):
|
| 194 |
+
return self.model.speech_bias_factor
|
| 195 |
+
|
| 196 |
+
@property
|
| 197 |
+
def acoustic_tokenizer(self):
|
| 198 |
+
return self.model.acoustic_tokenizer
|
| 199 |
+
|
| 200 |
+
@property
|
| 201 |
+
def acoustic_connector(self):
|
| 202 |
+
return self.model.acoustic_connector
|
| 203 |
+
|
| 204 |
+
def tie_weights(self):
|
| 205 |
+
"""
|
| 206 |
+
Tie the weights between the input embeddings and the output embeddings.
|
| 207 |
+
"""
|
| 208 |
+
# Tie lm_head.weight to language_model.embed_tokens.weight
|
| 209 |
+
if not getattr(self.config, 'tie_word_embeddings', False):
|
| 210 |
+
return
|
| 211 |
+
|
| 212 |
+
if hasattr(self, 'lm_head') and hasattr(self.model.language_model, 'embed_tokens'):
|
| 213 |
+
self.lm_head.weight = self.model.language_model.embed_tokens.weight
|
| 214 |
+
|
| 215 |
+
def get_input_embeddings(self):
|
| 216 |
+
return self.model.get_input_embeddings()
|
| 217 |
+
|
| 218 |
+
def set_input_embeddings(self, value):
|
| 219 |
+
self.model.set_input_embeddings(value)
|
| 220 |
+
|
| 221 |
+
def get_output_embeddings(self):
|
| 222 |
+
"""
|
| 223 |
+
This model does not define an `lm_head` (vocabulary projection).
|
| 224 |
+
"""
|
| 225 |
+
return None
|
| 226 |
+
|
| 227 |
+
def set_output_embeddings(self, new_embeddings):
|
| 228 |
+
"""
|
| 229 |
+
No-op because there is no `lm_head`. Provided only to satisfy optional API calls.
|
| 230 |
+
To enable, first create `self.lm_head` then allow assignment.
|
| 231 |
+
"""
|
| 232 |
+
raise RuntimeError("Output embeddings (lm_head) are not defined for this model. "
|
| 233 |
+
"Create one before calling set_output_embeddings if needed.")
|
| 234 |
+
|
| 235 |
+
def set_speech_tokenizers(self, acoustic_tokenizer=None):
|
| 236 |
+
"""Set the speech tokenizers used for encoding and decoding speech."""
|
| 237 |
+
self.model.set_speech_tokenizers(acoustic_tokenizer)
|
| 238 |
+
|
| 239 |
+
def set_ddpm_inference_steps(self, num_steps=None):
|
| 240 |
+
self.ddpm_inference_steps = num_steps or self.config.diffusion_head_config.ddpm_num_inference_steps
|
| 241 |
+
|
| 242 |
+
def prepare_inputs_for_generation(
|
| 243 |
+
self,
|
| 244 |
+
input_ids: torch.LongTensor,
|
| 245 |
+
past_key_values=None,
|
| 246 |
+
attention_mask=None,
|
| 247 |
+
inputs_embeds=None,
|
| 248 |
+
cache_position=None,
|
| 249 |
+
**kwargs,
|
| 250 |
+
):
|
| 251 |
+
"""Prepare model inputs for generation (transformers >= 4.57 compatible)."""
|
| 252 |
+
model_inputs = {"cache_position": cache_position}
|
| 253 |
+
|
| 254 |
+
# Slice inputs when using cache
|
| 255 |
+
if past_key_values is not None:
|
| 256 |
+
model_inputs["past_key_values"] = past_key_values
|
| 257 |
+
if inputs_embeds is not None and input_ids.shape[1] == 0:
|
| 258 |
+
inputs_embeds = inputs_embeds[:, -cache_position.shape[0]:]
|
| 259 |
+
elif inputs_embeds is not None or (cache_position is not None and cache_position[-1] >= input_ids.shape[1]):
|
| 260 |
+
input_ids = input_ids[:, -cache_position.shape[0]:]
|
| 261 |
+
elif cache_position is not None and input_ids.shape[1] != cache_position.shape[0]:
|
| 262 |
+
input_ids = input_ids[:, cache_position]
|
| 263 |
+
|
| 264 |
+
# Set input_ids or inputs_embeds
|
| 265 |
+
use_embeds = inputs_embeds is not None and (
|
| 266 |
+
past_key_values is None or (cache_position is not None and len(cache_position) == inputs_embeds.shape[1])
|
| 267 |
+
)
|
| 268 |
+
if use_embeds:
|
| 269 |
+
model_inputs["input_ids"] = None
|
| 270 |
+
model_inputs["inputs_embeds"] = inputs_embeds
|
| 271 |
+
else:
|
| 272 |
+
model_inputs["input_ids"] = input_ids.clone(memory_format=torch.contiguous_format) if input_ids is not None else None
|
| 273 |
+
model_inputs["inputs_embeds"] = None
|
| 274 |
+
|
| 275 |
+
if attention_mask is not None:
|
| 276 |
+
model_inputs["attention_mask"] = attention_mask
|
| 277 |
+
|
| 278 |
+
# Create position_ids from attention_mask
|
| 279 |
+
if attention_mask is not None and kwargs.get("position_ids") is None:
|
| 280 |
+
position_ids = attention_mask.long().cumsum(-1) - 1
|
| 281 |
+
position_ids.masked_fill_(attention_mask == 0, 1)
|
| 282 |
+
kwargs["position_ids"] = position_ids
|
| 283 |
+
|
| 284 |
+
# Slice position_ids when using cache
|
| 285 |
+
if kwargs.get("position_ids") is not None:
|
| 286 |
+
if past_key_values is not None:
|
| 287 |
+
seq_len = model_inputs["inputs_embeds"].shape[1] if model_inputs.get("inputs_embeds") is not None else model_inputs["input_ids"].shape[1]
|
| 288 |
+
model_inputs["position_ids"] = kwargs["position_ids"][:, -seq_len:].clone(memory_format=torch.contiguous_format)
|
| 289 |
+
else:
|
| 290 |
+
model_inputs["position_ids"] = kwargs.pop("position_ids").clone(memory_format=torch.contiguous_format)
|
| 291 |
+
|
| 292 |
+
# Forward remaining kwargs
|
| 293 |
+
for key, value in kwargs.items():
|
| 294 |
+
if key not in model_inputs:
|
| 295 |
+
model_inputs[key] = value
|
| 296 |
+
|
| 297 |
+
model_inputs.pop("labels", None)
|
| 298 |
+
return model_inputs
|
| 299 |
+
|
| 300 |
+
def _update_model_kwargs_for_generation(
|
| 301 |
+
self,
|
| 302 |
+
outputs,
|
| 303 |
+
model_kwargs,
|
| 304 |
+
is_encoder_decoder=False,
|
| 305 |
+
num_new_tokens=1,
|
| 306 |
+
):
|
| 307 |
+
"""Override to ensure cache compatibility with transformers >= 4.57."""
|
| 308 |
+
model_kwargs = super()._update_model_kwargs_for_generation(
|
| 309 |
+
outputs, model_kwargs, is_encoder_decoder=is_encoder_decoder, num_new_tokens=num_new_tokens
|
| 310 |
+
)
|
| 311 |
+
if "past_key_values" in model_kwargs:
|
| 312 |
+
model_kwargs["past_key_values"] = _ensure_cache_has_layers(model_kwargs["past_key_values"])
|
| 313 |
+
return model_kwargs
|
| 314 |
+
|
| 315 |
+
def _init_cache_for_generation(self, generation_config, model_kwargs, batch_size, max_cache_length, device):
|
| 316 |
+
"""
|
| 317 |
+
Initialize cache for generation, handling different transformers versions.
|
| 318 |
+
For transformers >= 4.57, returns None to let the model create the cache dynamically.
|
| 319 |
+
"""
|
| 320 |
+
try:
|
| 321 |
+
from transformers.cache_utils import DynamicCache
|
| 322 |
+
sig = inspect.signature(DynamicCache.__init__)
|
| 323 |
+
if 'config' in sig.parameters:
|
| 324 |
+
# transformers >= 4.57: let model handle cache creation
|
| 325 |
+
return None
|
| 326 |
+
else:
|
| 327 |
+
# Older versions: use parent method
|
| 328 |
+
prep_sig = inspect.signature(self._prepare_cache_for_generation)
|
| 329 |
+
if 'device' in prep_sig.parameters:
|
| 330 |
+
self._prepare_cache_for_generation(generation_config, model_kwargs, None, batch_size, max_cache_length, device)
|
| 331 |
+
else:
|
| 332 |
+
self._prepare_cache_for_generation(generation_config, model_kwargs, None, batch_size, max_cache_length)
|
| 333 |
+
return model_kwargs.get("past_key_values")
|
| 334 |
+
except Exception:
|
| 335 |
+
return None
|
| 336 |
+
|
| 337 |
+
# @can_return_tuple
|
| 338 |
+
def forward_lm(
|
| 339 |
+
self,
|
| 340 |
+
input_ids: torch.LongTensor = None,
|
| 341 |
+
attention_mask: Optional[torch.Tensor] = None,
|
| 342 |
+
position_ids: Optional[torch.LongTensor] = None,
|
| 343 |
+
past_key_values: Optional[Tuple[Tuple[torch.FloatTensor]]] = None,
|
| 344 |
+
inputs_embeds: Optional[torch.FloatTensor] = None,
|
| 345 |
+
labels: Optional[torch.LongTensor] = None,
|
| 346 |
+
use_cache: Optional[bool] = None,
|
| 347 |
+
output_attentions: Optional[bool] = None,
|
| 348 |
+
output_hidden_states: Optional[bool] = None,
|
| 349 |
+
return_dict: Optional[bool] = None,
|
| 350 |
+
cache_position: Optional[torch.LongTensor] = None,
|
| 351 |
+
**kwargs,
|
| 352 |
+
) -> Union[Tuple, BaseModelOutputWithPast]:
|
| 353 |
+
"""
|
| 354 |
+
Single pass of the base text LM.
|
| 355 |
+
|
| 356 |
+
- Builds embeddings if `inputs_embeds` not provided.
|
| 357 |
+
- Uses (and returns) `past_key_values` when `use_cache=True`.
|
| 358 |
+
- No loss / no lm_head / no speech logic.
|
| 359 |
+
|
| 360 |
+
Args:
|
| 361 |
+
input_ids: (B, S) token ids.
|
| 362 |
+
attention_mask: (B, S) mask.
|
| 363 |
+
past_key_values: cache from previous steps.
|
| 364 |
+
cache_position: positions for cached tokens.
|
| 365 |
+
labels: unsupported (will raise).
|
| 366 |
+
|
| 367 |
+
Returns:
|
| 368 |
+
BaseModelOutputWithPast with `last_hidden_state` and `past_key_values`.
|
| 369 |
+
"""
|
| 370 |
+
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
|
| 371 |
+
|
| 372 |
+
# Get embeddings
|
| 373 |
+
if inputs_embeds is None:
|
| 374 |
+
inputs_embeds = self.model.get_input_embeddings()(input_ids)
|
| 375 |
+
|
| 376 |
+
outputs = self.model.language_model(
|
| 377 |
+
inputs_embeds=inputs_embeds,
|
| 378 |
+
attention_mask=attention_mask,
|
| 379 |
+
position_ids=position_ids,
|
| 380 |
+
past_key_values=past_key_values,
|
| 381 |
+
use_cache=use_cache,
|
| 382 |
+
output_attentions=output_attentions,
|
| 383 |
+
output_hidden_states=output_hidden_states,
|
| 384 |
+
return_dict=return_dict,
|
| 385 |
+
cache_position=cache_position,
|
| 386 |
+
**kwargs,
|
| 387 |
+
)
|
| 388 |
+
|
| 389 |
+
hidden_states = outputs[0] if not return_dict else outputs.last_hidden_state
|
| 390 |
+
|
| 391 |
+
if labels is not None:
|
| 392 |
+
raise NotImplementedError("Loss computation is not implemented in this version.")
|
| 393 |
+
|
| 394 |
+
return BaseModelOutputWithPast(
|
| 395 |
+
past_key_values=outputs.past_key_values,
|
| 396 |
+
last_hidden_state=hidden_states,
|
| 397 |
+
attentions=outputs.attentions,
|
| 398 |
+
)
|
| 399 |
+
|
| 400 |
+
# @can_return_tuple
|
| 401 |
+
def forward_tts_lm(
|
| 402 |
+
self,
|
| 403 |
+
input_ids: torch.LongTensor = None,
|
| 404 |
+
attention_mask: Optional[torch.Tensor] = None,
|
| 405 |
+
position_ids: Optional[torch.LongTensor] = None,
|
| 406 |
+
past_key_values: Optional[Tuple[Tuple[torch.FloatTensor]]] = None,
|
| 407 |
+
inputs_embeds: Optional[torch.FloatTensor] = None,
|
| 408 |
+
labels: Optional[torch.LongTensor] = None,
|
| 409 |
+
use_cache: Optional[bool] = None,
|
| 410 |
+
output_attentions: Optional[bool] = None,
|
| 411 |
+
output_hidden_states: Optional[bool] = None,
|
| 412 |
+
return_dict: Optional[bool] = None,
|
| 413 |
+
cache_position: Optional[torch.LongTensor] = None,
|
| 414 |
+
lm_last_hidden_state: Optional[torch.FloatTensor] = None,
|
| 415 |
+
tts_text_masks: Optional[torch.BoolTensor] = None,
|
| 416 |
+
**kwargs,
|
| 417 |
+
) -> Union[Tuple, VibeVoiceCausalLMOutputWithPast]:
|
| 418 |
+
"""
|
| 419 |
+
Single pass of the TTS LM.
|
| 420 |
+
|
| 421 |
+
- Overwrites tail embeddings with `lm_last_hidden_state`.
|
| 422 |
+
- Adds type embedding via `tts_text_masks` (1=text, 0=speech).
|
| 423 |
+
- Predicts EOS from last hidden state (binary classifier).
|
| 424 |
+
- No loss / no full acoustic decoding here.
|
| 425 |
+
|
| 426 |
+
Args:
|
| 427 |
+
input_ids: (B, S) token ids.
|
| 428 |
+
attention_mask: (B, S) mask.
|
| 429 |
+
lm_last_hidden_state: (B, K, H) hidden states to splice into the tail.
|
| 430 |
+
tts_text_masks: (B, 1) mask marking current position as text(1)/speech(0).
|
| 431 |
+
past_key_values: cache from previous TTS steps.
|
| 432 |
+
cache_position: positions for cached tokens.
|
| 433 |
+
labels: unsupported (will raise).
|
| 434 |
+
|
| 435 |
+
Returns:
|
| 436 |
+
VibeVoiceCausalLMOutputWithPast with `logits` (EOS), `last_hidden_state`, `past_key_values`.
|
| 437 |
+
"""
|
| 438 |
+
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
|
| 439 |
+
|
| 440 |
+
# Get embeddings
|
| 441 |
+
if inputs_embeds is None:
|
| 442 |
+
# Will be replaced with lm_last_hidden_state
|
| 443 |
+
inputs_embeds = self.model.get_input_embeddings()(input_ids)
|
| 444 |
+
|
| 445 |
+
# Replace the last part of inputs_embeds with lm_last_hidden_state
|
| 446 |
+
start_idx = inputs_embeds.shape[1] - lm_last_hidden_state.shape[1]
|
| 447 |
+
inputs_embeds[:, start_idx:, :] = lm_last_hidden_state
|
| 448 |
+
|
| 449 |
+
# Adds type embedding via `tts_text_masks`.
|
| 450 |
+
inputs_embeds = inputs_embeds + self.model.tts_input_types(tts_text_masks.long())
|
| 451 |
+
|
| 452 |
+
outputs = self.model.tts_language_model(
|
| 453 |
+
inputs_embeds=inputs_embeds,
|
| 454 |
+
attention_mask=attention_mask,
|
| 455 |
+
position_ids=position_ids,
|
| 456 |
+
past_key_values=past_key_values,
|
| 457 |
+
use_cache=use_cache,
|
| 458 |
+
output_attentions=output_attentions,
|
| 459 |
+
output_hidden_states=output_hidden_states,
|
| 460 |
+
return_dict=return_dict,
|
| 461 |
+
cache_position=cache_position,
|
| 462 |
+
**kwargs,
|
| 463 |
+
)
|
| 464 |
+
|
| 465 |
+
hidden_states = outputs[0] if not return_dict else outputs.last_hidden_state
|
| 466 |
+
logits = self.tts_eos_classifier(hidden_states[:, -1, :])
|
| 467 |
+
|
| 468 |
+
if labels is not None:
|
| 469 |
+
raise NotImplementedError("Loss computation is not implemented in this version.")
|
| 470 |
+
|
| 471 |
+
return VibeVoiceCausalLMOutputWithPast(
|
| 472 |
+
logits=logits,
|
| 473 |
+
past_key_values=outputs.past_key_values,
|
| 474 |
+
last_hidden_state=hidden_states,
|
| 475 |
+
attentions=outputs.attentions,
|
| 476 |
+
)
|
| 477 |
+
|
| 478 |
+
def forward(self, *args, **kwargs):
|
| 479 |
+
"""
|
| 480 |
+
Unified forward is intentionally disabled.
|
| 481 |
+
|
| 482 |
+
Reasons:
|
| 483 |
+
1. The inference pipeline is staged: base text LM, then TTS LM, plus streaming & diffusion handled in `generate`.
|
| 484 |
+
2. A monolithic call would hide required sequencing (prefill, window stepping, speech diffusion sampling).
|
| 485 |
+
|
| 486 |
+
Use instead:
|
| 487 |
+
- self.forward_lm(...) for a base text LM step (prefill or incremental).
|
| 488 |
+
- self.forward_tts_lm(...) for a single TTS LM step (needs LM hidden states).
|
| 489 |
+
- self.generate(...) for full streaming (text + speech + diffusion + audio assembly).
|
| 490 |
+
|
| 491 |
+
Raises:
|
| 492 |
+
RuntimeError: Always (by design).
|
| 493 |
+
"""
|
| 494 |
+
raise RuntimeError(
|
| 495 |
+
"Unified forward is disabled. Use `forward_lm`, `forward_tts_lm`, or `generate` instead."
|
| 496 |
+
)
|
| 497 |
+
|
| 498 |
+
def _build_generate_config_model_kwargs(self, generation_config, inputs, tokenizer, return_processors=False, **kwargs):
|
| 499 |
+
if generation_config is None:
|
| 500 |
+
generation_config = GenerationConfig(
|
| 501 |
+
bos_token_id=tokenizer.bos_token_id,
|
| 502 |
+
eos_token_id=tokenizer.eos_token_id,
|
| 503 |
+
pad_token_id = tokenizer.pad_token_id
|
| 504 |
+
)
|
| 505 |
+
else:
|
| 506 |
+
generation_config = GenerationConfig(
|
| 507 |
+
**generation_config,
|
| 508 |
+
bos_token_id=tokenizer.bos_token_id,
|
| 509 |
+
eos_token_id=tokenizer.eos_token_id,
|
| 510 |
+
pad_token_id = tokenizer.pad_token_id
|
| 511 |
+
)
|
| 512 |
+
|
| 513 |
+
generation_config, model_kwargs = self._prepare_generation_config(
|
| 514 |
+
generation_config,
|
| 515 |
+
True,
|
| 516 |
+
speech_start_id=tokenizer.speech_start_id,
|
| 517 |
+
speech_end_id=tokenizer.speech_end_id,
|
| 518 |
+
speech_diffusion_id=tokenizer.speech_diffusion_id,
|
| 519 |
+
**kwargs
|
| 520 |
+
)
|
| 521 |
+
generation_config.speech_start_id = tokenizer.speech_start_id
|
| 522 |
+
generation_config.speech_end_id = tokenizer.speech_end_id
|
| 523 |
+
generation_config.speech_diffusion_id = tokenizer.speech_diffusion_id
|
| 524 |
+
|
| 525 |
+
inputs_tensor, model_input_name, model_kwargs = self._prepare_model_inputs(inputs, generation_config.bos_token_id, model_kwargs)
|
| 526 |
+
batch_size = inputs_tensor.shape[0]
|
| 527 |
+
device = self.device
|
| 528 |
+
|
| 529 |
+
self._prepare_special_tokens(generation_config, True, device=device)
|
| 530 |
+
generation_config.use_cache = True
|
| 531 |
+
model_kwargs["use_cache"] = generation_config.use_cache
|
| 532 |
+
input_ids = inputs_tensor.to(self.device)
|
| 533 |
+
|
| 534 |
+
input_ids_length = input_ids.shape[1]
|
| 535 |
+
has_default_max_length = kwargs.get("max_length") is None and generation_config.max_length is not None
|
| 536 |
+
has_default_min_length = kwargs.get("min_length") is None and generation_config.min_length is not None
|
| 537 |
+
generation_config = self._prepare_generated_length(
|
| 538 |
+
generation_config=generation_config,
|
| 539 |
+
has_default_max_length=has_default_max_length,
|
| 540 |
+
has_default_min_length=has_default_min_length,
|
| 541 |
+
model_input_name=model_input_name,
|
| 542 |
+
inputs_tensor=inputs_tensor,
|
| 543 |
+
input_ids_length=input_ids_length,
|
| 544 |
+
)
|
| 545 |
+
|
| 546 |
+
max_cache_length = generation_config.max_length - 1
|
| 547 |
+
# Handle cache initialization for different transformers versions
|
| 548 |
+
model_kwargs["past_key_values"] = self._init_cache_for_generation(
|
| 549 |
+
generation_config, model_kwargs, batch_size, max_cache_length, device
|
| 550 |
+
)
|
| 551 |
+
model_kwargs['cache_position'] = torch.arange(input_ids_length, device=device, dtype=torch.long)
|
| 552 |
+
for k, v in model_kwargs.items():
|
| 553 |
+
if isinstance(v, torch.Tensor):
|
| 554 |
+
model_kwargs[k] = v.to(device=device)
|
| 555 |
+
|
| 556 |
+
if return_processors:
|
| 557 |
+
logits_processor = self._get_logits_processor(
|
| 558 |
+
generation_config=generation_config,
|
| 559 |
+
input_ids_seq_length=input_ids_length,
|
| 560 |
+
encoder_input_ids=inputs_tensor,
|
| 561 |
+
prefix_allowed_tokens_fn=None,
|
| 562 |
+
logits_processor=LogitsProcessorList(),
|
| 563 |
+
device=inputs_tensor.device,
|
| 564 |
+
model_kwargs=model_kwargs,
|
| 565 |
+
)
|
| 566 |
+
|
| 567 |
+
stopping_criteria = self._get_stopping_criteria(generation_config=generation_config, stopping_criteria=StoppingCriteriaList())
|
| 568 |
+
|
| 569 |
+
return generation_config, model_kwargs, input_ids, logits_processor, stopping_criteria
|
| 570 |
+
else:
|
| 571 |
+
return generation_config, model_kwargs, input_ids
|
| 572 |
+
|
| 573 |
+
@torch.no_grad()
|
| 574 |
+
def generate(
|
| 575 |
+
self,
|
| 576 |
+
inputs: Optional[torch.Tensor] = None,
|
| 577 |
+
generation_config: Optional[GenerationConfig] = None,
|
| 578 |
+
logits_processor: Optional[LogitsProcessorList] = None,
|
| 579 |
+
stopping_criteria: Optional[StoppingCriteriaList] = None,
|
| 580 |
+
prefix_allowed_tokens_fn: Optional[Callable[[int, torch.Tensor], List[int]]] = None,
|
| 581 |
+
synced_gpus: Optional[bool] = None,
|
| 582 |
+
assistant_model: Optional["PreTrainedModel"] = None,
|
| 583 |
+
audio_streamer: Optional[Union[AudioStreamer, AsyncAudioStreamer]] = None,
|
| 584 |
+
negative_prompt_ids: Optional[torch.Tensor] = None,
|
| 585 |
+
negative_prompt_attention_mask: Optional[torch.Tensor] = None,
|
| 586 |
+
speech_tensors: Optional[torch.FloatTensor] = None,
|
| 587 |
+
speech_masks: Optional[torch.BoolTensor] = None,
|
| 588 |
+
speech_input_mask: Optional[torch.BoolTensor] = None,
|
| 589 |
+
tts_text_ids: Optional[torch.LongTensor] = None,
|
| 590 |
+
return_speech: bool = True,
|
| 591 |
+
cfg_scale: float = 1.0,
|
| 592 |
+
stop_check_fn: Optional[Callable[[], bool]] = None,
|
| 593 |
+
**kwargs,
|
| 594 |
+
) -> Union[torch.LongTensor, VibeVoiceGenerationOutput]:
|
| 595 |
+
"""
|
| 596 |
+
Text is fed in small windows (dynamic slicing of `tts_text_ids`), which enables streaming text input: you don’t need the full text upfront. After each text window, a loop samples several speech latents (diffusion). The interleaved text encoding + speech generation enables streaming text input and realtime speech output.
|
| 597 |
+
The function only supports batch size = 1 currently.
|
| 598 |
+
|
| 599 |
+
- Windowed text prefill → incremental LM + TTS LM updates.
|
| 600 |
+
- Interleave speech token diffusion sampling (`sample_speech_tokens`).
|
| 601 |
+
- Stops on EOS (binary classifier) or max length / external `stop_check_fn`.
|
| 602 |
+
- Returns final token `sequences` and (optionally) concatenated speech audio.
|
| 603 |
+
|
| 604 |
+
Args (selected):
|
| 605 |
+
tts_text_ids: Full text tokens to stream in windows.
|
| 606 |
+
audio_streamer: If provided, emits audio chunks during generation.
|
| 607 |
+
cfg_scale: Classifier-free guidance scale for speech diffusion.
|
| 608 |
+
return_speech: If False, skips audio decode concatenation.
|
| 609 |
+
stop_check_fn: External early-stop hook (returns True to halt).
|
| 610 |
+
|
| 611 |
+
Returns:
|
| 612 |
+
VibeVoiceGenerationOutput with:
|
| 613 |
+
- sequences: final token ids
|
| 614 |
+
- speech_outputs: list of concatenated audio tensors (or None)
|
| 615 |
+
- reach_max_step_sample: flags for samples stopped by max length
|
| 616 |
+
"""
|
| 617 |
+
# 1. Handle `generation_config` and kwargs that might update it, and validate the `.generate()` call
|
| 618 |
+
tokenizer = kwargs.pop("tokenizer", None)
|
| 619 |
+
neg_text_input_id = tokenizer.convert_tokens_to_ids("<|image_pad|>")
|
| 620 |
+
|
| 621 |
+
tts_lm_input_ids = kwargs.pop("tts_lm_input_ids", None)
|
| 622 |
+
tts_lm_attention_mask = kwargs.pop("tts_lm_attention_mask", None)
|
| 623 |
+
# all_prefilled_outputs: cached prefilled prompt outputs for lm, tts_lm, neg_lm, neg_tts_lm
|
| 624 |
+
all_prefilled_outputs = kwargs.pop("all_prefilled_outputs", None)
|
| 625 |
+
tts_text_ids = tts_text_ids.to(self.device)
|
| 626 |
+
|
| 627 |
+
if kwargs.get('max_new_tokens', None) is None:
|
| 628 |
+
kwargs['max_new_tokens'] = self.config.decoder_config.max_position_embeddings - tts_lm_input_ids.shape[-1]
|
| 629 |
+
|
| 630 |
+
generation_config, model_kwargs, input_ids, logits_processor, stopping_criteria = self._build_generate_config_model_kwargs(
|
| 631 |
+
generation_config, inputs, tokenizer, return_processors=True, **kwargs
|
| 632 |
+
)
|
| 633 |
+
|
| 634 |
+
negative_kwargs = {
|
| 635 |
+
'input_ids': torch.full((kwargs['input_ids'].shape[0], 1), neg_text_input_id, dtype=torch.long, device=kwargs['input_ids'].device),
|
| 636 |
+
'attention_mask': torch.ones((kwargs['input_ids'].shape[0], 1), dtype=torch.long, device=kwargs['input_ids'].device),
|
| 637 |
+
'max_new_tokens': kwargs.get('max_new_tokens', 100)
|
| 638 |
+
}
|
| 639 |
+
negative_generation_config, negative_model_kwargs, negative_input_ids = self._build_generate_config_model_kwargs(
|
| 640 |
+
None, None, tokenizer, return_processors=False, **negative_kwargs
|
| 641 |
+
)
|
| 642 |
+
|
| 643 |
+
tts_lm_kwargs = {
|
| 644 |
+
'input_ids': tts_lm_input_ids,
|
| 645 |
+
'attention_mask': tts_lm_attention_mask,
|
| 646 |
+
'max_new_tokens': kwargs.get('max_new_tokens', 100)
|
| 647 |
+
}
|
| 648 |
+
tts_lm_generation_config, tts_lm_model_kwargs, tts_lm_input_ids = self._build_generate_config_model_kwargs(
|
| 649 |
+
None, None, tokenizer, return_processors=False, **tts_lm_kwargs
|
| 650 |
+
)
|
| 651 |
+
|
| 652 |
+
tts_lm_negative_kwargs = {
|
| 653 |
+
'input_ids': torch.full((kwargs['input_ids'].shape[0], 1), neg_text_input_id, dtype=torch.long, device=kwargs['input_ids'].device),
|
| 654 |
+
'attention_mask': torch.ones((kwargs['input_ids'].shape[0], 1), dtype=torch.long, device=kwargs['input_ids'].device),
|
| 655 |
+
'max_new_tokens': kwargs.get('max_new_tokens', 100)
|
| 656 |
+
}
|
| 657 |
+
tts_lm_negative_generation_config, tts_lm_negative_model_kwargs, tts_lm_negative_input_ids = self._build_generate_config_model_kwargs(
|
| 658 |
+
None, None, tokenizer, return_processors=False, **tts_lm_negative_kwargs
|
| 659 |
+
)
|
| 660 |
+
|
| 661 |
+
acoustic_cache = VibeVoiceTokenizerStreamingCache()
|
| 662 |
+
batch_size = input_ids.shape[0]
|
| 663 |
+
assert batch_size == 1, "Currently only supports batch size == 1"
|
| 664 |
+
device = input_ids.device
|
| 665 |
+
finished_tags = torch.zeros(batch_size, dtype=torch.bool, device=device)
|
| 666 |
+
verbose = kwargs.get("verbose", False)
|
| 667 |
+
|
| 668 |
+
# Initialize audio chunks storage for each sample
|
| 669 |
+
audio_chunks = [[] for _ in range(batch_size)]
|
| 670 |
+
tts_text_window_index = 0
|
| 671 |
+
reach_max_step_sample = torch.zeros(batch_size, dtype=torch.bool, device=device)
|
| 672 |
+
first_text_window_size = TTS_TEXT_WINDOW_SIZE if tts_text_ids.shape[1] >= TTS_TEXT_WINDOW_SIZE else tts_text_ids.shape[1]
|
| 673 |
+
|
| 674 |
+
outputs = all_prefilled_outputs["lm"]
|
| 675 |
+
tts_lm_outputs = all_prefilled_outputs["tts_lm"]
|
| 676 |
+
negative_outputs = all_prefilled_outputs["neg_lm"]
|
| 677 |
+
tts_lm_negative_outputs = all_prefilled_outputs["neg_tts_lm"]
|
| 678 |
+
|
| 679 |
+
model_kwargs = _update_model_kwargs_for_generation(
|
| 680 |
+
outputs, model_kwargs, num_new_tokens=first_text_window_size,
|
| 681 |
+
)
|
| 682 |
+
tts_lm_model_kwargs = _update_model_kwargs_for_generation(
|
| 683 |
+
tts_lm_outputs, tts_lm_model_kwargs, num_new_tokens=first_text_window_size,
|
| 684 |
+
)
|
| 685 |
+
negative_model_kwargs = self._update_model_kwargs_for_generation(
|
| 686 |
+
negative_outputs, negative_model_kwargs, is_encoder_decoder=False,
|
| 687 |
+
)
|
| 688 |
+
tts_lm_negative_model_kwargs = self._update_model_kwargs_for_generation(
|
| 689 |
+
tts_lm_negative_outputs, tts_lm_negative_model_kwargs, is_encoder_decoder=False,
|
| 690 |
+
)
|
| 691 |
+
|
| 692 |
+
step = tts_lm_input_ids.shape[1]
|
| 693 |
+
total_generated_speech_tokens = 0
|
| 694 |
+
total_prefilled_text_tokens = 0
|
| 695 |
+
if kwargs.get("show_progress_bar", True):
|
| 696 |
+
progress_bar = tqdm(
|
| 697 |
+
total=tts_lm_generation_config.max_length,
|
| 698 |
+
desc=f"Prefilled {step} tokens, current step ({step} / {tts_lm_generation_config.max_length})",
|
| 699 |
+
initial=step,
|
| 700 |
+
leave=False
|
| 701 |
+
)
|
| 702 |
+
else:
|
| 703 |
+
progress_bar = None
|
| 704 |
+
|
| 705 |
+
while True:
|
| 706 |
+
# Check for external stop signal
|
| 707 |
+
if stop_check_fn is not None and stop_check_fn():
|
| 708 |
+
if verbose:
|
| 709 |
+
print(f"Generation stopped externally at step {step + 1}")
|
| 710 |
+
# End the audio streamer if it exists
|
| 711 |
+
if audio_streamer is not None:
|
| 712 |
+
audio_streamer.end()
|
| 713 |
+
break
|
| 714 |
+
|
| 715 |
+
# # Check if audio_streamer has been ended (stopped externally)
|
| 716 |
+
# if audio_streamer is not None and hasattr(audio_streamer, 'finished_flags'):
|
| 717 |
+
# if any(audio_streamer.finished_flags):
|
| 718 |
+
# if verbose:
|
| 719 |
+
# print(f"Audio generation stopped externally at step {step + 1}")
|
| 720 |
+
# break
|
| 721 |
+
|
| 722 |
+
if finished_tags.all():
|
| 723 |
+
if hasattr(progress_bar, 'set_description'):
|
| 724 |
+
progress_bar.set_description("Generation complete")
|
| 725 |
+
break
|
| 726 |
+
|
| 727 |
+
cur_input_tts_text_ids = tts_text_ids[:, tts_text_window_index*TTS_TEXT_WINDOW_SIZE:(tts_text_window_index+1)*TTS_TEXT_WINDOW_SIZE]
|
| 728 |
+
next_text_window_size = tts_text_ids[:, (tts_text_window_index+1)*TTS_TEXT_WINDOW_SIZE:(tts_text_window_index+2)*TTS_TEXT_WINDOW_SIZE].shape[1]
|
| 729 |
+
tts_text_window_index += 1
|
| 730 |
+
|
| 731 |
+
if cur_input_tts_text_ids.shape[1] > 0:
|
| 732 |
+
input_ids = torch.cat([input_ids, cur_input_tts_text_ids], dim=-1)
|
| 733 |
+
tts_lm_input_ids = torch.cat([tts_lm_input_ids, cur_input_tts_text_ids], dim=-1)
|
| 734 |
+
|
| 735 |
+
if tts_lm_input_ids.shape[1] > tts_lm_generation_config.max_length:
|
| 736 |
+
if verbose:
|
| 737 |
+
print(f"Reached maximum generation length {generation_config.max_length}, stopped it.")
|
| 738 |
+
reached_samples = torch.arange(batch_size, device=device)[~finished_tags]
|
| 739 |
+
if reached_samples.numel() > 0:
|
| 740 |
+
reach_max_step_sample[reached_samples] = True
|
| 741 |
+
break
|
| 742 |
+
|
| 743 |
+
step += cur_input_tts_text_ids.shape[1]
|
| 744 |
+
total_prefilled_text_tokens += cur_input_tts_text_ids.shape[1]
|
| 745 |
+
if progress_bar is not None:
|
| 746 |
+
progress_bar.update(cur_input_tts_text_ids.shape[1])
|
| 747 |
+
progress_bar.set_description(f"Prefilled {total_prefilled_text_tokens} text tokens, generated {total_generated_speech_tokens} speech tokens, current step ({step} / {tts_lm_generation_config.max_length})")
|
| 748 |
+
|
| 749 |
+
model_inputs = self.prepare_inputs_for_generation(input_ids, **model_kwargs)
|
| 750 |
+
# Forward pass through the model
|
| 751 |
+
outputs = self.forward_lm(
|
| 752 |
+
**model_inputs, return_dict=True, output_attentions=False, output_hidden_states=False,
|
| 753 |
+
)
|
| 754 |
+
model_kwargs = _update_model_kwargs_for_generation(
|
| 755 |
+
outputs, model_kwargs, num_new_tokens=next_text_window_size,
|
| 756 |
+
)
|
| 757 |
+
|
| 758 |
+
tts_lm_model_inputs = self.prepare_inputs_for_generation(tts_lm_input_ids, **tts_lm_model_kwargs)
|
| 759 |
+
tts_lm_additional_inputs = {
|
| 760 |
+
"tts_text_masks": torch.ones_like(tts_lm_input_ids[:, -1:]),
|
| 761 |
+
"lm_last_hidden_state": outputs.last_hidden_state,
|
| 762 |
+
}
|
| 763 |
+
# Forward pass through the model
|
| 764 |
+
tts_lm_outputs = self.forward_tts_lm(
|
| 765 |
+
**tts_lm_model_inputs, **tts_lm_additional_inputs, return_dict=True, output_attentions=False, output_hidden_states=False,
|
| 766 |
+
)
|
| 767 |
+
tts_lm_model_kwargs = self._update_model_kwargs_for_generation(
|
| 768 |
+
tts_lm_outputs, tts_lm_model_kwargs, is_encoder_decoder=False,
|
| 769 |
+
)
|
| 770 |
+
|
| 771 |
+
diffusion_indices = torch.LongTensor([0])
|
| 772 |
+
for cur_speech_index in range(TTS_SPEECH_WINDOW_SIZE):
|
| 773 |
+
positive_condition = tts_lm_outputs.last_hidden_state[diffusion_indices, -1, :]
|
| 774 |
+
negative_condition = tts_lm_negative_outputs.last_hidden_state[diffusion_indices, -1, :]
|
| 775 |
+
|
| 776 |
+
speech_latent = self.sample_speech_tokens(
|
| 777 |
+
positive_condition,
|
| 778 |
+
negative_condition,
|
| 779 |
+
cfg_scale=cfg_scale,
|
| 780 |
+
).unsqueeze(1)
|
| 781 |
+
|
| 782 |
+
# Decode acoustic latent to audio using acoustic streaming cache
|
| 783 |
+
scaled_latent = speech_latent / self.model.speech_scaling_factor.to(speech_latent.device) - self.model.speech_bias_factor.to(speech_latent.device)
|
| 784 |
+
audio_chunk = self.model.acoustic_tokenizer.decode(
|
| 785 |
+
scaled_latent.to(self.model.acoustic_tokenizer.device),
|
| 786 |
+
cache=acoustic_cache, # Use acoustic-specific cache
|
| 787 |
+
sample_indices=diffusion_indices.to(self.model.acoustic_tokenizer.device),
|
| 788 |
+
use_cache=True,
|
| 789 |
+
debug=False
|
| 790 |
+
)
|
| 791 |
+
|
| 792 |
+
# Store audio chunks for each sample
|
| 793 |
+
for i, sample_idx in enumerate(diffusion_indices):
|
| 794 |
+
idx = sample_idx.item()
|
| 795 |
+
# Only append audio chunk if the sample is not finished
|
| 796 |
+
if not finished_tags[idx]:
|
| 797 |
+
audio_chunks[idx].append(audio_chunk[i])
|
| 798 |
+
|
| 799 |
+
# Add streaming support here
|
| 800 |
+
if audio_streamer is not None:
|
| 801 |
+
# Stream the audio chunks immediately
|
| 802 |
+
audio_streamer.put(audio_chunk, diffusion_indices)
|
| 803 |
+
|
| 804 |
+
acoustic_embed = self.model.acoustic_connector(speech_latent)
|
| 805 |
+
tts_lm_input_ids = torch.cat([tts_lm_input_ids, torch.ones_like(tts_lm_input_ids[:, -1:])], dim=-1)
|
| 806 |
+
|
| 807 |
+
if tts_lm_input_ids.shape[1] > tts_lm_generation_config.max_length:
|
| 808 |
+
break
|
| 809 |
+
|
| 810 |
+
step += 1
|
| 811 |
+
total_generated_speech_tokens += 1
|
| 812 |
+
if progress_bar is not None:
|
| 813 |
+
progress_bar.update(1)
|
| 814 |
+
progress_bar.set_description(f"Prefilled {total_prefilled_text_tokens} text tokens, generated {total_generated_speech_tokens} speech tokens, current step ({step} / {tts_lm_generation_config.max_length})")
|
| 815 |
+
|
| 816 |
+
tts_lm_model_inputs = self.prepare_inputs_for_generation(tts_lm_input_ids, **tts_lm_model_kwargs)
|
| 817 |
+
tts_lm_additional_inputs = {
|
| 818 |
+
"tts_text_masks": torch.zeros_like(tts_lm_input_ids[:, -1:]),
|
| 819 |
+
"lm_last_hidden_state": acoustic_embed,
|
| 820 |
+
}
|
| 821 |
+
# Forward pass through the model
|
| 822 |
+
tts_lm_outputs = self.forward_tts_lm(
|
| 823 |
+
**tts_lm_model_inputs, **tts_lm_additional_inputs, return_dict=True, output_attentions=False, output_hidden_states=False,
|
| 824 |
+
)
|
| 825 |
+
if cur_speech_index == TTS_SPEECH_WINDOW_SIZE - 1 and next_text_window_size > 0:
|
| 826 |
+
tts_lm_model_kwargs = _update_model_kwargs_for_generation(
|
| 827 |
+
tts_lm_outputs, tts_lm_model_kwargs, num_new_tokens=next_text_window_size,
|
| 828 |
+
)
|
| 829 |
+
else:
|
| 830 |
+
tts_lm_model_kwargs = self._update_model_kwargs_for_generation(
|
| 831 |
+
tts_lm_outputs, tts_lm_model_kwargs, is_encoder_decoder=False,
|
| 832 |
+
)
|
| 833 |
+
|
| 834 |
+
tts_lm_negative_input_ids = torch.cat([tts_lm_negative_input_ids, torch.ones_like(tts_lm_input_ids[:, -1:])], dim=-1)
|
| 835 |
+
tts_lm_negative_model_inputs = self.prepare_inputs_for_generation(tts_lm_negative_input_ids, **tts_lm_negative_model_kwargs)
|
| 836 |
+
# Forward negative pass through the model
|
| 837 |
+
tts_lm_negative_additional_inputs = {
|
| 838 |
+
"tts_text_masks": torch.zeros_like(tts_lm_negative_input_ids[:, -1:]),
|
| 839 |
+
"lm_last_hidden_state": acoustic_embed,
|
| 840 |
+
}
|
| 841 |
+
tts_lm_negative_outputs = self.forward_tts_lm(
|
| 842 |
+
**tts_lm_negative_model_inputs, **tts_lm_negative_additional_inputs, return_dict=True, output_attentions=False, output_hidden_states=False,
|
| 843 |
+
)
|
| 844 |
+
tts_lm_negative_model_kwargs = self._update_model_kwargs_for_generation(
|
| 845 |
+
tts_lm_negative_outputs, tts_lm_negative_model_kwargs, is_encoder_decoder=False,
|
| 846 |
+
)
|
| 847 |
+
|
| 848 |
+
tts_eos_logits = torch.sigmoid(self.tts_eos_classifier(tts_lm_outputs.last_hidden_state[diffusion_indices, -1, :]))
|
| 849 |
+
if tts_eos_logits[0].item() > 0.5:
|
| 850 |
+
# If EOS token is predicted, we can stop generation for this sample
|
| 851 |
+
finished_tags[diffusion_indices] = True
|
| 852 |
+
if audio_streamer is not None:
|
| 853 |
+
audio_streamer.end(diffusion_indices)
|
| 854 |
+
|
| 855 |
+
if tts_lm_input_ids.shape[1] > tts_lm_generation_config.max_length:
|
| 856 |
+
if verbose:
|
| 857 |
+
print(f"Reached maximum generation length {tts_lm_generation_config.max_length}, stopped it.")
|
| 858 |
+
reached_samples = torch.arange(batch_size, device=device)[~finished_tags]
|
| 859 |
+
if reached_samples.numel() > 0:
|
| 860 |
+
reach_max_step_sample[reached_samples] = True
|
| 861 |
+
break
|
| 862 |
+
|
| 863 |
+
if audio_streamer is not None:
|
| 864 |
+
audio_streamer.end()
|
| 865 |
+
|
| 866 |
+
# Concatenate audio chunks for each sample
|
| 867 |
+
final_audio_outputs = []
|
| 868 |
+
for sample_chunks in audio_chunks:
|
| 869 |
+
if sample_chunks:
|
| 870 |
+
# Concatenate all chunks along the time dimension (assumed to be the last dimension)
|
| 871 |
+
concatenated_audio = torch.cat(sample_chunks, dim=-1)
|
| 872 |
+
final_audio_outputs.append(concatenated_audio)
|
| 873 |
+
else:
|
| 874 |
+
# If no audio was generated for this sample, append None
|
| 875 |
+
final_audio_outputs.append(None)
|
| 876 |
+
|
| 877 |
+
if reach_max_step_sample is not None and reach_max_step_sample.any():
|
| 878 |
+
print(f"Reached maximum generation length {tts_lm_generation_config.max_length}, stopped it.")
|
| 879 |
+
|
| 880 |
+
return VibeVoiceGenerationOutput(
|
| 881 |
+
sequences=tts_lm_input_ids,
|
| 882 |
+
speech_outputs=final_audio_outputs if return_speech else None,
|
| 883 |
+
reach_max_step_sample=reach_max_step_sample,
|
| 884 |
+
)
|
| 885 |
+
|
| 886 |
+
@torch.no_grad()
|
| 887 |
+
def sample_speech_tokens(self, condition, neg_condition, cfg_scale=3.0):
|
| 888 |
+
self.model.noise_scheduler.set_timesteps(self.ddpm_inference_steps)
|
| 889 |
+
condition = torch.cat([condition, neg_condition], dim=0).to(self.model.prediction_head.device)
|
| 890 |
+
speech = torch.randn(condition.shape[0], self.config.acoustic_vae_dim).to(condition)
|
| 891 |
+
for t in self.model.noise_scheduler.timesteps:
|
| 892 |
+
half = speech[: len(speech) // 2]
|
| 893 |
+
combined = torch.cat([half, half], dim=0)
|
| 894 |
+
eps = self.model.prediction_head(combined, t.repeat(combined.shape[0]).to(combined), condition=condition)
|
| 895 |
+
cond_eps, uncond_eps = torch.split(eps, len(eps) // 2, dim=0)
|
| 896 |
+
half_eps = uncond_eps + cfg_scale * (cond_eps - uncond_eps)
|
| 897 |
+
eps = torch.cat([half_eps, half_eps], dim=0)
|
| 898 |
+
speech = self.model.noise_scheduler.step(eps, t, speech).prev_sample
|
| 899 |
+
return speech[: len(speech) // 2]
|
| 900 |
+
|
| 901 |
+
|
| 902 |
+
AutoModelForCausalLM.register(VibeVoiceStreamingConfig, VibeVoiceStreamingForConditionalGenerationInference)
|
| 903 |
+
|
| 904 |
+
__all__ = [
|
| 905 |
+
"VibeVoiceStreamingForConditionalGenerationInference",
|
| 906 |
+
]
|
vibevoice/modular/modular_vibevoice_diffusion_head.py
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
from typing import Optional, Tuple, Union
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
import torch.nn as nn
|
| 6 |
+
import torch.nn.functional as F
|
| 7 |
+
|
| 8 |
+
from transformers.models.auto import AutoModel
|
| 9 |
+
from transformers.modeling_utils import PreTrainedModel
|
| 10 |
+
# from transformers.modeling_layers import GradientCheckpointingLayer
|
| 11 |
+
from transformers.activations import ACT2FN
|
| 12 |
+
from transformers.utils import logging
|
| 13 |
+
|
| 14 |
+
from .configuration_vibevoice import VibeVoiceDiffusionHeadConfig
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
logger = logging.get_logger(__name__)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class RMSNorm(nn.Module):
|
| 21 |
+
def __init__(self, dim: int, eps: float = 1e-6, elementwise_affine=True, memory_efficient=False):
|
| 22 |
+
super().__init__()
|
| 23 |
+
self.dim = dim
|
| 24 |
+
self.eps = eps
|
| 25 |
+
self.elementwise_affine = elementwise_affine
|
| 26 |
+
if self.elementwise_affine:
|
| 27 |
+
self.weight = nn.Parameter(torch.ones(dim))
|
| 28 |
+
else:
|
| 29 |
+
self.register_parameter('weight', None)
|
| 30 |
+
|
| 31 |
+
def _norm(self, x):
|
| 32 |
+
return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
|
| 33 |
+
|
| 34 |
+
def forward(self, x):
|
| 35 |
+
output = self._norm(x.float()).type_as(x)
|
| 36 |
+
if self.weight is not None:
|
| 37 |
+
output = output * self.weight
|
| 38 |
+
return output
|
| 39 |
+
|
| 40 |
+
def extra_repr(self) -> str:
|
| 41 |
+
return f'dim={self.dim}, eps={self.eps}, elementwise_affine={self.elementwise_affine}'
|
| 42 |
+
|
| 43 |
+
def modulate(x, shift, scale):
|
| 44 |
+
"""Apply modulation to input tensor."""
|
| 45 |
+
return x * (1 + scale) + shift
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class TimestepEmbedder(nn.Module):
|
| 49 |
+
"""
|
| 50 |
+
Embeds scalar timesteps into vector representations.
|
| 51 |
+
|
| 52 |
+
Args:
|
| 53 |
+
hidden_size (`int`): Size of the output embedding
|
| 54 |
+
frequency_embedding_size (`int`, optional): Size of the intermediate frequency embedding
|
| 55 |
+
"""
|
| 56 |
+
def __init__(self, hidden_size, frequency_embedding_size=256):
|
| 57 |
+
super().__init__()
|
| 58 |
+
self.mlp = nn.Sequential(
|
| 59 |
+
nn.Linear(frequency_embedding_size, hidden_size, bias=False),
|
| 60 |
+
# nn.SiLU(),
|
| 61 |
+
ACT2FN['silu'],
|
| 62 |
+
nn.Linear(hidden_size, hidden_size, bias=False),
|
| 63 |
+
)
|
| 64 |
+
self.frequency_embedding_size = frequency_embedding_size
|
| 65 |
+
|
| 66 |
+
@staticmethod
|
| 67 |
+
def timestep_embedding(t, dim, max_period=10000):
|
| 68 |
+
"""
|
| 69 |
+
Create sinusoidal timestep embeddings.
|
| 70 |
+
|
| 71 |
+
Args:
|
| 72 |
+
t (`torch.Tensor`): A 1-D Tensor of N indices, one per batch element.
|
| 73 |
+
These may be fractional.
|
| 74 |
+
dim (`int`): The dimension of the output.
|
| 75 |
+
max_period (`int`, optional): Controls the minimum frequency of the embeddings.
|
| 76 |
+
|
| 77 |
+
Returns:
|
| 78 |
+
`torch.Tensor`: An [N, D] Tensor of positional embeddings.
|
| 79 |
+
"""
|
| 80 |
+
half = dim // 2
|
| 81 |
+
freqs = torch.exp(
|
| 82 |
+
-math.log(max_period) * torch.arange(start=0, end=half, dtype=torch.float32) / half
|
| 83 |
+
).to(t.device)
|
| 84 |
+
args = t[:, None].float() * freqs[None]
|
| 85 |
+
embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
|
| 86 |
+
if dim % 2:
|
| 87 |
+
embedding = torch.cat([embedding, torch.zeros_like(embedding[:, :1])], dim=-1)
|
| 88 |
+
return embedding.to(t.dtype)
|
| 89 |
+
|
| 90 |
+
def forward(self, t):
|
| 91 |
+
t_freq = self.timestep_embedding(t, self.frequency_embedding_size)
|
| 92 |
+
t_emb = self.mlp(t_freq)
|
| 93 |
+
return t_emb
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
class FeedForwardNetwork(nn.Module):
|
| 97 |
+
"""
|
| 98 |
+
Standard feed-forward network with SwiGLU activation.
|
| 99 |
+
|
| 100 |
+
Args:
|
| 101 |
+
embed_dim (`int`): Input dimension
|
| 102 |
+
ffn_dim (`int`): Hidden dimension
|
| 103 |
+
"""
|
| 104 |
+
def __init__(
|
| 105 |
+
self,
|
| 106 |
+
embed_dim,
|
| 107 |
+
ffn_dim,
|
| 108 |
+
):
|
| 109 |
+
super().__init__()
|
| 110 |
+
self.embed_dim = embed_dim
|
| 111 |
+
self.gate_proj = nn.Linear(self.embed_dim, ffn_dim, bias=False)
|
| 112 |
+
self.up_proj = nn.Linear(self.embed_dim, ffn_dim, bias=False)
|
| 113 |
+
self.down_proj = nn.Linear(ffn_dim, self.embed_dim, bias=False)
|
| 114 |
+
self.act_fn = ACT2FN['silu'] # Using SiLU as the activation function
|
| 115 |
+
|
| 116 |
+
def forward(self, x):
|
| 117 |
+
gate = self.gate_proj(x)
|
| 118 |
+
up = self.up_proj(x)
|
| 119 |
+
|
| 120 |
+
# SwiGLU activation
|
| 121 |
+
# gate = F.silu(gate)
|
| 122 |
+
gate = self.act_fn(gate)
|
| 123 |
+
return self.down_proj(gate * up)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
class HeadLayer(nn.Module):
|
| 127 |
+
"""
|
| 128 |
+
A layer in the diffusion head.
|
| 129 |
+
|
| 130 |
+
Args:
|
| 131 |
+
embed_dim (`int`): Input dimension
|
| 132 |
+
ffn_dim (`int`): Hidden dimension
|
| 133 |
+
cond_dim (`int`): Condition embedding dimension
|
| 134 |
+
norm_eps (`float`, optional): Epsilon for normalization
|
| 135 |
+
"""
|
| 136 |
+
def __init__(
|
| 137 |
+
self,
|
| 138 |
+
embed_dim,
|
| 139 |
+
ffn_dim,
|
| 140 |
+
cond_dim,
|
| 141 |
+
norm_eps=1e-5,
|
| 142 |
+
):
|
| 143 |
+
super().__init__()
|
| 144 |
+
self.embed_dim = embed_dim
|
| 145 |
+
self.cond_dim = cond_dim
|
| 146 |
+
self.ffn_dim = ffn_dim
|
| 147 |
+
self.ffn = FeedForwardNetwork(
|
| 148 |
+
self.embed_dim,
|
| 149 |
+
self.ffn_dim,
|
| 150 |
+
)
|
| 151 |
+
self.norm = RMSNorm(self.embed_dim, eps=norm_eps)
|
| 152 |
+
self.adaLN_modulation = nn.Sequential(
|
| 153 |
+
# nn.SiLU(),
|
| 154 |
+
ACT2FN['silu'],
|
| 155 |
+
nn.Linear(cond_dim, 3 * self.embed_dim, bias=False)
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
def forward(self, x, c):
|
| 159 |
+
shift_ffn, scale_ffn, gate_ffn = self.adaLN_modulation(c).chunk(3, dim=-1)
|
| 160 |
+
x = x + gate_ffn * self.ffn(modulate(self.norm(x), shift_ffn, scale_ffn))
|
| 161 |
+
return x
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
class FinalLayer(nn.Module):
|
| 165 |
+
"""
|
| 166 |
+
Final layer in the diffusion head.
|
| 167 |
+
|
| 168 |
+
Args:
|
| 169 |
+
hidden_size (`int`): Input dimension
|
| 170 |
+
output_size (`int`): Output dimension
|
| 171 |
+
cond_size (`int`): Condition embedding dimension
|
| 172 |
+
norm_eps (`float`, optional): Epsilon for normalization
|
| 173 |
+
"""
|
| 174 |
+
def __init__(self, hidden_size, output_size, cond_size, norm_eps=1e-5):
|
| 175 |
+
super().__init__()
|
| 176 |
+
self.norm_final = RMSNorm(hidden_size, eps=norm_eps, elementwise_affine=False)
|
| 177 |
+
self.linear = nn.Linear(hidden_size, output_size, bias=False)
|
| 178 |
+
self.adaLN_modulation = nn.Sequential(
|
| 179 |
+
# nn.SiLU(),
|
| 180 |
+
ACT2FN['silu'],
|
| 181 |
+
nn.Linear(cond_size, 2 * hidden_size, bias=False)
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
def forward(self, x, c):
|
| 185 |
+
shift, scale = self.adaLN_modulation(c).chunk(2, dim=-1)
|
| 186 |
+
x = modulate(self.norm_final(x), shift, scale)
|
| 187 |
+
x = self.linear(x)
|
| 188 |
+
return x
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
class VibeVoiceDiffusionHead(PreTrainedModel):
|
| 192 |
+
"""
|
| 193 |
+
Diffusion head model for vibevoice.
|
| 194 |
+
|
| 195 |
+
Args:
|
| 196 |
+
config (`VibeVoiceDiffusionHeadConfig`): Model configuration
|
| 197 |
+
latent_size (`int`, optional): Size of the latent space. If not provided, uses `config.latent_size`.
|
| 198 |
+
"""
|
| 199 |
+
config_class = VibeVoiceDiffusionHeadConfig
|
| 200 |
+
supports_gradient_checkpointing = True
|
| 201 |
+
_supports_flash_attn_2 = True
|
| 202 |
+
_supports_sdpa = True
|
| 203 |
+
|
| 204 |
+
def __init__(
|
| 205 |
+
self,
|
| 206 |
+
config,
|
| 207 |
+
):
|
| 208 |
+
super().__init__(config)
|
| 209 |
+
self.config = config
|
| 210 |
+
self.cond_dim = config.hidden_size
|
| 211 |
+
latent_size = config.latent_size
|
| 212 |
+
|
| 213 |
+
self.noisy_images_proj = nn.Linear(latent_size, config.hidden_size, bias=False)
|
| 214 |
+
self.cond_proj = nn.Linear(config.hidden_size, self.cond_dim, bias=False)
|
| 215 |
+
self.t_embedder = TimestepEmbedder(self.cond_dim)
|
| 216 |
+
|
| 217 |
+
ffn_dim = int(config.hidden_size * config.head_ffn_ratio)
|
| 218 |
+
|
| 219 |
+
# Create the intermediate layers
|
| 220 |
+
self.layers = nn.ModuleList([
|
| 221 |
+
HeadLayer(
|
| 222 |
+
embed_dim=config.hidden_size,
|
| 223 |
+
ffn_dim=ffn_dim,
|
| 224 |
+
cond_dim=self.cond_dim,
|
| 225 |
+
norm_eps=config.rms_norm_eps
|
| 226 |
+
)
|
| 227 |
+
for _ in range(config.head_layers)
|
| 228 |
+
])
|
| 229 |
+
|
| 230 |
+
# Final layer for output
|
| 231 |
+
self.final_layer = FinalLayer(
|
| 232 |
+
hidden_size=config.hidden_size,
|
| 233 |
+
output_size=latent_size,
|
| 234 |
+
cond_size=self.cond_dim,
|
| 235 |
+
norm_eps=config.rms_norm_eps
|
| 236 |
+
)
|
| 237 |
+
|
| 238 |
+
self.initialize_weights()
|
| 239 |
+
|
| 240 |
+
def initialize_weights(self):
|
| 241 |
+
"""Initialize the weights of the model."""
|
| 242 |
+
# Initialize timestep embedder
|
| 243 |
+
nn.init.normal_(self.t_embedder.mlp[0].weight, std=0.02)
|
| 244 |
+
nn.init.normal_(self.t_embedder.mlp[2].weight, std=0.02)
|
| 245 |
+
|
| 246 |
+
# Zero-out adaLN modulation layers
|
| 247 |
+
for layer in self.layers:
|
| 248 |
+
nn.init.constant_(layer.adaLN_modulation[-1].weight, 0)
|
| 249 |
+
|
| 250 |
+
# Zero-out output layers
|
| 251 |
+
nn.init.constant_(self.final_layer.adaLN_modulation[-1].weight, 0)
|
| 252 |
+
nn.init.constant_(self.final_layer.linear.weight, 0)
|
| 253 |
+
|
| 254 |
+
def forward(
|
| 255 |
+
self,
|
| 256 |
+
noisy_images,
|
| 257 |
+
timesteps,
|
| 258 |
+
condition,
|
| 259 |
+
):
|
| 260 |
+
"""
|
| 261 |
+
Forward pass of the prediction head.
|
| 262 |
+
|
| 263 |
+
Args:
|
| 264 |
+
noisy_images (`torch.Tensor`): Noisy images/latents to denoise
|
| 265 |
+
timesteps (`torch.Tensor`): Timesteps for diffusion
|
| 266 |
+
condition (`torch.Tensor`): Conditioning information
|
| 267 |
+
|
| 268 |
+
Returns:
|
| 269 |
+
`torch.Tensor`: The predicted noise/velocity
|
| 270 |
+
"""
|
| 271 |
+
x = self.noisy_images_proj(noisy_images)
|
| 272 |
+
t = self.t_embedder(timesteps)
|
| 273 |
+
condition = self.cond_proj(condition)
|
| 274 |
+
c = condition + t
|
| 275 |
+
|
| 276 |
+
for layer in self.layers:
|
| 277 |
+
x = layer(x, c)
|
| 278 |
+
|
| 279 |
+
x = self.final_layer(x, c)
|
| 280 |
+
return x
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
AutoModel.register(VibeVoiceDiffusionHeadConfig, VibeVoiceDiffusionHead)
|
| 284 |
+
|
| 285 |
+
__all__ = [
|
| 286 |
+
"VibeVoiceDiffusionHead",
|
| 287 |
+
]
|
vibevoice/modular/modular_vibevoice_text_tokenizer.py
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tokenization classes for vibevoice."""
|
| 2 |
+
|
| 3 |
+
from typing import List, Optional, Union
|
| 4 |
+
|
| 5 |
+
from transformers.utils import logging
|
| 6 |
+
from transformers.models.qwen2.tokenization_qwen2 import Qwen2Tokenizer
|
| 7 |
+
from transformers.models.qwen2.tokenization_qwen2_fast import Qwen2TokenizerFast
|
| 8 |
+
|
| 9 |
+
logger = logging.get_logger(__name__)
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class VibeVoiceTextTokenizer(Qwen2Tokenizer):
|
| 13 |
+
"""
|
| 14 |
+
Construct a VibeVoice tokenizer. Based on the Qwen2 tokenizer with additional special tokens for speech.
|
| 15 |
+
|
| 16 |
+
Args:
|
| 17 |
+
vocab_file (`str`):
|
| 18 |
+
Path to the vocabulary file.
|
| 19 |
+
merges_file (`str`):
|
| 20 |
+
Path to the merges file.
|
| 21 |
+
errors (`str`, *optional*, defaults to `"replace"`):
|
| 22 |
+
Paradigm to follow when decoding bytes to UTF-8.
|
| 23 |
+
unk_token (`str`, *optional*, defaults to `"<|endoftext|>"`):
|
| 24 |
+
The unknown token.
|
| 25 |
+
bos_token (`str`, *optional*):
|
| 26 |
+
The beginning of sequence token. Not used for vibevoice.
|
| 27 |
+
eos_token (`str`, *optional*, defaults to `"<|endoftext|>"`):
|
| 28 |
+
The end of sequence token.
|
| 29 |
+
pad_token (`str`, *optional*, defaults to `"<|endoftext|>"`):
|
| 30 |
+
The token used for padding.
|
| 31 |
+
add_special_tokens (`bool`, *optional*, defaults to `True`):
|
| 32 |
+
Whether or not to add special tokens when encoding.
|
| 33 |
+
"""
|
| 34 |
+
|
| 35 |
+
model_input_names = ["input_ids", "attention_mask"]
|
| 36 |
+
|
| 37 |
+
def __init__(
|
| 38 |
+
self,
|
| 39 |
+
vocab_file,
|
| 40 |
+
merges_file,
|
| 41 |
+
errors="replace",
|
| 42 |
+
unk_token="<|endoftext|>",
|
| 43 |
+
bos_token=None,
|
| 44 |
+
eos_token="<|endoftext|>",
|
| 45 |
+
pad_token="<|endoftext|>",
|
| 46 |
+
add_prefix_space=False,
|
| 47 |
+
add_special_tokens=True,
|
| 48 |
+
**kwargs,
|
| 49 |
+
):
|
| 50 |
+
super().__init__(
|
| 51 |
+
vocab_file=vocab_file,
|
| 52 |
+
merges_file=merges_file,
|
| 53 |
+
errors=errors,
|
| 54 |
+
unk_token=unk_token,
|
| 55 |
+
bos_token=bos_token,
|
| 56 |
+
eos_token=eos_token,
|
| 57 |
+
pad_token=pad_token,
|
| 58 |
+
add_prefix_space=add_prefix_space,
|
| 59 |
+
add_special_tokens=add_special_tokens,
|
| 60 |
+
**kwargs,
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
# Add VibeVoice-specific special tokens
|
| 64 |
+
self._add_vibevoice_special_tokens()
|
| 65 |
+
|
| 66 |
+
def _add_vibevoice_special_tokens(self):
|
| 67 |
+
"""Add VibeVoice-specific special tokens."""
|
| 68 |
+
special_tokens = {
|
| 69 |
+
"additional_special_tokens": [
|
| 70 |
+
"<|vision_start|>", # Speech start (reusing vision tokens)
|
| 71 |
+
"<|vision_end|>", # Speech end
|
| 72 |
+
"<|vision_pad|>", # Speech diffusion pad
|
| 73 |
+
]
|
| 74 |
+
}
|
| 75 |
+
num_added = self.add_special_tokens(special_tokens)
|
| 76 |
+
|
| 77 |
+
# Cache special token IDs
|
| 78 |
+
self._speech_start_id = self.convert_tokens_to_ids("<|vision_start|>")
|
| 79 |
+
self._speech_end_id = self.convert_tokens_to_ids("<|vision_end|>")
|
| 80 |
+
self._speech_diffusion_id = self.convert_tokens_to_ids("<|vision_pad|>")
|
| 81 |
+
|
| 82 |
+
self._eos_id = self.convert_tokens_to_ids('<|endoftext|>')
|
| 83 |
+
|
| 84 |
+
return num_added
|
| 85 |
+
|
| 86 |
+
@property
|
| 87 |
+
def eos_id(self) -> int:
|
| 88 |
+
"""ID of the end of sequence token."""
|
| 89 |
+
return self._eos_id
|
| 90 |
+
|
| 91 |
+
@property
|
| 92 |
+
def speech_start_id(self) -> int:
|
| 93 |
+
"""ID of the speech start token."""
|
| 94 |
+
return self._speech_start_id
|
| 95 |
+
|
| 96 |
+
@property
|
| 97 |
+
def speech_end_id(self) -> int:
|
| 98 |
+
"""ID of the speech end token."""
|
| 99 |
+
return self._speech_end_id
|
| 100 |
+
|
| 101 |
+
@property
|
| 102 |
+
def speech_diffusion_id(self) -> int:
|
| 103 |
+
"""ID of the speech diffusion token."""
|
| 104 |
+
return self._speech_diffusion_id
|
| 105 |
+
|
| 106 |
+
@property
|
| 107 |
+
def pad_id(self) -> int:
|
| 108 |
+
"""ID used for padding (returns -100 for loss masking)."""
|
| 109 |
+
return -100
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
class VibeVoiceTextTokenizerFast(Qwen2TokenizerFast):
|
| 113 |
+
"""
|
| 114 |
+
Construct a "fast" VibeVoice tokenizer (backed by HuggingFace's *tokenizers* library).
|
| 115 |
+
Based on the Qwen2 tokenizer with additional special tokens for speech.
|
| 116 |
+
|
| 117 |
+
Args:
|
| 118 |
+
vocab_file (`str`, *optional*):
|
| 119 |
+
Path to the vocabulary file.
|
| 120 |
+
merges_file (`str`, *optional*):
|
| 121 |
+
Path to the merges file.
|
| 122 |
+
tokenizer_file (`str`, *optional*):
|
| 123 |
+
Path to [tokenizers](https://github.com/huggingface/tokenizers) file.
|
| 124 |
+
unk_token (`str`, *optional*, defaults to `"<|endoftext|>"`):
|
| 125 |
+
The unknown token.
|
| 126 |
+
bos_token (`str`, *optional*):
|
| 127 |
+
The beginning of sequence token. Not used for vibevoice.
|
| 128 |
+
eos_token (`str`, *optional*, defaults to `"<|endoftext|>"`):
|
| 129 |
+
The end of sequence token.
|
| 130 |
+
pad_token (`str`, *optional*, defaults to `"<|endoftext|>"`):
|
| 131 |
+
The token used for padding.
|
| 132 |
+
"""
|
| 133 |
+
|
| 134 |
+
model_input_names = ["input_ids", "attention_mask"]
|
| 135 |
+
|
| 136 |
+
def __init__(
|
| 137 |
+
self,
|
| 138 |
+
vocab_file=None,
|
| 139 |
+
merges_file=None,
|
| 140 |
+
tokenizer_file=None,
|
| 141 |
+
unk_token="<|endoftext|>",
|
| 142 |
+
bos_token=None,
|
| 143 |
+
eos_token="<|endoftext|>",
|
| 144 |
+
pad_token="<|endoftext|>",
|
| 145 |
+
add_prefix_space=False,
|
| 146 |
+
**kwargs,
|
| 147 |
+
):
|
| 148 |
+
super().__init__(
|
| 149 |
+
vocab_file=vocab_file,
|
| 150 |
+
merges_file=merges_file,
|
| 151 |
+
tokenizer_file=tokenizer_file,
|
| 152 |
+
unk_token=unk_token,
|
| 153 |
+
bos_token=bos_token,
|
| 154 |
+
eos_token=eos_token,
|
| 155 |
+
pad_token=pad_token,
|
| 156 |
+
add_prefix_space=add_prefix_space,
|
| 157 |
+
**kwargs,
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
# Add VibeVoice-specific special tokens
|
| 161 |
+
self._add_vibevoice_special_tokens()
|
| 162 |
+
|
| 163 |
+
def _add_vibevoice_special_tokens(self):
|
| 164 |
+
"""Add VibeVoice-specific special tokens."""
|
| 165 |
+
special_tokens = {
|
| 166 |
+
"additional_special_tokens": [
|
| 167 |
+
"<|vision_start|>", # Speech start (reusing vision tokens)
|
| 168 |
+
"<|vision_end|>", # Speech end
|
| 169 |
+
"<|vision_pad|>", # Speech diffusion pad
|
| 170 |
+
]
|
| 171 |
+
}
|
| 172 |
+
num_added = self.add_special_tokens(special_tokens)
|
| 173 |
+
|
| 174 |
+
# Cache special token IDs
|
| 175 |
+
self._speech_start_id = self.convert_tokens_to_ids("<|vision_start|>")
|
| 176 |
+
self._speech_end_id = self.convert_tokens_to_ids("<|vision_end|>")
|
| 177 |
+
self._speech_diffusion_id = self.convert_tokens_to_ids("<|vision_pad|>")
|
| 178 |
+
|
| 179 |
+
# self._eos_id = self.convert_tokens_to_ids('<|endoftext|>')
|
| 180 |
+
self._eos_id = self.eos_token_id # qwen2 / qwen3
|
| 181 |
+
self._pad_id = self.convert_tokens_to_ids('<|image_pad|>')
|
| 182 |
+
|
| 183 |
+
return num_added
|
| 184 |
+
|
| 185 |
+
@property
|
| 186 |
+
def eos_id(self) -> int:
|
| 187 |
+
"""ID of the end of sequence token."""
|
| 188 |
+
return self._eos_id
|
| 189 |
+
|
| 190 |
+
@property
|
| 191 |
+
def speech_start_id(self) -> int:
|
| 192 |
+
"""ID of the speech start token."""
|
| 193 |
+
return self._speech_start_id
|
| 194 |
+
|
| 195 |
+
@property
|
| 196 |
+
def speech_end_id(self) -> int:
|
| 197 |
+
"""ID of the speech end token."""
|
| 198 |
+
return self._speech_end_id
|
| 199 |
+
|
| 200 |
+
@property
|
| 201 |
+
def speech_diffusion_id(self) -> int:
|
| 202 |
+
"""ID of the speech diffusion token."""
|
| 203 |
+
return self._speech_diffusion_id
|
| 204 |
+
|
| 205 |
+
@property
|
| 206 |
+
def pad_id(self) -> int:
|
| 207 |
+
"""ID used for padding (returns -100 for loss masking)."""
|
| 208 |
+
return self._pad_id
|
| 209 |
+
|
| 210 |
+
class VibeVoiceASRTextTokenizerFast(Qwen2TokenizerFast):
|
| 211 |
+
"""
|
| 212 |
+
Construct a "fast" VibeVoice tokenizer (backed by HuggingFace's *tokenizers* library).
|
| 213 |
+
Based on the Qwen2 tokenizer with additional special tokens for speech.
|
| 214 |
+
|
| 215 |
+
Args:
|
| 216 |
+
vocab_file (`str`, *optional*):
|
| 217 |
+
Path to the vocabulary file.
|
| 218 |
+
merges_file (`str`, *optional*):
|
| 219 |
+
Path to the merges file.
|
| 220 |
+
tokenizer_file (`str`, *optional*):
|
| 221 |
+
Path to [tokenizers](https://github.com/huggingface/tokenizers) file.
|
| 222 |
+
unk_token (`str`, *optional*, defaults to `"<|endoftext|>"`):
|
| 223 |
+
The unknown token.
|
| 224 |
+
bos_token (`str`, *optional*):
|
| 225 |
+
The beginning of sequence token. Not used for vibevoice.
|
| 226 |
+
eos_token (`str`, *optional*, defaults to `"<|endoftext|>"`):
|
| 227 |
+
The end of sequence token.
|
| 228 |
+
pad_token (`str`, *optional*, defaults to `"<|endoftext|>"`):
|
| 229 |
+
The token used for padding.
|
| 230 |
+
"""
|
| 231 |
+
|
| 232 |
+
model_input_names = ["input_ids", "attention_mask"]
|
| 233 |
+
|
| 234 |
+
def __init__(
|
| 235 |
+
self,
|
| 236 |
+
vocab_file=None,
|
| 237 |
+
merges_file=None,
|
| 238 |
+
tokenizer_file=None,
|
| 239 |
+
unk_token="<|endoftext|>",
|
| 240 |
+
bos_token=None,
|
| 241 |
+
eos_token="<|endoftext|>",
|
| 242 |
+
pad_token="<|endoftext|>",
|
| 243 |
+
add_prefix_space=False,
|
| 244 |
+
**kwargs,
|
| 245 |
+
):
|
| 246 |
+
super().__init__(
|
| 247 |
+
vocab_file=vocab_file,
|
| 248 |
+
merges_file=merges_file,
|
| 249 |
+
tokenizer_file=tokenizer_file,
|
| 250 |
+
unk_token=unk_token,
|
| 251 |
+
bos_token=bos_token,
|
| 252 |
+
eos_token=eos_token,
|
| 253 |
+
pad_token=pad_token,
|
| 254 |
+
add_prefix_space=add_prefix_space,
|
| 255 |
+
**kwargs,
|
| 256 |
+
)
|
| 257 |
+
|
| 258 |
+
# Add VibeVoice-specific special tokens
|
| 259 |
+
self._add_vibevoice_special_tokens()
|
| 260 |
+
|
| 261 |
+
# https://github.com/QwenLM/Qwen2.5-VL/blob/d2240f11656bfe404b9ba56db4e51cd09f522ff1/qwen-vl-finetune/qwenvl/data/data_qwen_packed.py#L57C5-L57C222
|
| 262 |
+
self.chat_template = "{% for message in messages %}{{'<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n'}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant\n' }}{% endif %}"
|
| 263 |
+
|
| 264 |
+
def _add_vibevoice_special_tokens(self):
|
| 265 |
+
"""Add VibeVoice-specific special tokens."""
|
| 266 |
+
special_tokens = {
|
| 267 |
+
"additional_special_tokens": [
|
| 268 |
+
"<|object_ref_start|>", # Speech start (reusing vision tokens)
|
| 269 |
+
"<|object_ref_end|>", # Speech end
|
| 270 |
+
"<|box_start|>", # Speech diffusion pad
|
| 271 |
+
]
|
| 272 |
+
}
|
| 273 |
+
num_added = self.add_special_tokens(special_tokens)
|
| 274 |
+
|
| 275 |
+
# Cache special token IDs
|
| 276 |
+
self._speech_start_id = self.convert_tokens_to_ids("<|object_ref_start|>")
|
| 277 |
+
self._speech_end_id = self.convert_tokens_to_ids("<|object_ref_end|>")
|
| 278 |
+
self._speech_pad_id = self.convert_tokens_to_ids("<|box_start|>")
|
| 279 |
+
|
| 280 |
+
self._eos_id = self.eos_token_id # qwen2 / qwen3
|
| 281 |
+
self._pad_id = self.convert_tokens_to_ids('<|image_pad|>')
|
| 282 |
+
|
| 283 |
+
return num_added
|
| 284 |
+
|
| 285 |
+
@property
|
| 286 |
+
def eos_id(self) -> int:
|
| 287 |
+
"""ID of the end of sequence token."""
|
| 288 |
+
return self._eos_id
|
| 289 |
+
|
| 290 |
+
@property
|
| 291 |
+
def speech_start_id(self) -> int:
|
| 292 |
+
"""ID of the speech start token."""
|
| 293 |
+
return self._speech_start_id
|
| 294 |
+
|
| 295 |
+
@property
|
| 296 |
+
def speech_end_id(self) -> int:
|
| 297 |
+
"""ID of the speech end token."""
|
| 298 |
+
return self._speech_end_id
|
| 299 |
+
|
| 300 |
+
@property
|
| 301 |
+
def speech_pad_id(self) -> int:
|
| 302 |
+
"""ID of the speech diffusion token."""
|
| 303 |
+
return self._speech_pad_id
|
| 304 |
+
|
| 305 |
+
@property
|
| 306 |
+
def pad_id(self) -> int:
|
| 307 |
+
return self._pad_id
|
| 308 |
+
|
| 309 |
+
__all__ = [
|
| 310 |
+
"VibeVoiceTextTokenizer",
|
| 311 |
+
"VibeVoiceTextTokenizerFast",
|
| 312 |
+
"VibeVoiceASRTextTokenizerFast",
|
| 313 |
+
]
|
vibevoice/modular/modular_vibevoice_tokenizer.py
ADDED
|
@@ -0,0 +1,1207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
import typing as tp
|
| 3 |
+
from functools import partial
|
| 4 |
+
from dataclasses import dataclass, field
|
| 5 |
+
from typing import Dict, List, Optional, Tuple, Union
|
| 6 |
+
import copy
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
import torch
|
| 10 |
+
import torch.nn as nn
|
| 11 |
+
import torch.nn.functional as F
|
| 12 |
+
|
| 13 |
+
from transformers.models.auto import AutoModel
|
| 14 |
+
|
| 15 |
+
from transformers.configuration_utils import PretrainedConfig
|
| 16 |
+
from transformers.utils import logging
|
| 17 |
+
from transformers.modeling_utils import PreTrainedModel
|
| 18 |
+
from transformers.activations import ACT2FN
|
| 19 |
+
|
| 20 |
+
from .configuration_vibevoice import VibeVoiceAcousticTokenizerConfig, VibeVoiceSemanticTokenizerConfig
|
| 21 |
+
|
| 22 |
+
logger = logging.get_logger(__name__)
|
| 23 |
+
|
| 24 |
+
import os
|
| 25 |
+
# Try to import APEX FusedRMSNorm
|
| 26 |
+
try:
|
| 27 |
+
from apex.normalization.fused_layer_norm import fused_rms_norm_affine
|
| 28 |
+
APEX_AVAILABLE = True
|
| 29 |
+
# logger.info("APEX FusedRMSNorm is available and will be used for optimization")
|
| 30 |
+
if int(os.getenv("OPTIMIZE_FOR_SPEED", "0")) == 0:
|
| 31 |
+
APEX_AVAILABLE = False
|
| 32 |
+
# logger.warning("APEX FusedRMSNorm is disabled by environment variable OPTIMIZE_FOR_SPEED=0")
|
| 33 |
+
except ImportError:
|
| 34 |
+
APEX_AVAILABLE = False
|
| 35 |
+
# logger.warning("APEX FusedRMSNorm not available, using native implementation")
|
| 36 |
+
|
| 37 |
+
# Normalization modules
|
| 38 |
+
class ConvLayerNorm(nn.LayerNorm):
|
| 39 |
+
"""
|
| 40 |
+
Convolution-friendly LayerNorm that moves channels to last dimensions
|
| 41 |
+
before running the normalization and moves them back to original position right after.
|
| 42 |
+
"""
|
| 43 |
+
def __init__(self, normalized_shape: tp.Union[int, tp.List[int], torch.Size], **kwargs):
|
| 44 |
+
super().__init__(normalized_shape, **kwargs)
|
| 45 |
+
|
| 46 |
+
def forward(self, x):
|
| 47 |
+
x = x.transpose(1, 2) # b ... t -> b t ...
|
| 48 |
+
x = nn.functional.layer_norm(x.float(), self.normalized_shape, self.weight.float(), self.bias.float(), self.eps).type_as(x)
|
| 49 |
+
x = x.transpose(1, 2) # b t ... -> b ... t
|
| 50 |
+
return x
|
| 51 |
+
|
| 52 |
+
class RMSNorm(nn.Module):
|
| 53 |
+
def __init__(self, dim: int, eps: float = 1e-5, elementwise_affine=True, weight_shape=None):
|
| 54 |
+
super().__init__()
|
| 55 |
+
self.dim = dim
|
| 56 |
+
self.eps = eps
|
| 57 |
+
self.elementwise_affine = elementwise_affine
|
| 58 |
+
if self.elementwise_affine:
|
| 59 |
+
weight_shape = (dim,) if weight_shape is None else weight_shape
|
| 60 |
+
self.weight = nn.Parameter(torch.ones(weight_shape))
|
| 61 |
+
else:
|
| 62 |
+
self.register_parameter('weight', None)
|
| 63 |
+
|
| 64 |
+
def _norm(self, x):
|
| 65 |
+
return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
|
| 66 |
+
|
| 67 |
+
def forward(self, x):
|
| 68 |
+
output = self._norm(x.float()).type_as(x)
|
| 69 |
+
if self.weight is not None:
|
| 70 |
+
output = output * self.weight
|
| 71 |
+
return output
|
| 72 |
+
|
| 73 |
+
def extra_repr(self) -> str:
|
| 74 |
+
return f'dim={self.dim}, eps={self.eps}, elementwise_affine={self.elementwise_affine}'
|
| 75 |
+
|
| 76 |
+
class ConvRMSNorm(RMSNorm):
|
| 77 |
+
def __init__(self, dim: int, eps: float = 1e-5, elementwise_affine=True, weight_shape=None):
|
| 78 |
+
super().__init__(dim, eps, elementwise_affine, weight_shape)
|
| 79 |
+
|
| 80 |
+
def forward(self, x):
|
| 81 |
+
x = x.transpose(1, 2) # b ... t -> b t ...
|
| 82 |
+
if (not APEX_AVAILABLE) or (not self.elementwise_affine):
|
| 83 |
+
# Fallback to native implementation
|
| 84 |
+
output = self._norm(x.float()).type_as(x)
|
| 85 |
+
if self.weight is not None:
|
| 86 |
+
output = output * self.weight
|
| 87 |
+
else:
|
| 88 |
+
output = fused_rms_norm_affine(x, self.weight, self.weight.shape, self.eps)
|
| 89 |
+
output = output.transpose(1, 2) # b t ... -> b ... t
|
| 90 |
+
return output
|
| 91 |
+
|
| 92 |
+
# Convolutional layers and utilities
|
| 93 |
+
CONV_NORMALIZATIONS = frozenset(['none', 'weight_norm', 'spectral_norm',
|
| 94 |
+
'time_layer_norm', 'layer_norm', 'time_group_norm'])
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def apply_parametrization_norm(module: nn.Module, norm: str = 'none') -> nn.Module:
|
| 98 |
+
assert norm in CONV_NORMALIZATIONS
|
| 99 |
+
if norm == 'weight_norm':
|
| 100 |
+
return nn.utils.weight_norm(module)
|
| 101 |
+
elif norm == 'spectral_norm':
|
| 102 |
+
return nn.utils.spectral_norm(module)
|
| 103 |
+
else:
|
| 104 |
+
# We already check was in CONV_NORMALIZATION, so any other choice
|
| 105 |
+
# doesn't need reparametrization.
|
| 106 |
+
return module
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def get_norm_module(module: nn.Module, causal: bool = False, norm: str = 'none', **norm_kwargs) -> nn.Module:
|
| 110 |
+
"""Return the proper normalization module. If causal is True, this will ensure the returned
|
| 111 |
+
module is causal, or return an error if the normalization doesn't support causal evaluation.
|
| 112 |
+
"""
|
| 113 |
+
assert norm in CONV_NORMALIZATIONS
|
| 114 |
+
if norm == 'layer_norm':
|
| 115 |
+
assert isinstance(module, nn.modules.conv._ConvNd)
|
| 116 |
+
return ConvLayerNorm(module.out_channels, **norm_kwargs)
|
| 117 |
+
elif norm == 'time_group_norm':
|
| 118 |
+
if causal:
|
| 119 |
+
raise ValueError("GroupNorm doesn't support causal evaluation.")
|
| 120 |
+
assert isinstance(module, nn.modules.conv._ConvNd)
|
| 121 |
+
return nn.GroupNorm(1, module.out_channels, **norm_kwargs)
|
| 122 |
+
else:
|
| 123 |
+
return nn.Identity()
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def get_extra_padding_for_conv1d(x: torch.Tensor, kernel_size: int, stride: int,
|
| 127 |
+
padding_total: int = 0) -> int:
|
| 128 |
+
"""Calculate extra padding needed for convolution to have the same output length"""
|
| 129 |
+
length = x.shape[-1]
|
| 130 |
+
n_frames = (length - kernel_size + padding_total) / stride + 1
|
| 131 |
+
ideal_length = (math.ceil(n_frames) - 1) * stride + (kernel_size - padding_total)
|
| 132 |
+
return ideal_length - length
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def pad1d(x: torch.Tensor, paddings: tp.Tuple[int, int], mode: str = 'zero', value: float = 0.):
|
| 136 |
+
"""Pad 1D input with handling for small inputs in reflect mode"""
|
| 137 |
+
length = x.shape[-1]
|
| 138 |
+
padding_left, padding_right = paddings
|
| 139 |
+
assert padding_left >= 0 and padding_right >= 0, (padding_left, padding_right)
|
| 140 |
+
if mode == 'reflect':
|
| 141 |
+
max_pad = max(padding_left, padding_right)
|
| 142 |
+
extra_pad = 0
|
| 143 |
+
if length <= max_pad:
|
| 144 |
+
extra_pad = max_pad - length + 1
|
| 145 |
+
x = F.pad(x, (0, extra_pad))
|
| 146 |
+
padded = F.pad(x, paddings, mode, value)
|
| 147 |
+
end = padded.shape[-1] - extra_pad
|
| 148 |
+
return padded[..., :end]
|
| 149 |
+
else:
|
| 150 |
+
return F.pad(x, paddings, mode, value)
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def unpad1d(x: torch.Tensor, paddings: tp.Tuple[int, int]):
|
| 154 |
+
"""Remove padding from x, handling properly zero padding. Only for 1d!"""
|
| 155 |
+
padding_left, padding_right = paddings
|
| 156 |
+
assert padding_left >= 0 and padding_right >= 0, (padding_left, padding_right)
|
| 157 |
+
assert (padding_left + padding_right) <= x.shape[-1]
|
| 158 |
+
end = x.shape[-1] - padding_right
|
| 159 |
+
return x[..., padding_left: end]
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
class NormConv1d(nn.Module):
|
| 163 |
+
"""Wrapper around Conv1d and normalization applied to this conv"""
|
| 164 |
+
def __init__(self, *args, causal: bool = False, norm: str = 'none',
|
| 165 |
+
norm_kwargs: tp.Dict[str, tp.Any] = {}, **kwargs):
|
| 166 |
+
super().__init__()
|
| 167 |
+
self.conv = apply_parametrization_norm(nn.Conv1d(*args, **kwargs), norm)
|
| 168 |
+
self.norm = get_norm_module(self.conv, causal, norm, **norm_kwargs)
|
| 169 |
+
self.norm_type = norm
|
| 170 |
+
|
| 171 |
+
def forward(self, x):
|
| 172 |
+
x = self.conv(x)
|
| 173 |
+
x = self.norm(x)
|
| 174 |
+
return x
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
class NormConvTranspose1d(nn.Module):
|
| 178 |
+
"""Wrapper around ConvTranspose1d and normalization applied to this conv"""
|
| 179 |
+
def __init__(self, *args, causal: bool = False, norm: str = 'none',
|
| 180 |
+
norm_kwargs: tp.Dict[str, tp.Any] = {}, **kwargs):
|
| 181 |
+
super().__init__()
|
| 182 |
+
self.convtr = apply_parametrization_norm(nn.ConvTranspose1d(*args, **kwargs), norm)
|
| 183 |
+
self.norm = get_norm_module(self.convtr, causal, norm, **norm_kwargs)
|
| 184 |
+
self.norm_type = norm
|
| 185 |
+
|
| 186 |
+
def forward(self, x):
|
| 187 |
+
x = self.convtr(x)
|
| 188 |
+
x = self.norm(x)
|
| 189 |
+
return x
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
class VibeVoiceTokenizerStreamingCache:
|
| 193 |
+
"""Cache for streaming convolution, similar to KV cache in attention"""
|
| 194 |
+
def __init__(self):
|
| 195 |
+
self.cache = {} # Dict mapping (layer_id, sample_idx) to state tensor
|
| 196 |
+
|
| 197 |
+
def get(self, layer_id: str, sample_indices: torch.Tensor) -> Optional[torch.Tensor]:
|
| 198 |
+
"""Get cached states for given layer and sample indices"""
|
| 199 |
+
states = []
|
| 200 |
+
max_length = 0
|
| 201 |
+
|
| 202 |
+
# First pass: collect states and find max length
|
| 203 |
+
for idx in sample_indices.tolist():
|
| 204 |
+
key = (layer_id, idx)
|
| 205 |
+
if key not in self.cache:
|
| 206 |
+
return None # If any sample is missing, return None
|
| 207 |
+
state = self.cache[key]
|
| 208 |
+
states.append(state)
|
| 209 |
+
max_length = max(max_length, state.shape[-1])
|
| 210 |
+
|
| 211 |
+
# Second pass: pad states to max length if needed
|
| 212 |
+
if len(states) > 0 and states[0].dim() >= 2:
|
| 213 |
+
padded_states = []
|
| 214 |
+
for state in states:
|
| 215 |
+
if state.shape[-1] < max_length:
|
| 216 |
+
# Pad on the time dimension (last dimension)
|
| 217 |
+
pad_size = max_length - state.shape[-1]
|
| 218 |
+
# Pad with zeros on the LEFT to align the most recent samples
|
| 219 |
+
padded_state = F.pad(state, (pad_size, 0), mode='constant', value=0)
|
| 220 |
+
padded_states.append(padded_state)
|
| 221 |
+
else:
|
| 222 |
+
padded_states.append(state)
|
| 223 |
+
return torch.stack(padded_states, dim=0)
|
| 224 |
+
else:
|
| 225 |
+
return torch.stack(states, dim=0)
|
| 226 |
+
|
| 227 |
+
def set(self, layer_id: str, sample_indices: torch.Tensor, states: torch.Tensor):
|
| 228 |
+
"""Set cached states for given layer and sample indices"""
|
| 229 |
+
for i, idx in enumerate(sample_indices.tolist()):
|
| 230 |
+
key = (layer_id, idx)
|
| 231 |
+
self.cache[key] = states[i].detach()
|
| 232 |
+
|
| 233 |
+
def set_to_zero(self, sample_indices: torch.Tensor):
|
| 234 |
+
"""Set all cached states to zero for given sample indices"""
|
| 235 |
+
for key in list(self.cache.keys()):
|
| 236 |
+
layer_id, sample_idx = key
|
| 237 |
+
if sample_idx in sample_indices.tolist():
|
| 238 |
+
# Create zero tensor with same shape and dtype as cached tensor
|
| 239 |
+
cached_tensor = self.cache[key]
|
| 240 |
+
self.cache[key] = torch.zeros_like(cached_tensor)
|
| 241 |
+
|
| 242 |
+
def clear(self, layer_id: Optional[str] = None, sample_indices: Optional[torch.Tensor] = None):
|
| 243 |
+
"""Clear cache for specific layer/samples or everything"""
|
| 244 |
+
if layer_id is None and sample_indices is None:
|
| 245 |
+
self.cache.clear()
|
| 246 |
+
elif layer_id is not None and sample_indices is None:
|
| 247 |
+
# Clear all samples for a specific layer
|
| 248 |
+
keys_to_remove = [k for k in self.cache.keys() if k[0] == layer_id]
|
| 249 |
+
for k in keys_to_remove:
|
| 250 |
+
del self.cache[k]
|
| 251 |
+
elif layer_id is not None and sample_indices is not None:
|
| 252 |
+
# Clear specific samples for a specific layer
|
| 253 |
+
for idx in sample_indices.tolist():
|
| 254 |
+
key = (layer_id, idx)
|
| 255 |
+
self.cache.pop(key, None)
|
| 256 |
+
|
| 257 |
+
class SConv1d(nn.Module):
|
| 258 |
+
"""Conv1d with built-in handling of asymmetric or causal padding and normalization."""
|
| 259 |
+
def __init__(self, in_channels: int, out_channels: int,
|
| 260 |
+
kernel_size: int, stride: int = 1, dilation: int = 1,
|
| 261 |
+
groups: int = 1, bias: bool = True, causal: bool = False,
|
| 262 |
+
norm: str = 'none', norm_kwargs: tp.Dict[str, tp.Any] = {},
|
| 263 |
+
pad_mode: str = 'reflect'):
|
| 264 |
+
super().__init__()
|
| 265 |
+
self.conv = NormConv1d(in_channels, out_channels, kernel_size, stride,
|
| 266 |
+
dilation=dilation, groups=groups, bias=bias, causal=causal,
|
| 267 |
+
norm=norm, norm_kwargs=norm_kwargs)
|
| 268 |
+
self.causal = causal
|
| 269 |
+
self.pad_mode = pad_mode
|
| 270 |
+
|
| 271 |
+
# Store configuration
|
| 272 |
+
self.kernel_size = kernel_size
|
| 273 |
+
self.dilation = dilation
|
| 274 |
+
self.stride = stride
|
| 275 |
+
self.in_channels = in_channels
|
| 276 |
+
self.out_channels = out_channels
|
| 277 |
+
|
| 278 |
+
# For causal convolution, we need to maintain kernel_size - 1 samples as context
|
| 279 |
+
# need to check use which context_size is more suitable
|
| 280 |
+
# self.context_size = (kernel_size - 1) * dilation
|
| 281 |
+
self.context_size = (kernel_size - 1) * dilation - (stride - 1)
|
| 282 |
+
|
| 283 |
+
# For non-streaming mode, calculate padding
|
| 284 |
+
self.padding_total = (kernel_size - 1) * dilation - (stride - 1)
|
| 285 |
+
|
| 286 |
+
# Create a unique layer ID for cache management
|
| 287 |
+
self._layer_id = None
|
| 288 |
+
|
| 289 |
+
@property
|
| 290 |
+
def layer_id(self):
|
| 291 |
+
if self._layer_id is None:
|
| 292 |
+
self._layer_id = f"sconv1d_{id(self)}"
|
| 293 |
+
return self._layer_id
|
| 294 |
+
|
| 295 |
+
def forward(self, x: torch.Tensor,
|
| 296 |
+
cache: Optional[VibeVoiceTokenizerStreamingCache] = None,
|
| 297 |
+
sample_indices: Optional[torch.Tensor] = None,
|
| 298 |
+
use_cache: bool = False,
|
| 299 |
+
debug: bool = False,
|
| 300 |
+
is_final_chunk: bool = False) -> torch.Tensor:
|
| 301 |
+
"""
|
| 302 |
+
Forward pass with optional streaming support via cache.
|
| 303 |
+
|
| 304 |
+
Args:
|
| 305 |
+
x: Input tensor [batch_size, channels, time]
|
| 306 |
+
cache: VibeVoiceTokenizerStreamingCache object for maintaining states
|
| 307 |
+
sample_indices: Indices identifying each sample for cache management
|
| 308 |
+
use_cache: Whether to use cached states for streaming
|
| 309 |
+
debug: Whether to print debug information
|
| 310 |
+
is_final_chunk: Whether this is the final chunk (adds extra padding for alignment)
|
| 311 |
+
|
| 312 |
+
Returns:
|
| 313 |
+
Output tensor
|
| 314 |
+
"""
|
| 315 |
+
B, C, T = x.shape
|
| 316 |
+
|
| 317 |
+
# Non-streaming mode
|
| 318 |
+
if not use_cache or cache is None:
|
| 319 |
+
return self._forward_non_streaming(x, debug=debug)
|
| 320 |
+
|
| 321 |
+
# Streaming mode
|
| 322 |
+
assert self.causal, "Streaming mode is only supported for causal convolutions"
|
| 323 |
+
assert sample_indices is not None, "sample_indices must be provided for streaming mode"
|
| 324 |
+
assert len(sample_indices) == B, "sample_indices must match batch size"
|
| 325 |
+
|
| 326 |
+
return self._forward_streaming(x, cache, sample_indices, debug, is_final_chunk)
|
| 327 |
+
|
| 328 |
+
def _forward_streaming(self, x: torch.Tensor,
|
| 329 |
+
cache: VibeVoiceTokenizerStreamingCache,
|
| 330 |
+
sample_indices: torch.Tensor,
|
| 331 |
+
debug: bool = False,
|
| 332 |
+
is_final_chunk: bool = False) -> torch.Tensor:
|
| 333 |
+
"""Streaming forward pass with cache operations kept separate from compiled code"""
|
| 334 |
+
B, C, T = x.shape
|
| 335 |
+
|
| 336 |
+
# Cache operations (not compiled)
|
| 337 |
+
cached_states = cache.get(self.layer_id, sample_indices)
|
| 338 |
+
|
| 339 |
+
if cached_states is None:
|
| 340 |
+
# First chunk - initialize with zeros for context
|
| 341 |
+
if self.context_size > 0:
|
| 342 |
+
cached_states = torch.zeros(B, C, self.context_size, device=x.device, dtype=x.dtype)
|
| 343 |
+
if debug:
|
| 344 |
+
print(f"[DEBUG] Initialized cache with shape: {cached_states.shape}, context_size={self.context_size}")
|
| 345 |
+
else:
|
| 346 |
+
cached_states = torch.zeros(B, C, 0, device=x.device, dtype=x.dtype)
|
| 347 |
+
if debug:
|
| 348 |
+
print(f"[DEBUG] No context needed (kernel_size=stride)")
|
| 349 |
+
|
| 350 |
+
# Concatenate cached states with input
|
| 351 |
+
if cached_states.shape[2] > 0:
|
| 352 |
+
input_with_context = torch.cat([cached_states, x], dim=2)
|
| 353 |
+
else:
|
| 354 |
+
input_with_context = x
|
| 355 |
+
|
| 356 |
+
# For final chunk, add extra padding to ensure ceil behavior (same as non-streaming)
|
| 357 |
+
if is_final_chunk:
|
| 358 |
+
extra_padding = get_extra_padding_for_conv1d(
|
| 359 |
+
input_with_context, self.kernel_size, self.stride, self.padding_total
|
| 360 |
+
)
|
| 361 |
+
if extra_padding > 0:
|
| 362 |
+
input_with_context = pad1d(input_with_context, (0, extra_padding), mode=self.pad_mode)
|
| 363 |
+
if debug:
|
| 364 |
+
print(f"[DEBUG] Final chunk: added extra_padding={extra_padding}")
|
| 365 |
+
|
| 366 |
+
if debug:
|
| 367 |
+
print(f"[DEBUG] Input shape: {x.shape}, Cache shape: {cached_states.shape}, Combined: {input_with_context.shape}")
|
| 368 |
+
|
| 369 |
+
# Apply convolution directly - no extra padding in streaming mode
|
| 370 |
+
# The conv layer will handle its own padding internally
|
| 371 |
+
output = self.conv(input_with_context)
|
| 372 |
+
|
| 373 |
+
if debug:
|
| 374 |
+
print(f"[DEBUG] Output shape: {output.shape}")
|
| 375 |
+
|
| 376 |
+
# Update cache for next chunk
|
| 377 |
+
if self.context_size > 0:
|
| 378 |
+
# Calculate how many samples to keep
|
| 379 |
+
total_input_length = input_with_context.shape[2]
|
| 380 |
+
|
| 381 |
+
# Keep the last context_size samples
|
| 382 |
+
if total_input_length >= self.context_size:
|
| 383 |
+
new_cache_start = total_input_length - self.context_size
|
| 384 |
+
new_cache = input_with_context[:, :, new_cache_start:]
|
| 385 |
+
else:
|
| 386 |
+
# If we have less than context_size samples, keep everything
|
| 387 |
+
new_cache = input_with_context
|
| 388 |
+
|
| 389 |
+
if debug:
|
| 390 |
+
print(f"[DEBUG] New cache shape: {new_cache.shape}")
|
| 391 |
+
|
| 392 |
+
cache.set(self.layer_id, sample_indices, new_cache)
|
| 393 |
+
|
| 394 |
+
return output
|
| 395 |
+
|
| 396 |
+
def _forward_non_streaming(self, x: torch.Tensor, debug: bool = False) -> torch.Tensor:
|
| 397 |
+
"""Standard forward pass without streaming"""
|
| 398 |
+
B, C, T = x.shape
|
| 399 |
+
kernel_size = self.kernel_size
|
| 400 |
+
stride = self.stride
|
| 401 |
+
dilation = self.dilation
|
| 402 |
+
padding_total = self.padding_total
|
| 403 |
+
|
| 404 |
+
# Compute extra padding for stride alignment
|
| 405 |
+
extra_padding = get_extra_padding_for_conv1d(x, kernel_size, stride, padding_total)
|
| 406 |
+
|
| 407 |
+
if debug:
|
| 408 |
+
print(f"[DEBUG NON-STREAMING] Input shape: {x.shape}, padding_total={padding_total}, extra_padding={extra_padding}")
|
| 409 |
+
|
| 410 |
+
if self.causal:
|
| 411 |
+
# Left padding for causal
|
| 412 |
+
if self.pad_mode == 'constant':
|
| 413 |
+
x = pad1d(x, (padding_total, extra_padding), mode=self.pad_mode, value=0)
|
| 414 |
+
else:
|
| 415 |
+
x = pad1d(x, (padding_total, extra_padding), mode=self.pad_mode)
|
| 416 |
+
else:
|
| 417 |
+
# Symmetric padding for non-causal
|
| 418 |
+
padding_right = padding_total // 2
|
| 419 |
+
padding_left = padding_total - padding_right
|
| 420 |
+
x = pad1d(x, (padding_left, padding_right + extra_padding), mode=self.pad_mode)
|
| 421 |
+
|
| 422 |
+
if debug:
|
| 423 |
+
print(f"[DEBUG NON-STREAMING] After padding: {x.shape}")
|
| 424 |
+
|
| 425 |
+
output = self.conv(x)
|
| 426 |
+
|
| 427 |
+
if debug:
|
| 428 |
+
print(f"[DEBUG NON-STREAMING] Output shape: {output.shape}")
|
| 429 |
+
|
| 430 |
+
return output
|
| 431 |
+
|
| 432 |
+
|
| 433 |
+
class SConvTranspose1d(nn.Module):
|
| 434 |
+
"""ConvTranspose1d with built-in handling of asymmetric or causal padding and normalization."""
|
| 435 |
+
def __init__(self, in_channels: int, out_channels: int,
|
| 436 |
+
kernel_size: int, stride: int = 1, causal: bool = False,
|
| 437 |
+
norm: str = 'none', trim_right_ratio: float = 1.,
|
| 438 |
+
norm_kwargs: tp.Dict[str, tp.Any] = {}, bias: bool = True):
|
| 439 |
+
super().__init__()
|
| 440 |
+
self.convtr = NormConvTranspose1d(in_channels, out_channels, kernel_size, stride,
|
| 441 |
+
causal=causal, norm=norm, norm_kwargs=norm_kwargs, bias=bias)
|
| 442 |
+
self.causal = causal
|
| 443 |
+
self.trim_right_ratio = trim_right_ratio
|
| 444 |
+
assert self.causal or self.trim_right_ratio == 1., \
|
| 445 |
+
"`trim_right_ratio` != 1.0 only makes sense for causal convolutions"
|
| 446 |
+
assert self.trim_right_ratio >= 0. and self.trim_right_ratio <= 1.
|
| 447 |
+
|
| 448 |
+
# Store configuration
|
| 449 |
+
self.kernel_size = kernel_size
|
| 450 |
+
self.stride = stride
|
| 451 |
+
self.in_channels = in_channels
|
| 452 |
+
self.out_channels = out_channels
|
| 453 |
+
|
| 454 |
+
# For transposed convolution, padding calculation is different
|
| 455 |
+
self.padding_total = kernel_size - stride
|
| 456 |
+
|
| 457 |
+
# For streaming, we need to keep track of input history
|
| 458 |
+
# Transposed conv needs to see multiple input samples to produce correct output
|
| 459 |
+
self.context_size = kernel_size - 1
|
| 460 |
+
|
| 461 |
+
# Create a unique layer ID for cache management
|
| 462 |
+
self._layer_id = None
|
| 463 |
+
|
| 464 |
+
@property
|
| 465 |
+
def layer_id(self):
|
| 466 |
+
if self._layer_id is None:
|
| 467 |
+
self._layer_id = f"sconvtr1d_{id(self)}"
|
| 468 |
+
return self._layer_id
|
| 469 |
+
|
| 470 |
+
def forward(self, x: torch.Tensor,
|
| 471 |
+
cache: Optional[VibeVoiceTokenizerStreamingCache] = None,
|
| 472 |
+
sample_indices: Optional[torch.Tensor] = None,
|
| 473 |
+
use_cache: bool = False,
|
| 474 |
+
debug: bool = False) -> torch.Tensor:
|
| 475 |
+
"""
|
| 476 |
+
Forward pass with optional streaming support via cache.
|
| 477 |
+
"""
|
| 478 |
+
B, C, T = x.shape
|
| 479 |
+
|
| 480 |
+
# Non-streaming mode
|
| 481 |
+
if not use_cache or cache is None:
|
| 482 |
+
return self._forward_non_streaming(x, debug=debug)
|
| 483 |
+
|
| 484 |
+
# Streaming mode
|
| 485 |
+
assert sample_indices is not None, "sample_indices must be provided for streaming mode"
|
| 486 |
+
assert len(sample_indices) == B, "sample_indices must match batch size"
|
| 487 |
+
|
| 488 |
+
return self._forward_streaming(x, cache, sample_indices, debug)
|
| 489 |
+
|
| 490 |
+
def _forward_streaming(self, x: torch.Tensor,
|
| 491 |
+
cache: VibeVoiceTokenizerStreamingCache,
|
| 492 |
+
sample_indices: torch.Tensor,
|
| 493 |
+
debug: bool = False) -> torch.Tensor:
|
| 494 |
+
"""Streaming forward pass with cache operations kept separate from compiled code"""
|
| 495 |
+
B, C, T = x.shape
|
| 496 |
+
|
| 497 |
+
# Cache operations (not compiled)
|
| 498 |
+
cached_input = cache.get(self.layer_id, sample_indices)
|
| 499 |
+
|
| 500 |
+
if cached_input is None:
|
| 501 |
+
# First chunk - no history yet
|
| 502 |
+
cached_input = torch.zeros(B, C, 0, device=x.device, dtype=x.dtype)
|
| 503 |
+
if debug:
|
| 504 |
+
print(f"[DEBUG] Initialized empty cache for transposed conv")
|
| 505 |
+
|
| 506 |
+
# Concatenate cached input with new input
|
| 507 |
+
full_input = torch.cat([cached_input, x], dim=2)
|
| 508 |
+
|
| 509 |
+
if debug:
|
| 510 |
+
print(f"[DEBUG] Input shape: {x.shape}, Cache shape: {cached_input.shape}, Combined: {full_input.shape}")
|
| 511 |
+
|
| 512 |
+
# First chunk or debug mode - use uncompiled version
|
| 513 |
+
full_output = self.convtr(full_input)
|
| 514 |
+
|
| 515 |
+
if debug:
|
| 516 |
+
print(f"[DEBUG] Full transposed conv output shape: {full_output.shape}")
|
| 517 |
+
|
| 518 |
+
# Calculate padding to remove
|
| 519 |
+
if self.causal:
|
| 520 |
+
padding_right = math.ceil(self.padding_total * self.trim_right_ratio)
|
| 521 |
+
padding_left = self.padding_total - padding_right
|
| 522 |
+
else:
|
| 523 |
+
padding_right = self.padding_total // 2
|
| 524 |
+
padding_left = self.padding_total - padding_right
|
| 525 |
+
|
| 526 |
+
# Remove padding
|
| 527 |
+
if padding_left + padding_right > 0:
|
| 528 |
+
full_output = unpad1d(full_output, (padding_left, padding_right))
|
| 529 |
+
|
| 530 |
+
if debug:
|
| 531 |
+
print(f"[DEBUG] After unpadding: {full_output.shape}")
|
| 532 |
+
|
| 533 |
+
# Determine which part of the output corresponds to the new input
|
| 534 |
+
if cached_input.shape[2] == 0:
|
| 535 |
+
# First chunk - return all output
|
| 536 |
+
output = full_output
|
| 537 |
+
else:
|
| 538 |
+
# Subsequent chunks - return only the new output
|
| 539 |
+
expected_new_output = T * self.stride
|
| 540 |
+
|
| 541 |
+
# Take the last expected_new_output samples
|
| 542 |
+
if full_output.shape[2] >= expected_new_output:
|
| 543 |
+
output = full_output[:, :, -expected_new_output:]
|
| 544 |
+
else:
|
| 545 |
+
output = full_output
|
| 546 |
+
|
| 547 |
+
if debug:
|
| 548 |
+
print(f"[DEBUG] Final streaming output shape: {output.shape}")
|
| 549 |
+
|
| 550 |
+
# Update cache
|
| 551 |
+
if full_input.shape[2] > self.context_size:
|
| 552 |
+
new_cache = full_input[:, :, -self.context_size:]
|
| 553 |
+
else:
|
| 554 |
+
new_cache = full_input
|
| 555 |
+
|
| 556 |
+
if debug:
|
| 557 |
+
print(f"[DEBUG] New cache shape: {new_cache.shape}")
|
| 558 |
+
|
| 559 |
+
cache.set(self.layer_id, sample_indices, new_cache)
|
| 560 |
+
|
| 561 |
+
return output
|
| 562 |
+
|
| 563 |
+
def _forward_non_streaming(self, x: torch.Tensor, debug: bool = False) -> torch.Tensor:
|
| 564 |
+
"""Standard forward pass without streaming"""
|
| 565 |
+
if debug:
|
| 566 |
+
print(f"[DEBUG NON-STREAMING] Input shape: {x.shape}")
|
| 567 |
+
|
| 568 |
+
# Apply transposed convolution
|
| 569 |
+
y = self.convtr(x)
|
| 570 |
+
|
| 571 |
+
if debug:
|
| 572 |
+
print(f"[DEBUG NON-STREAMING] After transposed conv: {y.shape}")
|
| 573 |
+
|
| 574 |
+
# Calculate and remove padding
|
| 575 |
+
if self.causal:
|
| 576 |
+
padding_right = math.ceil(self.padding_total * self.trim_right_ratio)
|
| 577 |
+
padding_left = self.padding_total - padding_right
|
| 578 |
+
else:
|
| 579 |
+
padding_right = self.padding_total // 2
|
| 580 |
+
padding_left = self.padding_total - padding_right
|
| 581 |
+
|
| 582 |
+
if padding_left + padding_right > 0:
|
| 583 |
+
y = unpad1d(y, (padding_left, padding_right))
|
| 584 |
+
|
| 585 |
+
if debug:
|
| 586 |
+
print(f"[DEBUG NON-STREAMING] Final output shape: {y.shape}")
|
| 587 |
+
|
| 588 |
+
return y
|
| 589 |
+
|
| 590 |
+
# FFN
|
| 591 |
+
class FFN(nn.Module):
|
| 592 |
+
def __init__(
|
| 593 |
+
self,
|
| 594 |
+
embed_dim,
|
| 595 |
+
ffn_dim,
|
| 596 |
+
bias=False,
|
| 597 |
+
):
|
| 598 |
+
super().__init__()
|
| 599 |
+
self.embed_dim = embed_dim
|
| 600 |
+
self.linear1 = nn.Linear(self.embed_dim, ffn_dim, bias=bias)
|
| 601 |
+
self.gelu = ACT2FN["gelu"]
|
| 602 |
+
self.linear2 = nn.Linear(ffn_dim, self.embed_dim, bias=bias)
|
| 603 |
+
|
| 604 |
+
def forward(self, x):
|
| 605 |
+
x = self.linear1(x)
|
| 606 |
+
x = self.gelu(x)
|
| 607 |
+
x = self.linear2(x)
|
| 608 |
+
return x
|
| 609 |
+
|
| 610 |
+
|
| 611 |
+
class Convlayer(nn.Module):
|
| 612 |
+
def __init__(
|
| 613 |
+
self,
|
| 614 |
+
in_channels,
|
| 615 |
+
out_channels,
|
| 616 |
+
kernel_size,
|
| 617 |
+
stride=1,
|
| 618 |
+
dilation=1,
|
| 619 |
+
groups=1,
|
| 620 |
+
bias=True,
|
| 621 |
+
pad_mode='zeros',
|
| 622 |
+
norm='weight_norm',
|
| 623 |
+
causal=True,
|
| 624 |
+
):
|
| 625 |
+
super().__init__()
|
| 626 |
+
self.conv = SConv1d(in_channels, out_channels, kernel_size, stride=stride, dilation=dilation,
|
| 627 |
+
groups=groups, bias=bias, pad_mode=pad_mode, norm=norm, causal=causal)
|
| 628 |
+
|
| 629 |
+
def forward(self, x):
|
| 630 |
+
return self.conv(x)
|
| 631 |
+
|
| 632 |
+
class Block1D(nn.Module):
|
| 633 |
+
def __init__(self, dim, kernel_size=7, drop_path=0., mixer_layer='conv',
|
| 634 |
+
layer_scale_init_value=1e-6, **kwargs):
|
| 635 |
+
super().__init__()
|
| 636 |
+
|
| 637 |
+
if kwargs.get('layernorm', 'LN') == 'LN':
|
| 638 |
+
self.norm = ConvLayerNorm(dim, eps=kwargs.get('eps', 1e-6))
|
| 639 |
+
self.ffn_norm = ConvLayerNorm(dim, eps=kwargs.get('eps', 1e-6))
|
| 640 |
+
elif kwargs.get('layernorm', 'RMSNorm') == 'RMSNorm':
|
| 641 |
+
self.norm = ConvRMSNorm(dim, eps=kwargs.get('eps', 1e-6))
|
| 642 |
+
self.ffn_norm = ConvRMSNorm(dim, eps=kwargs.get('eps', 1e-6))
|
| 643 |
+
|
| 644 |
+
if mixer_layer == 'conv':
|
| 645 |
+
self.mixer = Convlayer(dim, dim, groups=kwargs.get('groups', 1),
|
| 646 |
+
kernel_size=kernel_size,
|
| 647 |
+
pad_mode=kwargs.get('pad_mode', 'reflect'),
|
| 648 |
+
norm=kwargs.get('norm', 'none'),
|
| 649 |
+
causal=kwargs.get('causal', True),
|
| 650 |
+
bias=kwargs.get('bias', True),
|
| 651 |
+
)
|
| 652 |
+
elif mixer_layer == 'depthwise_conv':
|
| 653 |
+
self.mixer = Convlayer(dim, dim, groups=dim,
|
| 654 |
+
kernel_size=kernel_size,
|
| 655 |
+
pad_mode=kwargs.get('pad_mode', 'reflect'),
|
| 656 |
+
norm=kwargs.get('norm', 'none'),
|
| 657 |
+
causal=kwargs.get('causal', True),
|
| 658 |
+
bias=kwargs.get('bias', True),
|
| 659 |
+
)
|
| 660 |
+
else:
|
| 661 |
+
raise ValueError(f"Unsupported mixer layer: {mixer_layer}")
|
| 662 |
+
|
| 663 |
+
self.ffn = FFN(
|
| 664 |
+
dim,
|
| 665 |
+
kwargs.get('ffn_expansion', 4) * dim,
|
| 666 |
+
bias=kwargs.get('bias', False),
|
| 667 |
+
)
|
| 668 |
+
self.drop_path = nn.Identity() if drop_path <= 0. else nn.modules.DropPath(drop_path)
|
| 669 |
+
|
| 670 |
+
if layer_scale_init_value > 0:
|
| 671 |
+
self.gamma = nn.Parameter(layer_scale_init_value * torch.ones((dim)), requires_grad=True)
|
| 672 |
+
self.ffn_gamma = nn.Parameter(layer_scale_init_value * torch.ones((dim)), requires_grad=True)
|
| 673 |
+
else:
|
| 674 |
+
self.gamma = None
|
| 675 |
+
self.ffn_gamma = None
|
| 676 |
+
|
| 677 |
+
def forward(self, x):
|
| 678 |
+
# mixer
|
| 679 |
+
residual = x
|
| 680 |
+
x = self.norm(x)
|
| 681 |
+
x = self.mixer(x)
|
| 682 |
+
if self.gamma is not None:
|
| 683 |
+
x = x * self.gamma.unsqueeze(-1)
|
| 684 |
+
x = residual + self.drop_path(x)
|
| 685 |
+
|
| 686 |
+
# ffn
|
| 687 |
+
residual = x
|
| 688 |
+
x = self.ffn_norm(x)
|
| 689 |
+
x = x.permute(0, 2, 1)
|
| 690 |
+
x = self.ffn(x)
|
| 691 |
+
x = x.permute(0, 2, 1)
|
| 692 |
+
if self.ffn_gamma is not None:
|
| 693 |
+
x = x * self.ffn_gamma.unsqueeze(-1)
|
| 694 |
+
x = residual + self.drop_path(x)
|
| 695 |
+
|
| 696 |
+
return x
|
| 697 |
+
|
| 698 |
+
|
| 699 |
+
class TokenizerEncoder(nn.Module):
|
| 700 |
+
"""
|
| 701 |
+
Encoder component for the VibeVoice tokenizer that converts audio to latent representations.
|
| 702 |
+
|
| 703 |
+
Args:
|
| 704 |
+
config: Configuration object with model parameters
|
| 705 |
+
"""
|
| 706 |
+
def __init__(self, config):
|
| 707 |
+
super().__init__()
|
| 708 |
+
|
| 709 |
+
# Extract parameters from config
|
| 710 |
+
self.channels = config.channels
|
| 711 |
+
self.dimension = config.dimension
|
| 712 |
+
self.n_filters = config.n_filters
|
| 713 |
+
self.ratios = list(reversed(config.ratios))
|
| 714 |
+
self.depths = config.depths
|
| 715 |
+
self.n_residual_layers = getattr(config, "n_residual_layers", 1)
|
| 716 |
+
self.hop_length = np.prod(self.ratios)
|
| 717 |
+
self.causal = config.causal
|
| 718 |
+
|
| 719 |
+
# Additional config parameters with defaults
|
| 720 |
+
kernel_size = getattr(config, "kernel_size", 7)
|
| 721 |
+
last_kernel_size = getattr(config, "last_kernel_size", 7)
|
| 722 |
+
norm = getattr(config, "norm", "none")
|
| 723 |
+
norm_params = getattr(config, "norm_params", {})
|
| 724 |
+
pad_mode = getattr(config, "pad_mode", "reflect")
|
| 725 |
+
bias = getattr(config, "bias", True)
|
| 726 |
+
layernorm = getattr(config, "layernorm", "LN")
|
| 727 |
+
layernorm_eps = getattr(config, "layernorm_eps", 1e-6)
|
| 728 |
+
layernorm_elementwise_affine = getattr(config, "layernorm_elementwise_affine", True)
|
| 729 |
+
drop_path_rate = getattr(config, "drop_path_rate", 0.0)
|
| 730 |
+
mixer_layer = getattr(config, "mixer_layer", "conv")
|
| 731 |
+
layer_scale_init_value = getattr(config, "layer_scale_init_value", 0)
|
| 732 |
+
disable_last_norm = getattr(config, "disable_last_norm", False)
|
| 733 |
+
|
| 734 |
+
# determine the norm type based on layernorm
|
| 735 |
+
if layernorm == 'LN':
|
| 736 |
+
norm_type = ConvLayerNorm
|
| 737 |
+
elif layernorm == 'RMSNorm':
|
| 738 |
+
norm_type = partial(ConvRMSNorm, elementwise_affine=layernorm_elementwise_affine)
|
| 739 |
+
else:
|
| 740 |
+
raise ValueError(f"Unsupported norm type: {layernorm}")
|
| 741 |
+
|
| 742 |
+
# stem and intermediate downsampling conv layers
|
| 743 |
+
stem = nn.Sequential(
|
| 744 |
+
SConv1d(self.channels, self.n_filters, kernel_size, norm=norm, norm_kwargs=norm_params, causal=self.causal, pad_mode=pad_mode, bias=bias),
|
| 745 |
+
)
|
| 746 |
+
|
| 747 |
+
self.downsample_layers = nn.ModuleList()
|
| 748 |
+
self.downsample_layers.append(stem)
|
| 749 |
+
for i in range(len(self.ratios)):
|
| 750 |
+
in_ch = self.n_filters * (2 ** i)
|
| 751 |
+
out_ch = self.n_filters * (2 ** (i + 1))
|
| 752 |
+
downsample_layer = nn.Sequential(
|
| 753 |
+
SConv1d(in_ch, out_ch, kernel_size=self.ratios[i] * 2, stride=self.ratios[i], causal=self.causal, pad_mode=pad_mode, norm=norm, bias=bias)
|
| 754 |
+
)
|
| 755 |
+
self.downsample_layers.append(downsample_layer)
|
| 756 |
+
|
| 757 |
+
# configure the transformer blocks
|
| 758 |
+
layer_type = partial(
|
| 759 |
+
Block1D,
|
| 760 |
+
mixer_layer=mixer_layer,
|
| 761 |
+
layernorm=layernorm,
|
| 762 |
+
eps=layernorm_eps,
|
| 763 |
+
causal=self.causal,
|
| 764 |
+
pad_mode=pad_mode,
|
| 765 |
+
norm=norm,
|
| 766 |
+
bias=bias,
|
| 767 |
+
layer_scale_init_value=layer_scale_init_value,
|
| 768 |
+
)
|
| 769 |
+
|
| 770 |
+
self.stages = nn.ModuleList()
|
| 771 |
+
dp_rates = [x.item() for x in torch.linspace(0, drop_path_rate, sum(self.depths))]
|
| 772 |
+
cur = 0
|
| 773 |
+
|
| 774 |
+
for i in range(len(self.depths)):
|
| 775 |
+
in_ch = self.n_filters * (2 ** i)
|
| 776 |
+
stage = nn.Sequential(
|
| 777 |
+
*[layer_type(dim=in_ch, drop_path=dp_rates[cur + j]) for j in range(self.depths[i])]
|
| 778 |
+
)
|
| 779 |
+
self.stages.append(stage)
|
| 780 |
+
cur += self.depths[i]
|
| 781 |
+
|
| 782 |
+
if not disable_last_norm:
|
| 783 |
+
self.norm = norm_type(in_ch, eps=layernorm_eps)
|
| 784 |
+
else:
|
| 785 |
+
self.norm = nn.Identity()
|
| 786 |
+
self.head = SConv1d(in_ch, self.dimension, kernel_size=last_kernel_size, causal=self.causal, pad_mode=pad_mode, norm=norm, bias=bias)
|
| 787 |
+
|
| 788 |
+
def forward_features(self, x, cache=None, sample_indices=None, use_cache=False, debug=False, is_final_chunk=False):
|
| 789 |
+
for i in range(len(self.depths)):
|
| 790 |
+
# Apply downsampling
|
| 791 |
+
for layer in self.downsample_layers[i]:
|
| 792 |
+
if isinstance(layer, SConv1d):
|
| 793 |
+
x = layer(x, cache=cache, sample_indices=sample_indices, use_cache=use_cache, debug=debug, is_final_chunk=is_final_chunk)
|
| 794 |
+
else:
|
| 795 |
+
x = layer(x)
|
| 796 |
+
|
| 797 |
+
# Apply stage (Block1D contains Convlayer which contains SConv1d)
|
| 798 |
+
for block in self.stages[i]:
|
| 799 |
+
if hasattr(block, 'mixer') and hasattr(block.mixer, 'conv') and isinstance(block.mixer.conv, SConv1d):
|
| 800 |
+
# Block1D forward with cache support
|
| 801 |
+
residual = x
|
| 802 |
+
x = block.norm(x)
|
| 803 |
+
x = block.mixer.conv(x, cache=cache, sample_indices=sample_indices, use_cache=use_cache, debug=debug, is_final_chunk=is_final_chunk)
|
| 804 |
+
if block.gamma is not None:
|
| 805 |
+
x = x * block.gamma.unsqueeze(-1)
|
| 806 |
+
x = residual + x
|
| 807 |
+
|
| 808 |
+
# FFN part
|
| 809 |
+
residual = x
|
| 810 |
+
x = block.ffn_norm(x)
|
| 811 |
+
x = x.permute(0, 2, 1)
|
| 812 |
+
x = block.ffn(x)
|
| 813 |
+
x = x.permute(0, 2, 1)
|
| 814 |
+
if block.ffn_gamma is not None:
|
| 815 |
+
x = x * block.ffn_gamma.unsqueeze(-1)
|
| 816 |
+
x = residual + x
|
| 817 |
+
else:
|
| 818 |
+
x = block(x)
|
| 819 |
+
|
| 820 |
+
return self.norm(x)
|
| 821 |
+
|
| 822 |
+
def forward(self, x, cache=None, sample_indices=None, use_cache=False, debug=False, is_final_chunk=False):
|
| 823 |
+
x = self.forward_features(x, cache=cache, sample_indices=sample_indices, use_cache=use_cache, debug=debug, is_final_chunk=is_final_chunk)
|
| 824 |
+
x = self.head(x, cache=cache, sample_indices=sample_indices, use_cache=use_cache, debug=debug, is_final_chunk=is_final_chunk)
|
| 825 |
+
return x
|
| 826 |
+
|
| 827 |
+
|
| 828 |
+
class TokenizerDecoder(nn.Module):
|
| 829 |
+
"""
|
| 830 |
+
Decoder component for the VibeVoice tokenizer that converts latent representations back to audio.
|
| 831 |
+
|
| 832 |
+
Args:
|
| 833 |
+
config: Configuration object with model parameters
|
| 834 |
+
"""
|
| 835 |
+
def __init__(self, config):
|
| 836 |
+
super().__init__()
|
| 837 |
+
|
| 838 |
+
# Extract parameters from config
|
| 839 |
+
self.dimension = config.dimension
|
| 840 |
+
self.channels = config.channels
|
| 841 |
+
self.n_filters = config.n_filters
|
| 842 |
+
self.ratios = config.ratios
|
| 843 |
+
|
| 844 |
+
# IMPORTANT CHANGE: Don't reverse depths again since they're already reversed in VibeVoiceAcousticTokenizerModel
|
| 845 |
+
self.depths = config.depths # Changed from list(reversed(config.depths))
|
| 846 |
+
|
| 847 |
+
self.n_residual_layers = getattr(config, "n_residual_layers", 1)
|
| 848 |
+
self.hop_length = np.prod(self.ratios)
|
| 849 |
+
self.causal = config.causal
|
| 850 |
+
|
| 851 |
+
# Additional config parameters with defaults
|
| 852 |
+
kernel_size = getattr(config, "kernel_size", 7)
|
| 853 |
+
last_kernel_size = getattr(config, "last_kernel_size", 7)
|
| 854 |
+
norm = getattr(config, "norm", "none")
|
| 855 |
+
norm_params = getattr(config, "norm_params", {})
|
| 856 |
+
pad_mode = getattr(config, "pad_mode", "reflect")
|
| 857 |
+
bias = getattr(config, "bias", True)
|
| 858 |
+
layernorm = getattr(config, "layernorm", "LN")
|
| 859 |
+
layernorm_eps = getattr(config, "layernorm_eps", 1e-6)
|
| 860 |
+
trim_right_ratio = getattr(config, "trim_right_ratio", 1.0)
|
| 861 |
+
layernorm_elementwise_affine = getattr(config, "layernorm_elementwise_affine", True)
|
| 862 |
+
drop_path_rate = getattr(config, "drop_path_rate", 0.0)
|
| 863 |
+
mixer_layer = getattr(config, "mixer_layer", "conv")
|
| 864 |
+
layer_scale_init_value = getattr(config, "layer_scale_init_value", 0)
|
| 865 |
+
disable_last_norm = getattr(config, "disable_last_norm", False)
|
| 866 |
+
|
| 867 |
+
# determine the norm type based on layernorm
|
| 868 |
+
if layernorm == 'LN':
|
| 869 |
+
norm_type = ConvLayerNorm
|
| 870 |
+
elif layernorm == 'RMSNorm':
|
| 871 |
+
norm_type = partial(ConvRMSNorm, elementwise_affine=layernorm_elementwise_affine)
|
| 872 |
+
else:
|
| 873 |
+
raise ValueError(f"Unsupported norm type: {layernorm}")
|
| 874 |
+
|
| 875 |
+
# stem and upsampling layers
|
| 876 |
+
stem = nn.Sequential(
|
| 877 |
+
SConv1d(self.dimension, self.n_filters * 2 ** (len(self.depths) - 1), kernel_size, norm=norm,
|
| 878 |
+
norm_kwargs=norm_params, causal=self.causal, pad_mode=pad_mode, bias=bias),
|
| 879 |
+
)
|
| 880 |
+
|
| 881 |
+
self.upsample_layers = nn.ModuleList()
|
| 882 |
+
self.upsample_layers.append(stem)
|
| 883 |
+
for i in range(len(self.ratios)):
|
| 884 |
+
in_ch = self.n_filters * (2 ** (len(self.depths) - 1 - i))
|
| 885 |
+
out_ch = self.n_filters * (2 ** (len(self.depths) - 1 - i - 1))
|
| 886 |
+
upsample_layer = nn.Sequential(
|
| 887 |
+
SConvTranspose1d(in_ch, out_ch,
|
| 888 |
+
kernel_size=self.ratios[i] * 2, stride=self.ratios[i],
|
| 889 |
+
norm=norm, norm_kwargs=norm_params, bias=bias,
|
| 890 |
+
causal=self.causal, trim_right_ratio=trim_right_ratio),
|
| 891 |
+
)
|
| 892 |
+
self.upsample_layers.append(upsample_layer)
|
| 893 |
+
|
| 894 |
+
# configure transformer blocks
|
| 895 |
+
layer_type = partial(
|
| 896 |
+
Block1D,
|
| 897 |
+
mixer_layer=mixer_layer,
|
| 898 |
+
layernorm=layernorm,
|
| 899 |
+
eps=layernorm_eps,
|
| 900 |
+
causal=self.causal,
|
| 901 |
+
pad_mode=pad_mode,
|
| 902 |
+
norm=norm,
|
| 903 |
+
bias=bias,
|
| 904 |
+
layer_scale_init_value=layer_scale_init_value,
|
| 905 |
+
)
|
| 906 |
+
|
| 907 |
+
self.stages = nn.ModuleList()
|
| 908 |
+
dp_rates = [x.item() for x in torch.linspace(0, drop_path_rate, sum(self.depths))]
|
| 909 |
+
cur = 0
|
| 910 |
+
|
| 911 |
+
# Create stages in the same order as the original model
|
| 912 |
+
for i in range(len(self.depths)):
|
| 913 |
+
in_ch = self.n_filters * (2 ** (len(self.depths) - 1 - i))
|
| 914 |
+
stage = nn.Sequential(
|
| 915 |
+
*[layer_type(dim=in_ch, drop_path=dp_rates[cur + j]) for j in range(self.depths[i])]
|
| 916 |
+
)
|
| 917 |
+
self.stages.append(stage)
|
| 918 |
+
cur += self.depths[i]
|
| 919 |
+
|
| 920 |
+
if not disable_last_norm:
|
| 921 |
+
self.norm = norm_type(in_ch, eps=layernorm_eps)
|
| 922 |
+
else:
|
| 923 |
+
self.norm = nn.Identity()
|
| 924 |
+
self.head = SConv1d(in_ch, self.channels, kernel_size=last_kernel_size, causal=self.causal, pad_mode=pad_mode, norm=norm, bias=bias)
|
| 925 |
+
|
| 926 |
+
def forward_features(self, x, cache=None, sample_indices=None, use_cache=False, debug=False):
|
| 927 |
+
for i in range(len(self.depths)):
|
| 928 |
+
# Apply upsampling
|
| 929 |
+
for layer in self.upsample_layers[i]:
|
| 930 |
+
if isinstance(layer, (SConv1d, SConvTranspose1d)):
|
| 931 |
+
x = layer(x, cache=cache, sample_indices=sample_indices, use_cache=use_cache, debug=debug)
|
| 932 |
+
else:
|
| 933 |
+
x = layer(x)
|
| 934 |
+
|
| 935 |
+
# Apply stage (Block1D contains Convlayer which contains SConv1d)
|
| 936 |
+
for block in self.stages[i]:
|
| 937 |
+
if hasattr(block, 'mixer') and hasattr(block.mixer, 'conv') and isinstance(block.mixer.conv, SConv1d):
|
| 938 |
+
# Block1D forward with cache support
|
| 939 |
+
residual = x
|
| 940 |
+
x = block.norm(x)
|
| 941 |
+
x = block.mixer.conv(x, cache=cache, sample_indices=sample_indices, use_cache=use_cache, debug=debug)
|
| 942 |
+
if block.gamma is not None:
|
| 943 |
+
x = x * block.gamma.unsqueeze(-1)
|
| 944 |
+
x = residual + x
|
| 945 |
+
|
| 946 |
+
# FFN part
|
| 947 |
+
residual = x
|
| 948 |
+
x = block.ffn_norm(x)
|
| 949 |
+
x = x.permute(0, 2, 1)
|
| 950 |
+
x = block.ffn(x)
|
| 951 |
+
x = x.permute(0, 2, 1)
|
| 952 |
+
if block.ffn_gamma is not None:
|
| 953 |
+
x = x * block.ffn_gamma.unsqueeze(-1)
|
| 954 |
+
x = residual + x
|
| 955 |
+
else:
|
| 956 |
+
x = block(x)
|
| 957 |
+
|
| 958 |
+
return self.norm(x)
|
| 959 |
+
|
| 960 |
+
def forward(self, x, cache=None, sample_indices=None, use_cache=False, debug=False):
|
| 961 |
+
x = self.forward_features(x, cache=cache, sample_indices=sample_indices, use_cache=use_cache, debug=debug)
|
| 962 |
+
x = self.head(x, cache=cache, sample_indices=sample_indices, use_cache=use_cache, debug=debug)
|
| 963 |
+
return x
|
| 964 |
+
|
| 965 |
+
|
| 966 |
+
@dataclass
|
| 967 |
+
class VibeVoiceTokenizerEncoderOutput:
|
| 968 |
+
"""
|
| 969 |
+
Output of VibeVoice tokenizer encoder, representing a Gaussian distribution with fixed variance.
|
| 970 |
+
|
| 971 |
+
Args:
|
| 972 |
+
mean (`torch.FloatTensor`): The mean parameters of the distribution.
|
| 973 |
+
std (`float` or `torch.FloatTensor`): Fixed standard deviation value.
|
| 974 |
+
"""
|
| 975 |
+
mean: torch.Tensor
|
| 976 |
+
std: Optional[Union[float, torch.Tensor]] = None
|
| 977 |
+
|
| 978 |
+
def sample(self, dist_type='fix'):
|
| 979 |
+
"""
|
| 980 |
+
Sample from the distribution.
|
| 981 |
+
|
| 982 |
+
Args:
|
| 983 |
+
dist_type (`str`): Sampling method, either 'fix' or 'gaussian'.
|
| 984 |
+
|
| 985 |
+
Returns:
|
| 986 |
+
`torch.FloatTensor`: Sampled values.
|
| 987 |
+
`torch.FloatTensor` (optional): Standard deviation used (only when dist_type='gaussian').
|
| 988 |
+
"""
|
| 989 |
+
if dist_type == 'fix':
|
| 990 |
+
x = self.mean + self.std * torch.randn_like(self.mean)
|
| 991 |
+
return x, self.std
|
| 992 |
+
elif dist_type == 'gaussian':
|
| 993 |
+
batch_size = self.mean.size(0)
|
| 994 |
+
value = self.std / 0.8
|
| 995 |
+
std = torch.randn(batch_size, device=self.mean.device, dtype=self.mean.dtype) * value
|
| 996 |
+
|
| 997 |
+
while std.dim() < self.mean.dim():
|
| 998 |
+
std = std.unsqueeze(-1)
|
| 999 |
+
|
| 1000 |
+
x = self.mean + std * torch.randn_like(self.mean)
|
| 1001 |
+
return x, std
|
| 1002 |
+
else:
|
| 1003 |
+
return self.mean, self.std
|
| 1004 |
+
|
| 1005 |
+
def kl(self):
|
| 1006 |
+
"""Compute KL divergence between this distribution and a standard normal."""
|
| 1007 |
+
target = torch.zeros_like(self.mean)
|
| 1008 |
+
return F.mse_loss(self.mean, target, reduction='none')
|
| 1009 |
+
|
| 1010 |
+
def mode(self):
|
| 1011 |
+
"""Return the distribution mode (which is the mean for Gaussian)."""
|
| 1012 |
+
return self.mean
|
| 1013 |
+
|
| 1014 |
+
class VibeVoiceAcousticTokenizerModel(PreTrainedModel):
|
| 1015 |
+
"""VibeVoice speech tokenizer model combining encoder and decoder for acoustic tokens"""
|
| 1016 |
+
|
| 1017 |
+
config_class = VibeVoiceAcousticTokenizerConfig
|
| 1018 |
+
base_model_prefix = "vibevoice_acoustic_tokenizer"
|
| 1019 |
+
_supports_flash_attn_2 = True
|
| 1020 |
+
_supports_sdpa = True
|
| 1021 |
+
_no_split_modules = ["TokenizerEncoder", "TokenizerDecoder"]
|
| 1022 |
+
|
| 1023 |
+
def __init__(self, config):
|
| 1024 |
+
super().__init__(config)
|
| 1025 |
+
|
| 1026 |
+
self.register_buffer('fix_std', torch.tensor(config.fix_std), persistent=False)
|
| 1027 |
+
self.std_dist_type = getattr(config, "std_dist_type", "fix")
|
| 1028 |
+
|
| 1029 |
+
# Parse encoder depths
|
| 1030 |
+
if isinstance(config.encoder_depths, str):
|
| 1031 |
+
encoder_depths = [int(d) for d in config.encoder_depths.split('-')]
|
| 1032 |
+
else:
|
| 1033 |
+
encoder_depths = config.encoder_depths
|
| 1034 |
+
|
| 1035 |
+
# Parse decoder depths if provided
|
| 1036 |
+
if config.decoder_depths is not None and isinstance(config.decoder_depths, str):
|
| 1037 |
+
decoder_depths = [int(d) for d in config.decoder_depths.split('-')]
|
| 1038 |
+
else:
|
| 1039 |
+
# Default: use reversed encoder depths if decoder_depths is None
|
| 1040 |
+
decoder_depths = list(reversed(encoder_depths))
|
| 1041 |
+
|
| 1042 |
+
# Create encoder config
|
| 1043 |
+
encoder_config = copy.deepcopy(config)
|
| 1044 |
+
encoder_config.dimension = config.vae_dim
|
| 1045 |
+
encoder_config.n_filters = config.encoder_n_filters
|
| 1046 |
+
encoder_config.ratios = config.encoder_ratios
|
| 1047 |
+
encoder_config.depths = encoder_depths
|
| 1048 |
+
encoder_config.norm = config.conv_norm
|
| 1049 |
+
encoder_config.pad_mode = config.pad_mode
|
| 1050 |
+
encoder_config.bias = config.conv_bias
|
| 1051 |
+
encoder_config.layernorm_eps = config.layernorm_eps
|
| 1052 |
+
encoder_config.layernorm_elementwise_affine = config.layernorm_elementwise_affine
|
| 1053 |
+
encoder_config.mixer_layer = config.mixer_layer
|
| 1054 |
+
encoder_config.layer_scale_init_value = config.layer_scale_init_value
|
| 1055 |
+
encoder_config.disable_last_norm = config.disable_last_norm
|
| 1056 |
+
|
| 1057 |
+
# Create decoder config
|
| 1058 |
+
decoder_config = copy.deepcopy(config)
|
| 1059 |
+
decoder_config.dimension = config.vae_dim
|
| 1060 |
+
decoder_config.n_filters = config.decoder_n_filters
|
| 1061 |
+
decoder_config.ratios = config.decoder_ratios
|
| 1062 |
+
decoder_config.depths = decoder_depths
|
| 1063 |
+
decoder_config.norm = config.conv_norm
|
| 1064 |
+
decoder_config.pad_mode = config.pad_mode
|
| 1065 |
+
decoder_config.bias = config.conv_bias
|
| 1066 |
+
decoder_config.layernorm_eps = config.layernorm_eps
|
| 1067 |
+
decoder_config.layernorm_elementwise_affine = config.layernorm_elementwise_affine
|
| 1068 |
+
decoder_config.mixer_layer = config.mixer_layer
|
| 1069 |
+
decoder_config.layer_scale_init_value = config.layer_scale_init_value
|
| 1070 |
+
decoder_config.disable_last_norm = config.disable_last_norm
|
| 1071 |
+
|
| 1072 |
+
# Initialize encoder and decoder
|
| 1073 |
+
self.encoder = TokenizerEncoder(encoder_config)
|
| 1074 |
+
self.decoder = TokenizerDecoder(decoder_config)
|
| 1075 |
+
|
| 1076 |
+
# Initialize weights
|
| 1077 |
+
self.apply(self._init_weights)
|
| 1078 |
+
|
| 1079 |
+
def _init_weights(self, module):
|
| 1080 |
+
"""Initialize weights for the model"""
|
| 1081 |
+
if isinstance(module, nn.Linear):
|
| 1082 |
+
nn.init.normal_(module.weight, std=self.config.weight_init_value)
|
| 1083 |
+
if module.bias is not None:
|
| 1084 |
+
nn.init.zeros_(module.bias)
|
| 1085 |
+
elif isinstance(module, nn.LayerNorm):
|
| 1086 |
+
nn.init.ones_(module.weight)
|
| 1087 |
+
nn.init.zeros_(module.bias)
|
| 1088 |
+
elif isinstance(module, nn.Conv1d):
|
| 1089 |
+
nn.init.normal_(module.weight, std=self.config.weight_init_value)
|
| 1090 |
+
if module.bias is not None:
|
| 1091 |
+
nn.init.zeros_(module.bias)
|
| 1092 |
+
|
| 1093 |
+
@torch.no_grad()
|
| 1094 |
+
def encode(self, audio, cache=None, sample_indices=None, use_cache=False, debug=False, is_final_chunk=False):
|
| 1095 |
+
"""Convert audio to latent representations"""
|
| 1096 |
+
latents = self.encoder(audio, cache=cache, sample_indices=sample_indices, use_cache=use_cache, debug=debug, is_final_chunk=is_final_chunk)
|
| 1097 |
+
return VibeVoiceTokenizerEncoderOutput(mean=latents.permute(0, 2, 1), std=self.fix_std)
|
| 1098 |
+
|
| 1099 |
+
@torch.no_grad()
|
| 1100 |
+
def sampling(self, encoder_output, dist_type=None):
|
| 1101 |
+
"""Sample from the encoder output distribution"""
|
| 1102 |
+
dist_type = dist_type or self.std_dist_type
|
| 1103 |
+
|
| 1104 |
+
if dist_type == 'fix':
|
| 1105 |
+
return encoder_output.sample(dist_type='fix')
|
| 1106 |
+
elif dist_type == 'gaussian':
|
| 1107 |
+
return encoder_output.sample(dist_type='gaussian')
|
| 1108 |
+
else:
|
| 1109 |
+
raise ValueError(f"Unsupported dist_type: {dist_type}, expected 'fix' or 'gaussian'")
|
| 1110 |
+
|
| 1111 |
+
@torch.no_grad()
|
| 1112 |
+
def decode(self, latents, cache=None, sample_indices=None, use_cache=False, debug=False):
|
| 1113 |
+
"""Convert latent representations back to audio"""
|
| 1114 |
+
if latents.shape[1] == self.config.vae_dim:
|
| 1115 |
+
pass
|
| 1116 |
+
else:
|
| 1117 |
+
latents = latents.permute(0, 2, 1)
|
| 1118 |
+
|
| 1119 |
+
audio = self.decoder(latents, cache=cache, sample_indices=sample_indices, use_cache=use_cache, debug=debug)
|
| 1120 |
+
return audio
|
| 1121 |
+
|
| 1122 |
+
def forward(self, audio, cache=None, sample_indices=None, use_cache=False, debug=False):
|
| 1123 |
+
"""Full forward pass: encode audio to latents, then decode back to audio"""
|
| 1124 |
+
encoder_output = self.encode(audio, cache=cache, sample_indices=sample_indices, use_cache=use_cache, debug=debug)
|
| 1125 |
+
sampled_latents, _ = self.sampling(encoder_output)
|
| 1126 |
+
reconstructed = self.decode(sampled_latents, cache=cache, sample_indices=sample_indices, use_cache=use_cache, debug=debug)
|
| 1127 |
+
return reconstructed, sampled_latents
|
| 1128 |
+
|
| 1129 |
+
|
| 1130 |
+
class VibeVoiceSemanticTokenizerModel(PreTrainedModel):
|
| 1131 |
+
"""VibeVoice speech tokenizer model with only encoder for semantic tokens"""
|
| 1132 |
+
|
| 1133 |
+
config_class = VibeVoiceSemanticTokenizerConfig
|
| 1134 |
+
base_model_prefix = "vibevoice_semantic_tokenizer"
|
| 1135 |
+
_supports_flash_attn_2 = True
|
| 1136 |
+
_supports_sdpa = True
|
| 1137 |
+
_no_split_modules = ["TokenizerEncoder"]
|
| 1138 |
+
|
| 1139 |
+
def __init__(self, config):
|
| 1140 |
+
super().__init__(config)
|
| 1141 |
+
|
| 1142 |
+
# Parse encoder depths
|
| 1143 |
+
if isinstance(config.encoder_depths, str):
|
| 1144 |
+
encoder_depths = [int(d) for d in config.encoder_depths.split('-')]
|
| 1145 |
+
else:
|
| 1146 |
+
encoder_depths = config.encoder_depths
|
| 1147 |
+
|
| 1148 |
+
# Create encoder config
|
| 1149 |
+
encoder_config = copy.deepcopy(config)
|
| 1150 |
+
encoder_config.dimension = config.vae_dim
|
| 1151 |
+
encoder_config.n_filters = config.encoder_n_filters
|
| 1152 |
+
encoder_config.ratios = config.encoder_ratios
|
| 1153 |
+
encoder_config.depths = encoder_depths
|
| 1154 |
+
encoder_config.norm = config.conv_norm
|
| 1155 |
+
encoder_config.pad_mode = config.pad_mode
|
| 1156 |
+
encoder_config.bias = config.conv_bias
|
| 1157 |
+
encoder_config.layernorm_eps = config.layernorm_eps
|
| 1158 |
+
encoder_config.layernorm_elementwise_affine = config.layernorm_elementwise_affine
|
| 1159 |
+
encoder_config.mixer_layer = config.mixer_layer
|
| 1160 |
+
encoder_config.layer_scale_init_value = config.layer_scale_init_value
|
| 1161 |
+
encoder_config.disable_last_norm = config.disable_last_norm
|
| 1162 |
+
|
| 1163 |
+
# Initialize encoder and decoder
|
| 1164 |
+
self.encoder = TokenizerEncoder(encoder_config)
|
| 1165 |
+
|
| 1166 |
+
# Initialize weights
|
| 1167 |
+
self.apply(self._init_weights)
|
| 1168 |
+
|
| 1169 |
+
def _init_weights(self, module):
|
| 1170 |
+
"""Initialize weights for the model"""
|
| 1171 |
+
if isinstance(module, nn.Linear):
|
| 1172 |
+
nn.init.normal_(module.weight, std=self.config.weight_init_value)
|
| 1173 |
+
if module.bias is not None:
|
| 1174 |
+
nn.init.zeros_(module.bias)
|
| 1175 |
+
elif isinstance(module, nn.LayerNorm):
|
| 1176 |
+
nn.init.ones_(module.weight)
|
| 1177 |
+
nn.init.zeros_(module.bias)
|
| 1178 |
+
elif isinstance(module, nn.Conv1d):
|
| 1179 |
+
nn.init.normal_(module.weight, std=self.config.weight_init_value)
|
| 1180 |
+
if module.bias is not None:
|
| 1181 |
+
nn.init.zeros_(module.bias)
|
| 1182 |
+
|
| 1183 |
+
@torch.no_grad()
|
| 1184 |
+
def encode(self, audio, cache=None, sample_indices=None, use_cache=False, debug=False, is_final_chunk=False):
|
| 1185 |
+
"""Convert audio to latent representations"""
|
| 1186 |
+
latents = self.encoder(audio, cache=cache, sample_indices=sample_indices, use_cache=use_cache, debug=debug, is_final_chunk=is_final_chunk)
|
| 1187 |
+
return VibeVoiceTokenizerEncoderOutput(mean=latents.permute(0, 2, 1))
|
| 1188 |
+
|
| 1189 |
+
@torch.no_grad()
|
| 1190 |
+
def sampling(self, encoder_output, dist_type=None):
|
| 1191 |
+
"""Sample from the encoder output distribution"""
|
| 1192 |
+
return encoder_output.sample(dist_type='none')
|
| 1193 |
+
|
| 1194 |
+
def forward(self, audio, cache=None, sample_indices=None, use_cache=False, debug=False):
|
| 1195 |
+
"""Full forward pass: encode audio to latents, then decode back to audio"""
|
| 1196 |
+
encoder_output = self.encode(audio, cache=cache, sample_indices=sample_indices, use_cache=use_cache, debug=debug)
|
| 1197 |
+
sampled_latents, _ = self.sampling(encoder_output, dist_type='none')
|
| 1198 |
+
return None, sampled_latents
|
| 1199 |
+
|
| 1200 |
+
AutoModel.register(VibeVoiceAcousticTokenizerConfig, VibeVoiceAcousticTokenizerModel)
|
| 1201 |
+
AutoModel.register(VibeVoiceSemanticTokenizerConfig, VibeVoiceSemanticTokenizerModel)
|
| 1202 |
+
|
| 1203 |
+
__all__ = [
|
| 1204 |
+
"VibeVoiceTokenizerStreamingCache",
|
| 1205 |
+
"VibeVoiceAcousticTokenizerModel",
|
| 1206 |
+
"VibeVoiceSemanticTokenizerModel",
|
| 1207 |
+
]
|
vibevoice/modular/streamer.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
|
| 5 |
+
import asyncio
|
| 6 |
+
from queue import Empty, Queue
|
| 7 |
+
from typing import TYPE_CHECKING, Optional
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
from transformers.generation import BaseStreamer
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class AudioStreamer(BaseStreamer):
|
| 14 |
+
"""
|
| 15 |
+
Audio streamer that stores audio chunks in queues for each sample in the batch.
|
| 16 |
+
This allows streaming audio generation for multiple samples simultaneously.
|
| 17 |
+
|
| 18 |
+
Parameters:
|
| 19 |
+
batch_size (`int`):
|
| 20 |
+
The batch size for generation
|
| 21 |
+
stop_signal (`any`, *optional*):
|
| 22 |
+
The signal to put in the queue when generation ends. Defaults to None.
|
| 23 |
+
timeout (`float`, *optional*):
|
| 24 |
+
The timeout for the audio queue. If `None`, the queue will block indefinitely.
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
def __init__(
|
| 28 |
+
self,
|
| 29 |
+
batch_size: int,
|
| 30 |
+
stop_signal: Optional[any] = None,
|
| 31 |
+
timeout: Optional[float] = None,
|
| 32 |
+
):
|
| 33 |
+
self.batch_size = batch_size
|
| 34 |
+
self.stop_signal = stop_signal
|
| 35 |
+
self.timeout = timeout
|
| 36 |
+
|
| 37 |
+
# Create a queue for each sample in the batch
|
| 38 |
+
self.audio_queues = [Queue() for _ in range(batch_size)]
|
| 39 |
+
self.finished_flags = [False for _ in range(batch_size)]
|
| 40 |
+
self.sample_indices_map = {} # Maps from sample index to queue index
|
| 41 |
+
|
| 42 |
+
def put(self, audio_chunks: torch.Tensor, sample_indices: torch.Tensor):
|
| 43 |
+
"""
|
| 44 |
+
Receives audio chunks and puts them in the appropriate queues.
|
| 45 |
+
|
| 46 |
+
Args:
|
| 47 |
+
audio_chunks: Tensor of shape (num_samples, ...) containing audio chunks
|
| 48 |
+
sample_indices: Tensor indicating which samples these chunks belong to
|
| 49 |
+
"""
|
| 50 |
+
for i, sample_idx in enumerate(sample_indices):
|
| 51 |
+
idx = sample_idx.item()
|
| 52 |
+
if idx < self.batch_size and not self.finished_flags[idx]:
|
| 53 |
+
# Convert to numpy or keep as tensor based on preference
|
| 54 |
+
audio_chunk = audio_chunks[i].detach().cpu()
|
| 55 |
+
self.audio_queues[idx].put(audio_chunk, timeout=self.timeout)
|
| 56 |
+
|
| 57 |
+
def end(self, sample_indices: Optional[torch.Tensor] = None):
|
| 58 |
+
"""
|
| 59 |
+
Signals the end of generation for specified samples or all samples.
|
| 60 |
+
|
| 61 |
+
Args:
|
| 62 |
+
sample_indices: Optional tensor of sample indices to end. If None, ends all.
|
| 63 |
+
"""
|
| 64 |
+
if sample_indices is None:
|
| 65 |
+
# End all samples
|
| 66 |
+
for idx in range(self.batch_size):
|
| 67 |
+
if not self.finished_flags[idx]:
|
| 68 |
+
self.audio_queues[idx].put(self.stop_signal, timeout=self.timeout)
|
| 69 |
+
self.finished_flags[idx] = True
|
| 70 |
+
else:
|
| 71 |
+
# End specific samples
|
| 72 |
+
for sample_idx in sample_indices:
|
| 73 |
+
idx = sample_idx.item() if torch.is_tensor(sample_idx) else sample_idx
|
| 74 |
+
if idx < self.batch_size and not self.finished_flags[idx]:
|
| 75 |
+
self.audio_queues[idx].put(self.stop_signal, timeout=self.timeout)
|
| 76 |
+
self.finished_flags[idx] = True
|
| 77 |
+
|
| 78 |
+
def __iter__(self):
|
| 79 |
+
"""Returns an iterator over the batch of audio streams."""
|
| 80 |
+
return AudioBatchIterator(self)
|
| 81 |
+
|
| 82 |
+
def get_stream(self, sample_idx: int):
|
| 83 |
+
"""Get the audio stream for a specific sample."""
|
| 84 |
+
if sample_idx >= self.batch_size:
|
| 85 |
+
raise ValueError(f"Sample index {sample_idx} exceeds batch size {self.batch_size}")
|
| 86 |
+
return AudioSampleIterator(self, sample_idx)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
class AudioSampleIterator:
|
| 90 |
+
"""Iterator for a single audio stream from the batch."""
|
| 91 |
+
|
| 92 |
+
def __init__(self, streamer: AudioStreamer, sample_idx: int):
|
| 93 |
+
self.streamer = streamer
|
| 94 |
+
self.sample_idx = sample_idx
|
| 95 |
+
|
| 96 |
+
def __iter__(self):
|
| 97 |
+
return self
|
| 98 |
+
|
| 99 |
+
def __next__(self):
|
| 100 |
+
value = self.streamer.audio_queues[self.sample_idx].get(timeout=self.streamer.timeout)
|
| 101 |
+
if value == self.streamer.stop_signal:
|
| 102 |
+
raise StopIteration()
|
| 103 |
+
return value
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
class AudioBatchIterator:
|
| 107 |
+
"""Iterator that yields audio chunks for all samples in the batch."""
|
| 108 |
+
|
| 109 |
+
def __init__(self, streamer: AudioStreamer):
|
| 110 |
+
self.streamer = streamer
|
| 111 |
+
self.active_samples = set(range(streamer.batch_size))
|
| 112 |
+
|
| 113 |
+
def __iter__(self):
|
| 114 |
+
return self
|
| 115 |
+
|
| 116 |
+
def __next__(self):
|
| 117 |
+
if not self.active_samples:
|
| 118 |
+
raise StopIteration()
|
| 119 |
+
|
| 120 |
+
batch_chunks = {}
|
| 121 |
+
samples_to_remove = set()
|
| 122 |
+
|
| 123 |
+
# Try to get chunks from all active samples
|
| 124 |
+
for idx in self.active_samples:
|
| 125 |
+
try:
|
| 126 |
+
value = self.streamer.audio_queues[idx].get(block=False)
|
| 127 |
+
if value == self.streamer.stop_signal:
|
| 128 |
+
samples_to_remove.add(idx)
|
| 129 |
+
else:
|
| 130 |
+
batch_chunks[idx] = value
|
| 131 |
+
except Empty:
|
| 132 |
+
# Queue is empty for this sample, skip it this iteration
|
| 133 |
+
pass
|
| 134 |
+
|
| 135 |
+
# Remove finished samples
|
| 136 |
+
self.active_samples -= samples_to_remove
|
| 137 |
+
|
| 138 |
+
if batch_chunks:
|
| 139 |
+
return batch_chunks
|
| 140 |
+
elif self.active_samples:
|
| 141 |
+
# If no chunks were ready but we still have active samples,
|
| 142 |
+
# wait a bit and try again
|
| 143 |
+
import time
|
| 144 |
+
time.sleep(0.01)
|
| 145 |
+
return self.__next__()
|
| 146 |
+
else:
|
| 147 |
+
raise StopIteration()
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
class AsyncAudioStreamer(AudioStreamer):
|
| 151 |
+
"""
|
| 152 |
+
Async version of AudioStreamer for use in async contexts.
|
| 153 |
+
"""
|
| 154 |
+
|
| 155 |
+
def __init__(
|
| 156 |
+
self,
|
| 157 |
+
batch_size: int,
|
| 158 |
+
stop_signal: Optional[any] = None,
|
| 159 |
+
timeout: Optional[float] = None,
|
| 160 |
+
):
|
| 161 |
+
super().__init__(batch_size, stop_signal, timeout)
|
| 162 |
+
# Replace regular queues with async queues
|
| 163 |
+
self.audio_queues = [asyncio.Queue() for _ in range(batch_size)]
|
| 164 |
+
self.loop = asyncio.get_running_loop()
|
| 165 |
+
|
| 166 |
+
def put(self, audio_chunks: torch.Tensor, sample_indices: torch.Tensor):
|
| 167 |
+
"""Put audio chunks in the appropriate async queues."""
|
| 168 |
+
for i, sample_idx in enumerate(sample_indices):
|
| 169 |
+
idx = sample_idx.item()
|
| 170 |
+
if idx < self.batch_size and not self.finished_flags[idx]:
|
| 171 |
+
audio_chunk = audio_chunks[i].detach().cpu()
|
| 172 |
+
self.loop.call_soon_threadsafe(
|
| 173 |
+
self.audio_queues[idx].put_nowait, audio_chunk
|
| 174 |
+
)
|
| 175 |
+
|
| 176 |
+
def end(self, sample_indices: Optional[torch.Tensor] = None):
|
| 177 |
+
"""Signal the end of generation for specified samples."""
|
| 178 |
+
if sample_indices is None:
|
| 179 |
+
indices_to_end = range(self.batch_size)
|
| 180 |
+
else:
|
| 181 |
+
indices_to_end = [s.item() if torch.is_tensor(s) else s for s in sample_indices]
|
| 182 |
+
|
| 183 |
+
for idx in indices_to_end:
|
| 184 |
+
if idx < self.batch_size and not self.finished_flags[idx]:
|
| 185 |
+
self.loop.call_soon_threadsafe(
|
| 186 |
+
self.audio_queues[idx].put_nowait, self.stop_signal
|
| 187 |
+
)
|
| 188 |
+
self.finished_flags[idx] = True
|
| 189 |
+
|
| 190 |
+
async def get_stream(self, sample_idx: int):
|
| 191 |
+
"""Get async iterator for a specific sample's audio stream."""
|
| 192 |
+
if sample_idx >= self.batch_size:
|
| 193 |
+
raise ValueError(f"Sample index {sample_idx} exceeds batch size {self.batch_size}")
|
| 194 |
+
|
| 195 |
+
while True:
|
| 196 |
+
value = await self.audio_queues[sample_idx].get()
|
| 197 |
+
if value == self.stop_signal:
|
| 198 |
+
break
|
| 199 |
+
yield value
|
| 200 |
+
|
| 201 |
+
def __aiter__(self):
|
| 202 |
+
"""Returns an async iterator over all audio streams."""
|
| 203 |
+
return AsyncAudioBatchIterator(self)
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
class AsyncAudioBatchIterator:
|
| 207 |
+
"""Async iterator for batch audio streaming."""
|
| 208 |
+
|
| 209 |
+
def __init__(self, streamer: AsyncAudioStreamer):
|
| 210 |
+
self.streamer = streamer
|
| 211 |
+
self.active_samples = set(range(streamer.batch_size))
|
| 212 |
+
|
| 213 |
+
def __aiter__(self):
|
| 214 |
+
return self
|
| 215 |
+
|
| 216 |
+
async def __anext__(self):
|
| 217 |
+
if not self.active_samples:
|
| 218 |
+
raise StopAsyncIteration()
|
| 219 |
+
|
| 220 |
+
batch_chunks = {}
|
| 221 |
+
samples_to_remove = set()
|
| 222 |
+
|
| 223 |
+
# Create tasks for all active samples
|
| 224 |
+
tasks = {
|
| 225 |
+
idx: asyncio.create_task(self._get_chunk(idx))
|
| 226 |
+
for idx in self.active_samples
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
# Wait for at least one chunk to be ready
|
| 230 |
+
done, pending = await asyncio.wait(
|
| 231 |
+
tasks.values(),
|
| 232 |
+
return_when=asyncio.FIRST_COMPLETED,
|
| 233 |
+
timeout=self.streamer.timeout
|
| 234 |
+
)
|
| 235 |
+
|
| 236 |
+
# Cancel pending tasks
|
| 237 |
+
for task in pending:
|
| 238 |
+
task.cancel()
|
| 239 |
+
|
| 240 |
+
# Process completed tasks
|
| 241 |
+
for idx, task in tasks.items():
|
| 242 |
+
if task in done:
|
| 243 |
+
try:
|
| 244 |
+
value = await task
|
| 245 |
+
if value == self.streamer.stop_signal:
|
| 246 |
+
samples_to_remove.add(idx)
|
| 247 |
+
else:
|
| 248 |
+
batch_chunks[idx] = value
|
| 249 |
+
except asyncio.CancelledError:
|
| 250 |
+
pass
|
| 251 |
+
|
| 252 |
+
self.active_samples -= samples_to_remove
|
| 253 |
+
|
| 254 |
+
if batch_chunks:
|
| 255 |
+
return batch_chunks
|
| 256 |
+
elif self.active_samples:
|
| 257 |
+
# Try again if we still have active samples
|
| 258 |
+
return await self.__anext__()
|
| 259 |
+
else:
|
| 260 |
+
raise StopAsyncIteration()
|
| 261 |
+
|
| 262 |
+
async def _get_chunk(self, idx):
|
| 263 |
+
"""Helper to get a chunk from a specific queue."""
|
| 264 |
+
return await self.streamer.audio_queues[idx].get()
|
vibevoice/processor/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# vibevoice/processor/__init__.py
|
| 2 |
+
from .vibevoice_processor import VibeVoiceProcessor
|
| 3 |
+
from .vibevoice_streaming_processor import VibeVoiceStreamingProcessor
|
| 4 |
+
from .vibevoice_tokenizer_processor import VibeVoiceTokenizerProcessor, AudioNormalizer
|
| 5 |
+
|
| 6 |
+
__all__ = [
|
| 7 |
+
"VibeVoiceProcessor",
|
| 8 |
+
"VibeVoiceStreamingProcessor",
|
| 9 |
+
"VibeVoiceTokenizerProcessor",
|
| 10 |
+
"AudioNormalizer",
|
| 11 |
+
]
|
vibevoice/processor/audio_utils.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import threading
|
| 3 |
+
|
| 4 |
+
import numpy as np
|
| 5 |
+
from subprocess import run
|
| 6 |
+
from typing import List, Optional, Union, Dict, Any
|
| 7 |
+
|
| 8 |
+
COMMON_AUDIO_EXTS = [
|
| 9 |
+
'.mp3', '.MP3', '.Mp3', # All case variations of mp3
|
| 10 |
+
'.m4a',
|
| 11 |
+
'.mp4', '.MP4',
|
| 12 |
+
'.wav', '.WAV',
|
| 13 |
+
'.m4v',
|
| 14 |
+
'.aac',
|
| 15 |
+
'.ogg',
|
| 16 |
+
'.mov', '.MOV',
|
| 17 |
+
'.opus',
|
| 18 |
+
'.m4b',
|
| 19 |
+
'.flac',
|
| 20 |
+
'.wma', '.WMA',
|
| 21 |
+
'.rm', '.3gp', '.mpeg', '.flv', '.webm', '.mp2', '.aif', '.aiff', '.oga', '.ogv', '.mpga', '.m3u8', '.amr'
|
| 22 |
+
]
|
| 23 |
+
|
| 24 |
+
def load_audio_use_ffmpeg(file: str, resample: bool = False, target_sr: int = 24000):
|
| 25 |
+
"""
|
| 26 |
+
Open an audio file and read as mono waveform, optionally resampling.
|
| 27 |
+
Returns both the audio data and the original sample rate.
|
| 28 |
+
|
| 29 |
+
Parameters
|
| 30 |
+
----------
|
| 31 |
+
file: str
|
| 32 |
+
The audio file to open
|
| 33 |
+
resample: bool
|
| 34 |
+
Whether to resample the audio
|
| 35 |
+
target_sr: int
|
| 36 |
+
The target sample rate if resampling is requested
|
| 37 |
+
|
| 38 |
+
Returns
|
| 39 |
+
-------
|
| 40 |
+
A tuple containing:
|
| 41 |
+
- A NumPy array with the audio waveform in float32 dtype
|
| 42 |
+
- The original sample rate of the audio file
|
| 43 |
+
"""
|
| 44 |
+
if not resample:
|
| 45 |
+
# First, get the original sample rate
|
| 46 |
+
cmd_probe = [
|
| 47 |
+
"ffprobe",
|
| 48 |
+
"-v", "quiet",
|
| 49 |
+
"-show_entries", "stream=sample_rate",
|
| 50 |
+
"-of", "default=noprint_wrappers=1:nokey=1",
|
| 51 |
+
file
|
| 52 |
+
]
|
| 53 |
+
|
| 54 |
+
original_sr = int(run(cmd_probe, capture_output=True, check=True).stdout.decode().strip())
|
| 55 |
+
else:
|
| 56 |
+
original_sr = None
|
| 57 |
+
|
| 58 |
+
# Now load the audio
|
| 59 |
+
sr_to_use = target_sr if resample else original_sr
|
| 60 |
+
|
| 61 |
+
cmd = [
|
| 62 |
+
"ffmpeg",
|
| 63 |
+
"-loglevel", "error",
|
| 64 |
+
"-nostdin",
|
| 65 |
+
"-threads", "0",
|
| 66 |
+
"-i", file,
|
| 67 |
+
"-f", "s16le",
|
| 68 |
+
"-ac", "1",
|
| 69 |
+
"-acodec", "pcm_s16le",
|
| 70 |
+
"-ar", str(sr_to_use),
|
| 71 |
+
"-",
|
| 72 |
+
]
|
| 73 |
+
|
| 74 |
+
out = _run_ffmpeg(cmd).stdout
|
| 75 |
+
audio_data = np.frombuffer(out, np.int16).flatten().astype(np.float32) / 32768.0
|
| 76 |
+
|
| 77 |
+
return audio_data, sr_to_use
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def _get_ffmpeg_max_concurrency() -> int:
|
| 81 |
+
"""Get the maximum FFmpeg concurrency from environment variable."""
|
| 82 |
+
v = os.getenv("VIBEVOICE_FFMPEG_MAX_CONCURRENCY", "")
|
| 83 |
+
try:
|
| 84 |
+
n = int(v) if v.strip() else 0
|
| 85 |
+
except Exception:
|
| 86 |
+
n = 0
|
| 87 |
+
# 0/negative means no explicit limit.
|
| 88 |
+
return n
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
_FFMPEG_MAX_CONCURRENCY = _get_ffmpeg_max_concurrency()
|
| 92 |
+
_FFMPEG_SEM = threading.Semaphore(_FFMPEG_MAX_CONCURRENCY) if _FFMPEG_MAX_CONCURRENCY > 0 else None
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def _run_ffmpeg(cmd: list, *, stdin_bytes: bytes = None):
|
| 96 |
+
"""Run ffmpeg with optional global concurrency limiting.
|
| 97 |
+
|
| 98 |
+
This is important for vLLM multi-request concurrency: spawning too many
|
| 99 |
+
ffmpeg processes can saturate CPU/IO and cause request failures/timeouts.
|
| 100 |
+
"""
|
| 101 |
+
if _FFMPEG_SEM is None:
|
| 102 |
+
return run(cmd, capture_output=True, check=True, input=stdin_bytes)
|
| 103 |
+
with _FFMPEG_SEM:
|
| 104 |
+
return run(cmd, capture_output=True, check=True, input=stdin_bytes)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def load_audio_bytes_use_ffmpeg(data: bytes, *, resample: bool = False, target_sr: int = 24000):
|
| 108 |
+
"""Decode audio bytes via ffmpeg stdin pipe.
|
| 109 |
+
|
| 110 |
+
Compared to writing bytes to a temp file, this avoids filesystem IO and
|
| 111 |
+
reduces contention under high request concurrency.
|
| 112 |
+
|
| 113 |
+
Parameters
|
| 114 |
+
----------
|
| 115 |
+
data: bytes
|
| 116 |
+
The audio data bytes
|
| 117 |
+
resample: bool
|
| 118 |
+
Whether to resample the audio (must be True)
|
| 119 |
+
target_sr: int
|
| 120 |
+
The target sample rate if resampling is requested
|
| 121 |
+
|
| 122 |
+
Returns
|
| 123 |
+
-------
|
| 124 |
+
A tuple containing:
|
| 125 |
+
- A NumPy array with the audio waveform in float32 dtype
|
| 126 |
+
- The sample rate
|
| 127 |
+
"""
|
| 128 |
+
if not resample:
|
| 129 |
+
# For stdin bytes, we don't have a cheap/robust way to probe original sr.
|
| 130 |
+
# Keep behavior explicit.
|
| 131 |
+
raise ValueError("load_audio_bytes_use_ffmpeg requires resample=True")
|
| 132 |
+
|
| 133 |
+
cmd = [
|
| 134 |
+
"ffmpeg",
|
| 135 |
+
"-loglevel", "error",
|
| 136 |
+
"-threads", "0",
|
| 137 |
+
"-i", "pipe:0",
|
| 138 |
+
"-f", "s16le",
|
| 139 |
+
"-ac", "1",
|
| 140 |
+
"-acodec", "pcm_s16le",
|
| 141 |
+
"-ar", str(target_sr),
|
| 142 |
+
"-",
|
| 143 |
+
]
|
| 144 |
+
out = _run_ffmpeg(cmd, stdin_bytes=data).stdout
|
| 145 |
+
audio_data = np.frombuffer(out, np.int16).flatten().astype(np.float32) / 32768.0
|
| 146 |
+
return audio_data, target_sr
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
class AudioNormalizer:
|
| 150 |
+
"""
|
| 151 |
+
Audio normalization class for VibeVoice tokenizer.
|
| 152 |
+
|
| 153 |
+
This class provides audio normalization to ensure consistent input levels
|
| 154 |
+
for the VibeVoice tokenizer while maintaining audio quality.
|
| 155 |
+
"""
|
| 156 |
+
|
| 157 |
+
def __init__(self, target_dB_FS: float = -25, eps: float = 1e-6):
|
| 158 |
+
"""
|
| 159 |
+
Initialize the audio normalizer.
|
| 160 |
+
|
| 161 |
+
Args:
|
| 162 |
+
target_dB_FS (float): Target dB FS level for the audio. Default: -25
|
| 163 |
+
eps (float): Small value to avoid division by zero. Default: 1e-6
|
| 164 |
+
"""
|
| 165 |
+
self.target_dB_FS = target_dB_FS
|
| 166 |
+
self.eps = eps
|
| 167 |
+
|
| 168 |
+
def tailor_dB_FS(self, audio: np.ndarray) -> tuple:
|
| 169 |
+
"""
|
| 170 |
+
Adjust the audio to the target dB FS level.
|
| 171 |
+
|
| 172 |
+
Args:
|
| 173 |
+
audio (np.ndarray): Input audio signal
|
| 174 |
+
|
| 175 |
+
Returns:
|
| 176 |
+
tuple: (normalized_audio, rms, scalar)
|
| 177 |
+
"""
|
| 178 |
+
rms = np.sqrt(np.mean(audio**2))
|
| 179 |
+
scalar = 10 ** (self.target_dB_FS / 20) / (rms + self.eps)
|
| 180 |
+
normalized_audio = audio * scalar
|
| 181 |
+
return normalized_audio, rms, scalar
|
| 182 |
+
|
| 183 |
+
def avoid_clipping(self, audio: np.ndarray, scalar: Optional[float] = None) -> tuple:
|
| 184 |
+
"""
|
| 185 |
+
Avoid clipping by scaling down if necessary.
|
| 186 |
+
|
| 187 |
+
Args:
|
| 188 |
+
audio (np.ndarray): Input audio signal
|
| 189 |
+
scalar (float, optional): Explicit scaling factor
|
| 190 |
+
|
| 191 |
+
Returns:
|
| 192 |
+
tuple: (normalized_audio, scalar)
|
| 193 |
+
"""
|
| 194 |
+
if scalar is None:
|
| 195 |
+
max_val = np.max(np.abs(audio))
|
| 196 |
+
if max_val > 1.0:
|
| 197 |
+
scalar = max_val + self.eps
|
| 198 |
+
else:
|
| 199 |
+
scalar = 1.0
|
| 200 |
+
|
| 201 |
+
return audio / scalar, scalar
|
| 202 |
+
|
| 203 |
+
def __call__(self, audio: np.ndarray) -> np.ndarray:
|
| 204 |
+
"""
|
| 205 |
+
Normalize the audio by adjusting to target dB FS and avoiding clipping.
|
| 206 |
+
|
| 207 |
+
Args:
|
| 208 |
+
audio (np.ndarray): Input audio signal
|
| 209 |
+
|
| 210 |
+
Returns:
|
| 211 |
+
np.ndarray: Normalized audio signal
|
| 212 |
+
"""
|
| 213 |
+
# First adjust to target dB FS
|
| 214 |
+
audio, _, _ = self.tailor_dB_FS(audio)
|
| 215 |
+
# Then avoid clipping
|
| 216 |
+
audio, _ = self.avoid_clipping(audio)
|
| 217 |
+
return audio
|
vibevoice/processor/vibevoice_asr_processor.py
ADDED
|
@@ -0,0 +1,572 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Processor class for VibeVoice ASR models.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
import json
|
| 7 |
+
import math
|
| 8 |
+
import warnings
|
| 9 |
+
from typing import List, Optional, Union, Dict, Any, Tuple
|
| 10 |
+
|
| 11 |
+
import numpy as np
|
| 12 |
+
import torch
|
| 13 |
+
|
| 14 |
+
from transformers.tokenization_utils_base import BatchEncoding
|
| 15 |
+
from transformers.utils import TensorType, logging
|
| 16 |
+
from .vibevoice_tokenizer_processor import VibeVoiceTokenizerProcessor, AudioNormalizer
|
| 17 |
+
|
| 18 |
+
try:
|
| 19 |
+
from .audio_utils import load_audio_use_ffmpeg
|
| 20 |
+
HAS_FFMPEG_UTILS = True
|
| 21 |
+
except ImportError:
|
| 22 |
+
HAS_FFMPEG_UTILS = False
|
| 23 |
+
warnings.warn("audio_utils not available, will fall back to soundfile for audio loading")
|
| 24 |
+
|
| 25 |
+
logger = logging.get_logger(__name__)
|
| 26 |
+
|
| 27 |
+
SYSTEM_PROMPT = "You are a helpful assistant that transcribes audio input into text output in JSON format."
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class VibeVoiceASRProcessor:
|
| 31 |
+
"""
|
| 32 |
+
Processor for VibeVoice ASR (Automatic Speech Recognition) models.
|
| 33 |
+
|
| 34 |
+
This processor handles audio preprocessing and tokenization for ASR tasks,
|
| 35 |
+
following the exact format used in training with proper chat templates.
|
| 36 |
+
|
| 37 |
+
Args:
|
| 38 |
+
tokenizer: The text tokenizer for processing text
|
| 39 |
+
audio_processor: The audio processor for processing speech
|
| 40 |
+
speech_tok_compress_ratio (int): Compression ratio for speech tokenization. Default: 3200 (product of encoder ratios [8,5,5,4,2,2])
|
| 41 |
+
target_sample_rate (int): Target sample rate for audio
|
| 42 |
+
normalize_audio (bool): Whether to normalize audio input
|
| 43 |
+
"""
|
| 44 |
+
|
| 45 |
+
def __init__(
|
| 46 |
+
self,
|
| 47 |
+
tokenizer=None,
|
| 48 |
+
audio_processor=None,
|
| 49 |
+
speech_tok_compress_ratio=3200,
|
| 50 |
+
target_sample_rate=24000,
|
| 51 |
+
normalize_audio=True,
|
| 52 |
+
**kwargs
|
| 53 |
+
):
|
| 54 |
+
self.tokenizer = tokenizer
|
| 55 |
+
self.audio_processor = audio_processor or VibeVoiceTokenizerProcessor(
|
| 56 |
+
sampling_rate=target_sample_rate,
|
| 57 |
+
normalize_audio=normalize_audio
|
| 58 |
+
)
|
| 59 |
+
self.speech_tok_compress_ratio = speech_tok_compress_ratio
|
| 60 |
+
self.target_sample_rate = target_sample_rate
|
| 61 |
+
self.normalize_audio = normalize_audio
|
| 62 |
+
|
| 63 |
+
if normalize_audio:
|
| 64 |
+
self.audio_normalizer = AudioNormalizer()
|
| 65 |
+
else:
|
| 66 |
+
self.audio_normalizer = None
|
| 67 |
+
|
| 68 |
+
# Cache special token IDs
|
| 69 |
+
self._cache_special_tokens()
|
| 70 |
+
|
| 71 |
+
def _cache_special_tokens(self):
|
| 72 |
+
"""Cache special token IDs for efficiency."""
|
| 73 |
+
# Add safety checks for special tokens
|
| 74 |
+
if hasattr(self.tokenizer, 'speech_start_id'):
|
| 75 |
+
self.speech_start_id = self.tokenizer.speech_start_id
|
| 76 |
+
else:
|
| 77 |
+
self.speech_start_id = self.tokenizer.convert_tokens_to_ids("<|speech_start|>")
|
| 78 |
+
|
| 79 |
+
if hasattr(self.tokenizer, 'speech_end_id'):
|
| 80 |
+
self.speech_end_id = self.tokenizer.speech_end_id
|
| 81 |
+
else:
|
| 82 |
+
self.speech_end_id = self.tokenizer.convert_tokens_to_ids("<|speech_end|>")
|
| 83 |
+
|
| 84 |
+
if hasattr(self.tokenizer, 'speech_pad_id'):
|
| 85 |
+
self.speech_pad_id = self.tokenizer.speech_pad_id
|
| 86 |
+
else:
|
| 87 |
+
self.speech_pad_id = self.tokenizer.convert_tokens_to_ids("<|speech_pad|>")
|
| 88 |
+
|
| 89 |
+
if hasattr(self.tokenizer, 'pad_id'):
|
| 90 |
+
self.pad_id = self.tokenizer.pad_id
|
| 91 |
+
elif hasattr(self.tokenizer, 'pad_token_id'):
|
| 92 |
+
self.pad_id = self.tokenizer.pad_token_id
|
| 93 |
+
else:
|
| 94 |
+
self.pad_id = self.tokenizer.convert_tokens_to_ids("<|endoftext|>")
|
| 95 |
+
|
| 96 |
+
@classmethod
|
| 97 |
+
def from_pretrained(cls, pretrained_model_name_or_path, **kwargs):
|
| 98 |
+
"""
|
| 99 |
+
Load processor from a pretrained model path.
|
| 100 |
+
|
| 101 |
+
Args:
|
| 102 |
+
pretrained_model_name_or_path: Path to the pretrained model
|
| 103 |
+
**kwargs: Additional keyword arguments
|
| 104 |
+
|
| 105 |
+
Returns:
|
| 106 |
+
VibeVoiceASRProcessor: The loaded processor
|
| 107 |
+
"""
|
| 108 |
+
import json
|
| 109 |
+
from transformers.utils import cached_file
|
| 110 |
+
from vibevoice.modular.modular_vibevoice_text_tokenizer import VibeVoiceASRTextTokenizerFast
|
| 111 |
+
|
| 112 |
+
# Try to load configuration
|
| 113 |
+
config_path = os.path.join(pretrained_model_name_or_path, "preprocessor_config.json")
|
| 114 |
+
config = {}
|
| 115 |
+
|
| 116 |
+
if os.path.exists(config_path):
|
| 117 |
+
with open(config_path, 'r') as f:
|
| 118 |
+
config = json.load(f)
|
| 119 |
+
else:
|
| 120 |
+
try:
|
| 121 |
+
config_file = cached_file(
|
| 122 |
+
pretrained_model_name_or_path,
|
| 123 |
+
"preprocessor_config.json",
|
| 124 |
+
**kwargs
|
| 125 |
+
)
|
| 126 |
+
with open(config_file, 'r') as f:
|
| 127 |
+
config = json.load(f)
|
| 128 |
+
except Exception as e:
|
| 129 |
+
logger.warning(f"Could not load preprocessor_config.json: {e}")
|
| 130 |
+
logger.warning("Using default configuration")
|
| 131 |
+
|
| 132 |
+
# Extract parameters
|
| 133 |
+
speech_tok_compress_ratio = config.get("speech_tok_compress_ratio", 3200)
|
| 134 |
+
target_sample_rate = config.get("target_sample_rate", 24000)
|
| 135 |
+
normalize_audio = config.get("normalize_audio", True)
|
| 136 |
+
|
| 137 |
+
# Load tokenizer
|
| 138 |
+
language_model_pretrained_name = config.get("language_model_pretrained_name", None) or kwargs.pop("language_model_pretrained_name", "Qwen/Qwen2.5-1.5B")
|
| 139 |
+
logger.info(f"Loading tokenizer from {language_model_pretrained_name}")
|
| 140 |
+
|
| 141 |
+
if 'qwen' in language_model_pretrained_name.lower():
|
| 142 |
+
tokenizer = VibeVoiceASRTextTokenizerFast.from_pretrained(
|
| 143 |
+
language_model_pretrained_name,
|
| 144 |
+
**kwargs
|
| 145 |
+
)
|
| 146 |
+
else:
|
| 147 |
+
raise ValueError(f"Unsupported tokenizer type for {language_model_pretrained_name}")
|
| 148 |
+
|
| 149 |
+
# Load audio processor
|
| 150 |
+
audio_processor = VibeVoiceTokenizerProcessor(
|
| 151 |
+
sampling_rate=target_sample_rate,
|
| 152 |
+
normalize_audio=normalize_audio,
|
| 153 |
+
target_dB_FS=config.get("target_dB_FS", -25),
|
| 154 |
+
eps=config.get("eps", 1e-6),
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
return cls(
|
| 158 |
+
tokenizer=tokenizer,
|
| 159 |
+
audio_processor=audio_processor,
|
| 160 |
+
speech_tok_compress_ratio=speech_tok_compress_ratio,
|
| 161 |
+
target_sample_rate=target_sample_rate,
|
| 162 |
+
normalize_audio=normalize_audio,
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
def save_pretrained(self, save_directory: Union[str, os.PathLike], **kwargs):
|
| 166 |
+
"""
|
| 167 |
+
Save processor configuration to a directory.
|
| 168 |
+
|
| 169 |
+
Args:
|
| 170 |
+
save_directory: Directory to save the configuration
|
| 171 |
+
**kwargs: Additional keyword arguments
|
| 172 |
+
"""
|
| 173 |
+
import json
|
| 174 |
+
|
| 175 |
+
os.makedirs(save_directory, exist_ok=True)
|
| 176 |
+
|
| 177 |
+
# Save processor configuration
|
| 178 |
+
processor_config = {
|
| 179 |
+
"processor_class": "VibeVoiceASRProcessor",
|
| 180 |
+
"speech_tok_compress_ratio": self.speech_tok_compress_ratio,
|
| 181 |
+
"target_sample_rate": self.target_sample_rate,
|
| 182 |
+
"normalize_audio": self.normalize_audio,
|
| 183 |
+
"target_dB_FS": -25,
|
| 184 |
+
"eps": 1e-6,
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
config_path = os.path.join(save_directory, "preprocessor_config.json")
|
| 188 |
+
with open(config_path, 'w') as f:
|
| 189 |
+
json.dump(processor_config, f, indent=2)
|
| 190 |
+
|
| 191 |
+
logger.info(f"Processor configuration saved in {config_path}")
|
| 192 |
+
|
| 193 |
+
def __call__(
|
| 194 |
+
self,
|
| 195 |
+
audio: Optional[Union[str, np.ndarray, torch.Tensor, List[Union[str, np.ndarray, torch.Tensor]]]] = None,
|
| 196 |
+
sampling_rate: Optional[int] = None,
|
| 197 |
+
return_tensors: Optional[Union[str, TensorType]] = None,
|
| 198 |
+
padding: bool = True,
|
| 199 |
+
max_length: Optional[int] = None,
|
| 200 |
+
truncation: bool = False,
|
| 201 |
+
add_generation_prompt: bool = True,
|
| 202 |
+
use_streaming: bool = True,
|
| 203 |
+
context_info: Optional[str] = None,
|
| 204 |
+
**kwargs
|
| 205 |
+
) -> BatchEncoding:
|
| 206 |
+
"""
|
| 207 |
+
Process audio input for ASR model.
|
| 208 |
+
|
| 209 |
+
Args:
|
| 210 |
+
audio: Audio input(s). Can be:
|
| 211 |
+
- str: Path to audio file
|
| 212 |
+
- np.ndarray: Audio array
|
| 213 |
+
- torch.Tensor: Audio tensor
|
| 214 |
+
- List of the above for batch processing
|
| 215 |
+
sampling_rate: Sampling rate of input audio
|
| 216 |
+
return_tensors: Output format ('pt' for PyTorch, 'np' for NumPy)
|
| 217 |
+
padding: Whether to pad batch inputs
|
| 218 |
+
max_length: Maximum sequence length
|
| 219 |
+
truncation: Whether to truncate long sequences
|
| 220 |
+
add_generation_prompt: Whether to add generation prompt for inference
|
| 221 |
+
use_streaming: Whether to use streaming mode (True by default, auto False if <60s)
|
| 222 |
+
context_info: Optional context information (e.g., hotwords, metadata) to help transcription
|
| 223 |
+
|
| 224 |
+
Returns:
|
| 225 |
+
BatchEncoding with:
|
| 226 |
+
- input_ids: Token IDs for the model
|
| 227 |
+
- attention_mask: Attention mask
|
| 228 |
+
- acoustic_input_mask: Mask indicating speech token positions
|
| 229 |
+
- speech_tensors: Processed speech features
|
| 230 |
+
- speech_masks: Valid speech masks
|
| 231 |
+
- vae_tok_seqlens: Length of each speech segment in tokens
|
| 232 |
+
"""
|
| 233 |
+
if audio is None:
|
| 234 |
+
raise ValueError("Audio input is required for ASR processing")
|
| 235 |
+
|
| 236 |
+
# Handle single vs batch input
|
| 237 |
+
if isinstance(audio, list):
|
| 238 |
+
is_batched = True
|
| 239 |
+
audio_list = audio
|
| 240 |
+
else:
|
| 241 |
+
is_batched = False
|
| 242 |
+
audio_list = [audio]
|
| 243 |
+
|
| 244 |
+
# Process each audio input
|
| 245 |
+
all_encodings = []
|
| 246 |
+
for audio_input in audio_list:
|
| 247 |
+
encoding = self._process_single_audio(
|
| 248 |
+
audio_input,
|
| 249 |
+
sampling_rate=sampling_rate,
|
| 250 |
+
add_generation_prompt=add_generation_prompt,
|
| 251 |
+
use_streaming=use_streaming,
|
| 252 |
+
context_info=context_info,
|
| 253 |
+
)
|
| 254 |
+
all_encodings.append(encoding)
|
| 255 |
+
|
| 256 |
+
# Combine into batch
|
| 257 |
+
batch_encoding = self._batch_encode(
|
| 258 |
+
all_encodings,
|
| 259 |
+
padding=padding,
|
| 260 |
+
max_length=max_length,
|
| 261 |
+
truncation=truncation,
|
| 262 |
+
return_tensors=return_tensors,
|
| 263 |
+
)
|
| 264 |
+
|
| 265 |
+
return batch_encoding
|
| 266 |
+
|
| 267 |
+
def _process_single_audio(
|
| 268 |
+
self,
|
| 269 |
+
audio: Union[str, np.ndarray, torch.Tensor],
|
| 270 |
+
sampling_rate: Optional[int] = None,
|
| 271 |
+
add_generation_prompt: bool = True,
|
| 272 |
+
use_streaming: bool = True,
|
| 273 |
+
context_info: Optional[str] = None,
|
| 274 |
+
) -> Dict[str, Any]:
|
| 275 |
+
"""
|
| 276 |
+
Process a single audio input.
|
| 277 |
+
|
| 278 |
+
Args:
|
| 279 |
+
audio: Single audio input
|
| 280 |
+
sampling_rate: Audio sampling rate
|
| 281 |
+
add_generation_prompt: Whether to add generation prompt
|
| 282 |
+
context_info: Optional context information (e.g., hotwords, metadata) to help transcription
|
| 283 |
+
|
| 284 |
+
Returns:
|
| 285 |
+
Dictionary with processed tokens and audio features
|
| 286 |
+
"""
|
| 287 |
+
# Process audio through audio processor
|
| 288 |
+
if isinstance(audio, str):
|
| 289 |
+
# Load from file using ffmpeg for better format support
|
| 290 |
+
if HAS_FFMPEG_UTILS:
|
| 291 |
+
try:
|
| 292 |
+
audio_array, file_sr = load_audio_use_ffmpeg(audio, resample=False)
|
| 293 |
+
except Exception as e:
|
| 294 |
+
# Fall back to soundfile if ffmpeg fails
|
| 295 |
+
warnings.warn(f"ffmpeg loading failed, falling back to soundfile: {e}")
|
| 296 |
+
import soundfile as sf
|
| 297 |
+
audio_array, file_sr = sf.read(audio)
|
| 298 |
+
if audio_array.ndim > 1:
|
| 299 |
+
audio_array = audio_array.mean(axis=1) # Convert to mono
|
| 300 |
+
else:
|
| 301 |
+
import soundfile as sf
|
| 302 |
+
audio_array, file_sr = sf.read(audio)
|
| 303 |
+
if audio_array.ndim > 1:
|
| 304 |
+
audio_array = audio_array.mean(axis=1) # Convert to mono
|
| 305 |
+
|
| 306 |
+
# Resample if needed
|
| 307 |
+
if file_sr != self.target_sample_rate:
|
| 308 |
+
import librosa
|
| 309 |
+
audio_array = librosa.resample(
|
| 310 |
+
audio_array,
|
| 311 |
+
orig_sr=file_sr,
|
| 312 |
+
target_sr=self.target_sample_rate
|
| 313 |
+
)
|
| 314 |
+
elif isinstance(audio, torch.Tensor):
|
| 315 |
+
audio_array = audio.cpu().numpy()
|
| 316 |
+
if audio_array.ndim > 1:
|
| 317 |
+
audio_array = audio_array.squeeze()
|
| 318 |
+
else:
|
| 319 |
+
audio_array = np.array(audio, dtype=np.float32)
|
| 320 |
+
if audio_array.ndim > 1:
|
| 321 |
+
audio_array = audio_array.squeeze()
|
| 322 |
+
|
| 323 |
+
# Ensure float32
|
| 324 |
+
audio_array = audio_array.astype(np.float32)
|
| 325 |
+
|
| 326 |
+
# Normalize if needed
|
| 327 |
+
if self.normalize_audio and self.audio_normalizer:
|
| 328 |
+
audio_array = self.audio_normalizer(audio_array)
|
| 329 |
+
|
| 330 |
+
# Calculate audio duration
|
| 331 |
+
audio_duration = len(audio_array) / self.target_sample_rate
|
| 332 |
+
|
| 333 |
+
# Auto-disable streaming for short audio (<60s)
|
| 334 |
+
if use_streaming and audio_duration < 60.0:
|
| 335 |
+
use_streaming = False
|
| 336 |
+
|
| 337 |
+
# Calculate token length based on streaming mode
|
| 338 |
+
# Non-streaming: uses ceil (encoder adds extra_padding for stride alignment)
|
| 339 |
+
# Streaming: uses floor (segments processed independently, no global alignment)
|
| 340 |
+
# if use_streaming:
|
| 341 |
+
# vae_tok_len = len(audio_array) // self.speech_tok_compress_ratio
|
| 342 |
+
# else:
|
| 343 |
+
vae_tok_len = math.ceil(len(audio_array) / self.speech_tok_compress_ratio)
|
| 344 |
+
|
| 345 |
+
# Build token sequence following training format
|
| 346 |
+
# 1. System prompt - use apply_chat_template then encode like in training
|
| 347 |
+
system_prompt_text = self.tokenizer.apply_chat_template(
|
| 348 |
+
[{"role": "system", "content": SYSTEM_PROMPT}],
|
| 349 |
+
tokenize=False
|
| 350 |
+
)
|
| 351 |
+
system_tokens = self.tokenizer.encode(system_prompt_text)
|
| 352 |
+
|
| 353 |
+
# 2. User input with speech tokens
|
| 354 |
+
# Build speech placeholder string
|
| 355 |
+
sp_start_token = self.tokenizer.convert_ids_to_tokens(self.speech_start_id)
|
| 356 |
+
sp_pad_token = self.tokenizer.convert_ids_to_tokens(self.speech_pad_id)
|
| 357 |
+
sp_end_token = self.tokenizer.convert_ids_to_tokens(self.speech_end_id)
|
| 358 |
+
|
| 359 |
+
# User suffix with audio duration info
|
| 360 |
+
show_keys = ['Start time', 'End time', 'Speaker ID', 'Content']
|
| 361 |
+
if context_info and context_info.strip():
|
| 362 |
+
user_suffix = f"This is a {audio_duration:.2f} seconds audio, with extra info: {context_info.strip()}\n\nPlease transcribe it with these keys: " + ", ".join(show_keys)
|
| 363 |
+
else:
|
| 364 |
+
user_suffix = f"This is a {audio_duration:.2f} seconds audio, please transcribe it with these keys: " + ", ".join(show_keys)
|
| 365 |
+
|
| 366 |
+
user_input_string = ''.join(
|
| 367 |
+
[sp_start_token] + [sp_pad_token] * vae_tok_len + [sp_end_token]
|
| 368 |
+
) + '\n' + user_suffix
|
| 369 |
+
|
| 370 |
+
user_tokens = self.tokenizer.apply_chat_template(
|
| 371 |
+
[{"role": "user", "content": user_input_string}],
|
| 372 |
+
tokenize=True
|
| 373 |
+
)
|
| 374 |
+
|
| 375 |
+
# Combine tokens
|
| 376 |
+
full_tokens = system_tokens + user_tokens
|
| 377 |
+
|
| 378 |
+
# Create acoustic input mask
|
| 379 |
+
acoustic_input_mask = [1 if token == self.speech_pad_id else 0 for token in full_tokens]
|
| 380 |
+
|
| 381 |
+
return {
|
| 382 |
+
"input_ids": full_tokens,
|
| 383 |
+
"acoustic_input_mask": acoustic_input_mask,
|
| 384 |
+
"speech": audio_array,
|
| 385 |
+
"vae_tok_len": vae_tok_len,
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
def _batch_encode(
|
| 389 |
+
self,
|
| 390 |
+
encodings: List[Dict[str, Any]],
|
| 391 |
+
padding: bool = True,
|
| 392 |
+
max_length: Optional[int] = None,
|
| 393 |
+
truncation: bool = False,
|
| 394 |
+
return_tensors: Optional[str] = None,
|
| 395 |
+
) -> BatchEncoding:
|
| 396 |
+
"""
|
| 397 |
+
Combine multiple encodings into a batch.
|
| 398 |
+
|
| 399 |
+
Args:
|
| 400 |
+
encodings: List of encoded samples
|
| 401 |
+
padding: Whether to pad sequences
|
| 402 |
+
max_length: Maximum sequence length
|
| 403 |
+
truncation: Whether to truncate
|
| 404 |
+
return_tensors: Output format
|
| 405 |
+
|
| 406 |
+
Returns:
|
| 407 |
+
BatchEncoding with batched data
|
| 408 |
+
"""
|
| 409 |
+
# Extract components
|
| 410 |
+
input_ids_list = [enc["input_ids"] for enc in encodings]
|
| 411 |
+
acoustic_masks_list = [enc["acoustic_input_mask"] for enc in encodings]
|
| 412 |
+
speech_list = [enc["speech"] for enc in encodings]
|
| 413 |
+
vae_tok_lens = [enc["vae_tok_len"] for enc in encodings]
|
| 414 |
+
|
| 415 |
+
# Determine max length for padding
|
| 416 |
+
if padding:
|
| 417 |
+
if max_length is not None:
|
| 418 |
+
target_length = max_length
|
| 419 |
+
else:
|
| 420 |
+
target_length = max(len(ids) for ids in input_ids_list)
|
| 421 |
+
|
| 422 |
+
# Pad sequences
|
| 423 |
+
padded_input_ids = []
|
| 424 |
+
padded_acoustic_masks = []
|
| 425 |
+
attention_masks = []
|
| 426 |
+
|
| 427 |
+
for input_ids, acoustic_mask in zip(input_ids_list, acoustic_masks_list):
|
| 428 |
+
# Truncate if needed
|
| 429 |
+
if truncation and len(input_ids) > target_length:
|
| 430 |
+
input_ids = input_ids[:target_length]
|
| 431 |
+
acoustic_mask = acoustic_mask[:target_length]
|
| 432 |
+
|
| 433 |
+
# Pad sequences to left (for autoregressive generation)
|
| 434 |
+
padding_length = target_length - len(input_ids)
|
| 435 |
+
padded_ids = [self.pad_id] * padding_length + input_ids
|
| 436 |
+
padded_acoustic = [0] * padding_length + acoustic_mask
|
| 437 |
+
attention_mask = [0] * padding_length + [1] * len(input_ids)
|
| 438 |
+
|
| 439 |
+
padded_input_ids.append(padded_ids)
|
| 440 |
+
padded_acoustic_masks.append(padded_acoustic)
|
| 441 |
+
attention_masks.append(attention_mask)
|
| 442 |
+
|
| 443 |
+
input_ids_list = padded_input_ids
|
| 444 |
+
acoustic_masks_list = padded_acoustic_masks
|
| 445 |
+
else:
|
| 446 |
+
attention_masks = [[1] * len(ids) for ids in input_ids_list]
|
| 447 |
+
|
| 448 |
+
# Process speech tensors - raw audio is 1D, so we keep it as is
|
| 449 |
+
max_speech_length = max(len(s) for s in speech_list)
|
| 450 |
+
padded_speeches = np.zeros((len(speech_list), max_speech_length), dtype=np.float32)
|
| 451 |
+
speech_masks = np.zeros((len(speech_list), max(vae_tok_lens)), dtype=bool)
|
| 452 |
+
|
| 453 |
+
for i, (speech, vae_len) in enumerate(zip(speech_list, vae_tok_lens)):
|
| 454 |
+
padded_speeches[i, :len(speech)] = speech
|
| 455 |
+
speech_masks[i, :vae_len] = True
|
| 456 |
+
|
| 457 |
+
# Create batch encoding
|
| 458 |
+
batch_encoding = BatchEncoding()
|
| 459 |
+
|
| 460 |
+
if return_tensors == "pt":
|
| 461 |
+
batch_encoding["input_ids"] = torch.tensor(input_ids_list, dtype=torch.long)
|
| 462 |
+
batch_encoding["attention_mask"] = torch.tensor(attention_masks, dtype=torch.long)
|
| 463 |
+
batch_encoding["acoustic_input_mask"] = torch.tensor(acoustic_masks_list, dtype=torch.bool)
|
| 464 |
+
batch_encoding["speech_tensors"] = torch.tensor(padded_speeches, dtype=torch.float32)
|
| 465 |
+
batch_encoding["speech_masks"] = torch.tensor(speech_masks, dtype=torch.bool)
|
| 466 |
+
# Note: vae_tok_seqlens and speech_type are not included as they are not model inputs
|
| 467 |
+
else:
|
| 468 |
+
batch_encoding["input_ids"] = input_ids_list if len(input_ids_list) > 1 else input_ids_list[0]
|
| 469 |
+
batch_encoding["attention_mask"] = attention_masks if len(attention_masks) > 1 else attention_masks[0]
|
| 470 |
+
batch_encoding["acoustic_input_mask"] = acoustic_masks_list if len(acoustic_masks_list) > 1 else acoustic_masks_list[0]
|
| 471 |
+
batch_encoding["speech_tensors"] = padded_speeches if len(padded_speeches) > 1 else padded_speeches[0]
|
| 472 |
+
batch_encoding["speech_masks"] = speech_masks if len(speech_masks) > 1 else speech_masks[0]
|
| 473 |
+
|
| 474 |
+
return batch_encoding
|
| 475 |
+
|
| 476 |
+
def batch_decode(self, *args, **kwargs):
|
| 477 |
+
"""
|
| 478 |
+
Decode batch of token IDs to text.
|
| 479 |
+
Forwards to tokenizer's batch_decode method.
|
| 480 |
+
"""
|
| 481 |
+
return self.tokenizer.batch_decode(*args, **kwargs)
|
| 482 |
+
|
| 483 |
+
def decode(self, *args, **kwargs):
|
| 484 |
+
"""
|
| 485 |
+
Decode token IDs to text.
|
| 486 |
+
Forwards to tokenizer's decode method.
|
| 487 |
+
"""
|
| 488 |
+
return self.tokenizer.decode(*args, **kwargs)
|
| 489 |
+
|
| 490 |
+
def post_process_transcription(self, text: str) -> List[Dict[str, Any]]:
|
| 491 |
+
"""
|
| 492 |
+
Post-process the generated transcription text to extract structured data.
|
| 493 |
+
|
| 494 |
+
Args:
|
| 495 |
+
text: Generated text from the model
|
| 496 |
+
|
| 497 |
+
Returns:
|
| 498 |
+
List of dictionaries with transcription segments
|
| 499 |
+
"""
|
| 500 |
+
try:
|
| 501 |
+
# Try to parse as JSON
|
| 502 |
+
if "```json" in text:
|
| 503 |
+
# Extract JSON from markdown code block
|
| 504 |
+
json_start = text.find("```json") + 7
|
| 505 |
+
json_end = text.find("```", json_start)
|
| 506 |
+
json_str = text[json_start:json_end].strip()
|
| 507 |
+
else:
|
| 508 |
+
# Try to find JSON array or object
|
| 509 |
+
json_start = text.find("[")
|
| 510 |
+
if json_start == -1:
|
| 511 |
+
json_start = text.find("{")
|
| 512 |
+
if json_start != -1:
|
| 513 |
+
# Find matching closing bracket
|
| 514 |
+
bracket_count = 0
|
| 515 |
+
json_end = json_start
|
| 516 |
+
for i in range(json_start, len(text)):
|
| 517 |
+
if text[i] in "[{":
|
| 518 |
+
bracket_count += 1
|
| 519 |
+
elif text[i] in "]}":
|
| 520 |
+
bracket_count -= 1
|
| 521 |
+
if bracket_count == 0:
|
| 522 |
+
json_end = i + 1
|
| 523 |
+
break
|
| 524 |
+
json_str = text[json_start:json_end]
|
| 525 |
+
else:
|
| 526 |
+
json_str = text
|
| 527 |
+
|
| 528 |
+
# Parse JSON
|
| 529 |
+
result = json.loads(json_str)
|
| 530 |
+
|
| 531 |
+
# Ensure it's a list
|
| 532 |
+
if isinstance(result, dict):
|
| 533 |
+
result = [result]
|
| 534 |
+
|
| 535 |
+
# Validate and clean up the result
|
| 536 |
+
cleaned_result = []
|
| 537 |
+
for item in result:
|
| 538 |
+
if isinstance(item, dict):
|
| 539 |
+
cleaned_item = {}
|
| 540 |
+
# Map keys to expected format
|
| 541 |
+
key_mapping = {
|
| 542 |
+
"Start time": "start_time",
|
| 543 |
+
"Start": "start_time",
|
| 544 |
+
"End time": "end_time",
|
| 545 |
+
"End": "end_time",
|
| 546 |
+
"Speaker ID": "speaker_id",
|
| 547 |
+
"Speaker": "speaker_id",
|
| 548 |
+
"Content": "text",
|
| 549 |
+
}
|
| 550 |
+
for key, mapped_key in key_mapping.items():
|
| 551 |
+
if key in item:
|
| 552 |
+
cleaned_item[mapped_key] = item[key]
|
| 553 |
+
|
| 554 |
+
if cleaned_item:
|
| 555 |
+
cleaned_result.append(cleaned_item)
|
| 556 |
+
|
| 557 |
+
return cleaned_result
|
| 558 |
+
|
| 559 |
+
except json.JSONDecodeError as e:
|
| 560 |
+
logger.warning(f"Failed to parse JSON from transcription: {e}")
|
| 561 |
+
logger.debug(f"Raw text: {text}")
|
| 562 |
+
return []
|
| 563 |
+
except Exception as e:
|
| 564 |
+
logger.warning(f"Error post-processing transcription: {e}")
|
| 565 |
+
return []
|
| 566 |
+
|
| 567 |
+
@property
|
| 568 |
+
def model_input_names(self):
|
| 569 |
+
"""Return the list of inputs accepted by the model."""
|
| 570 |
+
return ["input_ids", "attention_mask", "acoustic_input_mask", "speech_tensors", "speech_masks"]
|
| 571 |
+
|
| 572 |
+
__all__ = ["VibeVoiceASRProcessor"]
|
vibevoice/processor/vibevoice_processor.py
ADDED
|
@@ -0,0 +1,692 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
import warnings
|
| 3 |
+
from typing import List, Optional, Union, Dict, Any, Tuple
|
| 4 |
+
import os
|
| 5 |
+
import re
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
import torch
|
| 9 |
+
|
| 10 |
+
from transformers.tokenization_utils_base import BatchEncoding, PaddingStrategy, PreTokenizedInput, TextInput, TruncationStrategy
|
| 11 |
+
from transformers.utils import TensorType, logging
|
| 12 |
+
from .vibevoice_tokenizer_processor import AudioNormalizer
|
| 13 |
+
|
| 14 |
+
logger = logging.get_logger(__name__)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class VibeVoiceProcessor:
|
| 18 |
+
r"""
|
| 19 |
+
Constructs a VibeVoice processor which wraps a VibeVoice tokenizer and audio processor into a single processor.
|
| 20 |
+
|
| 21 |
+
[`VibeVoiceProcessor`] offers all the functionalities of [`VibeVoiceTokenizer`] and [`VibeVoiceTokenizerProcessor`].
|
| 22 |
+
See the [`~VibeVoiceProcessor.__call__`] and [`~VibeVoiceProcessor.decode`] for more information.
|
| 23 |
+
|
| 24 |
+
Args:
|
| 25 |
+
tokenizer (`VibeVoiceTextTokenizer` or `VibeVoiceTextTokenizerFast`):
|
| 26 |
+
The tokenizer for text processing.
|
| 27 |
+
audio_processor (`VibeVoiceTokenizerProcessor`):
|
| 28 |
+
The audio processor for speech processing.
|
| 29 |
+
speech_tok_compress_ratio (`int`, *optional*, defaults to 3200):
|
| 30 |
+
The compression ratio for speech tokenization.
|
| 31 |
+
db_normalize (`bool`, *optional*, defaults to True):
|
| 32 |
+
Whether to apply decibel normalization to audio inputs.
|
| 33 |
+
"""
|
| 34 |
+
|
| 35 |
+
def __init__(self, tokenizer=None, audio_processor=None, speech_tok_compress_ratio=3200, db_normalize=True, **kwargs):
|
| 36 |
+
self.tokenizer = tokenizer
|
| 37 |
+
self.audio_processor = audio_processor
|
| 38 |
+
self.speech_tok_compress_ratio = speech_tok_compress_ratio
|
| 39 |
+
self.db_normalize = db_normalize
|
| 40 |
+
self.audio_normalizer = AudioNormalizer() if db_normalize else None
|
| 41 |
+
self.system_prompt = " Transform the text provided by various speakers into speech output, utilizing the distinct voice of each respective speaker.\n"
|
| 42 |
+
|
| 43 |
+
@classmethod
|
| 44 |
+
def from_pretrained(cls, pretrained_model_name_or_path, **kwargs):
|
| 45 |
+
"""
|
| 46 |
+
Instantiate a VibeVoiceProcessor from a pretrained VibeVoice processor.
|
| 47 |
+
|
| 48 |
+
Args:
|
| 49 |
+
pretrained_model_name_or_path (`str` or `os.PathLike`):
|
| 50 |
+
This can be either:
|
| 51 |
+
- a string, the *model id* of a pretrained model
|
| 52 |
+
- a path to a *directory* containing processor config
|
| 53 |
+
|
| 54 |
+
Returns:
|
| 55 |
+
[`VibeVoiceProcessor`]: The processor object instantiated from pretrained model.
|
| 56 |
+
"""
|
| 57 |
+
import os
|
| 58 |
+
import json
|
| 59 |
+
from transformers.utils import cached_file
|
| 60 |
+
from .vibevoice_tokenizer_processor import VibeVoiceTokenizerProcessor
|
| 61 |
+
from vibevoice.modular.modular_vibevoice_text_tokenizer import (
|
| 62 |
+
VibeVoiceTextTokenizer,
|
| 63 |
+
VibeVoiceTextTokenizerFast
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
# Try to load from local path first, then from HF hub
|
| 67 |
+
config_path = os.path.join(pretrained_model_name_or_path, "preprocessor_config.json")
|
| 68 |
+
config = None
|
| 69 |
+
|
| 70 |
+
if os.path.exists(config_path):
|
| 71 |
+
# Local path exists
|
| 72 |
+
with open(config_path, 'r') as f:
|
| 73 |
+
config = json.load(f)
|
| 74 |
+
else:
|
| 75 |
+
# Try to load from HF hub
|
| 76 |
+
try:
|
| 77 |
+
config_file = cached_file(
|
| 78 |
+
pretrained_model_name_or_path,
|
| 79 |
+
"preprocessor_config.json",
|
| 80 |
+
**kwargs
|
| 81 |
+
)
|
| 82 |
+
with open(config_file, 'r') as f:
|
| 83 |
+
config = json.load(f)
|
| 84 |
+
except Exception as e:
|
| 85 |
+
logger.warning(f"Could not load preprocessor_config.json from {pretrained_model_name_or_path}: {e}")
|
| 86 |
+
logger.warning("Using default configuration")
|
| 87 |
+
config = {
|
| 88 |
+
"speech_tok_compress_ratio": 3200,
|
| 89 |
+
"db_normalize": True,
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
# Extract main processor parameters
|
| 93 |
+
speech_tok_compress_ratio = config.get("speech_tok_compress_ratio", 3200)
|
| 94 |
+
db_normalize = config.get("db_normalize", True)
|
| 95 |
+
|
| 96 |
+
# Load tokenizer - try from model path first, then fall back to Qwen
|
| 97 |
+
language_model_pretrained_name = config.get("language_model_pretrained_name", None) or kwargs.pop("language_model_pretrained_name", "Qwen/Qwen2.5-1.5B")
|
| 98 |
+
logger.info(f"Loading tokenizer from {language_model_pretrained_name}")
|
| 99 |
+
if 'qwen' in language_model_pretrained_name.lower():
|
| 100 |
+
tokenizer = VibeVoiceTextTokenizerFast.from_pretrained(
|
| 101 |
+
language_model_pretrained_name,
|
| 102 |
+
**kwargs
|
| 103 |
+
)
|
| 104 |
+
else:
|
| 105 |
+
raise ValueError(f"Unsupported tokenizer type for {language_model_pretrained_name}. Supported types: Qwen, Llama, Gemma.")
|
| 106 |
+
|
| 107 |
+
# Load audio processor
|
| 108 |
+
if "audio_processor" in config:
|
| 109 |
+
# Create audio processor from config
|
| 110 |
+
audio_config = config["audio_processor"]
|
| 111 |
+
audio_processor = VibeVoiceTokenizerProcessor(
|
| 112 |
+
sampling_rate=audio_config.get("sampling_rate", 24000),
|
| 113 |
+
normalize_audio=audio_config.get("normalize_audio", True),
|
| 114 |
+
target_dB_FS=audio_config.get("target_dB_FS", -25),
|
| 115 |
+
eps=audio_config.get("eps", 1e-6),
|
| 116 |
+
)
|
| 117 |
+
else:
|
| 118 |
+
# Create default audio processor
|
| 119 |
+
audio_processor = VibeVoiceTokenizerProcessor()
|
| 120 |
+
|
| 121 |
+
# Create and return the processor
|
| 122 |
+
return cls(
|
| 123 |
+
tokenizer=tokenizer,
|
| 124 |
+
audio_processor=audio_processor,
|
| 125 |
+
speech_tok_compress_ratio=speech_tok_compress_ratio,
|
| 126 |
+
db_normalize=db_normalize,
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
def save_pretrained(self, save_directory: Union[str, os.PathLike], **kwargs):
|
| 130 |
+
"""
|
| 131 |
+
Save a processor to a directory, so that it can be re-loaded using the
|
| 132 |
+
[`~VibeVoiceProcessor.from_pretrained`] class method.
|
| 133 |
+
|
| 134 |
+
Args:
|
| 135 |
+
save_directory (`str` or `os.PathLike`):
|
| 136 |
+
Directory where the processor will be saved.
|
| 137 |
+
"""
|
| 138 |
+
import os
|
| 139 |
+
import json
|
| 140 |
+
|
| 141 |
+
os.makedirs(save_directory, exist_ok=True)
|
| 142 |
+
|
| 143 |
+
# Save processor configuration
|
| 144 |
+
processor_config = {
|
| 145 |
+
"processor_class": "VibeVoiceProcessor",
|
| 146 |
+
"speech_tok_compress_ratio": self.speech_tok_compress_ratio,
|
| 147 |
+
"db_normalize": self.db_normalize,
|
| 148 |
+
"audio_processor": {
|
| 149 |
+
"feature_extractor_type": "VibeVoiceTokenizerProcessor",
|
| 150 |
+
"sampling_rate": getattr(self.audio_processor, 'sampling_rate', 24000),
|
| 151 |
+
"normalize_audio": getattr(self.audio_processor, 'normalize_audio', True),
|
| 152 |
+
"target_dB_FS": getattr(self.audio_processor, 'target_dB_FS', -25),
|
| 153 |
+
"eps": getattr(self.audio_processor, 'eps', 1e-6),
|
| 154 |
+
}
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
config_path = os.path.join(save_directory, "preprocessor_config.json")
|
| 158 |
+
with open(config_path, 'w') as f:
|
| 159 |
+
json.dump(processor_config, f, indent=2)
|
| 160 |
+
|
| 161 |
+
logger.info(f"Processor configuration saved in {config_path}")
|
| 162 |
+
|
| 163 |
+
def __call__(
|
| 164 |
+
self,
|
| 165 |
+
text: Optional[Union[str, List[str], TextInput, PreTokenizedInput, List[TextInput], List[PreTokenizedInput]]] = None,
|
| 166 |
+
voice_samples: Optional[Union[List[Union[str, np.ndarray]], List[List[Union[str, np.ndarray]]]]] = None,
|
| 167 |
+
padding: Union[bool, str, PaddingStrategy] = True,
|
| 168 |
+
truncation: Union[bool, str, TruncationStrategy] = False,
|
| 169 |
+
max_length: Optional[int] = None,
|
| 170 |
+
return_tensors: Optional[Union[str, TensorType]] = None,
|
| 171 |
+
return_attention_mask: bool = True,
|
| 172 |
+
**kwargs,
|
| 173 |
+
) -> BatchEncoding:
|
| 174 |
+
"""
|
| 175 |
+
Main method to process one or more podcast scripts with optional voice samples.
|
| 176 |
+
|
| 177 |
+
Args:
|
| 178 |
+
text (`str`, `List[str]`):
|
| 179 |
+
The input text(s) to process. Can be:
|
| 180 |
+
- A single script string
|
| 181 |
+
- A list of script strings for batch processing
|
| 182 |
+
- A path to a .json or .txt file
|
| 183 |
+
- A list of paths
|
| 184 |
+
voice_samples (`List[Union[str, np.ndarray]]`, `List[List[Union[str, np.ndarray]]]`, *optional*):
|
| 185 |
+
Voice samples for each script. Can be:
|
| 186 |
+
- A list of samples for a single script
|
| 187 |
+
- A list of lists for batch processing
|
| 188 |
+
padding (`bool`, `str` or `PaddingStrategy`, defaults to `True`):
|
| 189 |
+
Whether to pad sequences to the same length
|
| 190 |
+
truncation (`bool`, `str` or `TruncationStrategy`, defaults to `False`):
|
| 191 |
+
Whether to truncate sequences
|
| 192 |
+
max_length (`int`, *optional*):
|
| 193 |
+
Maximum length of the returned sequences
|
| 194 |
+
return_tensors (`str` or `TensorType`, *optional*):
|
| 195 |
+
If set, will return tensors of a particular framework
|
| 196 |
+
return_attention_mask (`bool`, defaults to `True`):
|
| 197 |
+
Whether to return the attention mask
|
| 198 |
+
|
| 199 |
+
Returns:
|
| 200 |
+
`BatchEncoding`: A BatchEncoding with the following fields:
|
| 201 |
+
- **input_ids** -- List of token id sequences or tensor
|
| 202 |
+
- **attention_mask** -- List of attention masks or tensor
|
| 203 |
+
- **speech_tensors** -- Padded speech inputs (if voice_samples provided)
|
| 204 |
+
- **speech_masks** -- Speech masks (if voice_samples provided)
|
| 205 |
+
- **speech_input_mask** -- Boolean masks indicating speech token positions
|
| 206 |
+
"""
|
| 207 |
+
# Handle single vs batch input
|
| 208 |
+
if isinstance(text, str) or (isinstance(text, list) and len(text) > 0 and not isinstance(text[0], str)):
|
| 209 |
+
# Single input
|
| 210 |
+
texts = [text]
|
| 211 |
+
is_batched = False
|
| 212 |
+
else:
|
| 213 |
+
# Batch input
|
| 214 |
+
texts = text
|
| 215 |
+
is_batched = True
|
| 216 |
+
|
| 217 |
+
# Handle voice samples
|
| 218 |
+
if voice_samples is not None:
|
| 219 |
+
if not is_batched or (isinstance(voice_samples[0], (str, np.ndarray))):
|
| 220 |
+
# Single set of voice samples
|
| 221 |
+
voice_samples_list = [voice_samples]
|
| 222 |
+
else:
|
| 223 |
+
# Batch of voice samples
|
| 224 |
+
voice_samples_list = voice_samples
|
| 225 |
+
else:
|
| 226 |
+
voice_samples_list = [None] * len(texts)
|
| 227 |
+
|
| 228 |
+
# Process each input
|
| 229 |
+
all_encodings = []
|
| 230 |
+
for text_input, voice_input in zip(texts, voice_samples_list):
|
| 231 |
+
encoding = self._process_single(text_input, voice_input)
|
| 232 |
+
all_encodings.append(encoding)
|
| 233 |
+
|
| 234 |
+
# Combine batch
|
| 235 |
+
batch_encoding = self._batch_encode(
|
| 236 |
+
all_encodings,
|
| 237 |
+
padding=padding,
|
| 238 |
+
truncation=truncation,
|
| 239 |
+
max_length=max_length,
|
| 240 |
+
return_tensors=return_tensors,
|
| 241 |
+
return_attention_mask=return_attention_mask,
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
return batch_encoding
|
| 245 |
+
|
| 246 |
+
def _process_single(
|
| 247 |
+
self,
|
| 248 |
+
text: Union[str, TextInput],
|
| 249 |
+
voice_samples: Optional[List[Union[str, np.ndarray]]] = None,
|
| 250 |
+
) -> Dict[str, Any]:
|
| 251 |
+
"""Process a single podcast script."""
|
| 252 |
+
# Determine if text is a file path or direct script
|
| 253 |
+
script = None
|
| 254 |
+
if isinstance(text, str):
|
| 255 |
+
# Check if it's a file path
|
| 256 |
+
if text.endswith('.json') and os.path.exists(text):
|
| 257 |
+
script = self._convert_json_to_script(text)
|
| 258 |
+
elif text.endswith('.txt') and os.path.exists(text):
|
| 259 |
+
script = self._convert_text_to_script(text)
|
| 260 |
+
else:
|
| 261 |
+
# Assume it's the script content directly
|
| 262 |
+
script = text
|
| 263 |
+
|
| 264 |
+
if script is None:
|
| 265 |
+
raise ValueError(f"Could not process input text: {text}")
|
| 266 |
+
|
| 267 |
+
# Parse the script
|
| 268 |
+
parsed_lines = self._parse_script(script)
|
| 269 |
+
all_speakers = list(set(speaker_id for speaker_id, _ in parsed_lines))
|
| 270 |
+
|
| 271 |
+
# Create system prompt
|
| 272 |
+
# system_tokens = self.tokenizer.encode(self.system_prompt, add_special_tokens=False)
|
| 273 |
+
system_tokens = self.tokenizer.encode(self.system_prompt)
|
| 274 |
+
|
| 275 |
+
# Process voice samples if provided
|
| 276 |
+
if voice_samples:
|
| 277 |
+
voice_tokens, voice_speech_inputs, voice_speech_masks = self._create_voice_prompt(voice_samples[:len(all_speakers)])
|
| 278 |
+
else:
|
| 279 |
+
voice_tokens, voice_speech_inputs, voice_speech_masks = [], [], []
|
| 280 |
+
|
| 281 |
+
# Build full token sequence
|
| 282 |
+
full_tokens = system_tokens + voice_tokens
|
| 283 |
+
speech_input_mask = [False] * len(system_tokens) + voice_speech_masks
|
| 284 |
+
|
| 285 |
+
# Add text input section
|
| 286 |
+
full_tokens += self.tokenizer.encode(' Text input:\n', add_special_tokens=False)
|
| 287 |
+
speech_input_mask += [False] * len(self.tokenizer.encode(' Text input:\n', add_special_tokens=False))
|
| 288 |
+
|
| 289 |
+
for speaker_id, speaker_text in parsed_lines:
|
| 290 |
+
speaker_text_tokens = self.tokenizer.encode(f" Speaker {speaker_id}:{speaker_text}\n", add_special_tokens=False)
|
| 291 |
+
full_tokens += speaker_text_tokens
|
| 292 |
+
speech_input_mask += [False] * len(speaker_text_tokens)
|
| 293 |
+
|
| 294 |
+
# Add speech output section
|
| 295 |
+
full_tokens += self.tokenizer.encode(' Speech output:\n', add_special_tokens=False) + [self.tokenizer.speech_start_id]
|
| 296 |
+
speech_input_mask += [False] * (len(self.tokenizer.encode(' Speech output:\n', add_special_tokens=False)) + 1)
|
| 297 |
+
|
| 298 |
+
return {
|
| 299 |
+
"input_ids": full_tokens,
|
| 300 |
+
"speech_inputs": voice_speech_inputs if voice_speech_inputs else None,
|
| 301 |
+
"speech_input_mask": speech_input_mask,
|
| 302 |
+
"parsed_script": parsed_lines,
|
| 303 |
+
"all_speakers": all_speakers,
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
def _batch_encode(
|
| 307 |
+
self,
|
| 308 |
+
encodings: List[Dict[str, Any]],
|
| 309 |
+
padding: Union[bool, str, PaddingStrategy] = True,
|
| 310 |
+
truncation: Union[bool, str, TruncationStrategy] = False,
|
| 311 |
+
max_length: Optional[int] = None,
|
| 312 |
+
return_tensors: Optional[Union[str, TensorType]] = None,
|
| 313 |
+
return_attention_mask: bool = True,
|
| 314 |
+
) -> BatchEncoding:
|
| 315 |
+
"""Combine multiple encodings into a batch with padding."""
|
| 316 |
+
# Extract input_ids and create attention_mask
|
| 317 |
+
input_ids_list = [enc["input_ids"] for enc in encodings]
|
| 318 |
+
speech_input_masks_list = [enc["speech_input_mask"] for enc in encodings]
|
| 319 |
+
|
| 320 |
+
# Determine padding strategy
|
| 321 |
+
if isinstance(padding, bool):
|
| 322 |
+
padding_strategy = PaddingStrategy.LONGEST if padding else PaddingStrategy.DO_NOT_PAD
|
| 323 |
+
elif isinstance(padding, str):
|
| 324 |
+
padding_strategy = PaddingStrategy(padding)
|
| 325 |
+
else:
|
| 326 |
+
padding_strategy = padding
|
| 327 |
+
|
| 328 |
+
# Apply padding to input_ids
|
| 329 |
+
if padding_strategy != PaddingStrategy.DO_NOT_PAD:
|
| 330 |
+
if padding_strategy == PaddingStrategy.LONGEST:
|
| 331 |
+
max_len = max(len(ids) for ids in input_ids_list)
|
| 332 |
+
elif padding_strategy == PaddingStrategy.MAX_LENGTH and max_length is not None:
|
| 333 |
+
max_len = max_length
|
| 334 |
+
else:
|
| 335 |
+
max_len = max(len(ids) for ids in input_ids_list)
|
| 336 |
+
|
| 337 |
+
# Pad sequences
|
| 338 |
+
padded_input_ids = []
|
| 339 |
+
attention_masks = []
|
| 340 |
+
padded_speech_input_masks = []
|
| 341 |
+
|
| 342 |
+
for input_ids, speech_mask in zip(input_ids_list, speech_input_masks_list):
|
| 343 |
+
# Truncate if needed
|
| 344 |
+
if truncation and len(input_ids) > max_len:
|
| 345 |
+
input_ids = input_ids[:max_len]
|
| 346 |
+
speech_mask = speech_mask[:max_len]
|
| 347 |
+
|
| 348 |
+
# Pad
|
| 349 |
+
padding_length = max_len - len(input_ids)
|
| 350 |
+
# padded_ids = [self.tokenizer.pad_token_id] * padding_length + input_ids
|
| 351 |
+
padded_ids = [self.tokenizer.pad_id] * padding_length + input_ids
|
| 352 |
+
attention_mask = [0] * padding_length + [1] * len(input_ids)
|
| 353 |
+
padded_speech_mask = [False] * padding_length + speech_mask
|
| 354 |
+
|
| 355 |
+
padded_input_ids.append(padded_ids)
|
| 356 |
+
attention_masks.append(attention_mask)
|
| 357 |
+
padded_speech_input_masks.append(padded_speech_mask)
|
| 358 |
+
|
| 359 |
+
input_ids_list = padded_input_ids
|
| 360 |
+
speech_input_masks_list = padded_speech_input_masks
|
| 361 |
+
else:
|
| 362 |
+
# No padding, just create attention masks
|
| 363 |
+
attention_masks = [[1] * len(ids) for ids in input_ids_list] if return_attention_mask else None
|
| 364 |
+
|
| 365 |
+
# Process speech inputs
|
| 366 |
+
all_speech_inputs = []
|
| 367 |
+
has_speech = False
|
| 368 |
+
for enc in encodings:
|
| 369 |
+
if enc["speech_inputs"] is not None:
|
| 370 |
+
all_speech_inputs.extend(enc["speech_inputs"])
|
| 371 |
+
has_speech = True
|
| 372 |
+
|
| 373 |
+
# Prepare batch encoding
|
| 374 |
+
batch_encoding = BatchEncoding()
|
| 375 |
+
|
| 376 |
+
# Handle tensor conversion
|
| 377 |
+
if return_tensors is not None:
|
| 378 |
+
batch_encoding["input_ids"] = torch.tensor(input_ids_list, dtype=torch.long)
|
| 379 |
+
if return_attention_mask and attention_masks is not None:
|
| 380 |
+
batch_encoding["attention_mask"] = torch.tensor(attention_masks, dtype=torch.long)
|
| 381 |
+
batch_encoding["speech_input_mask"] = torch.tensor(speech_input_masks_list, dtype=torch.bool)
|
| 382 |
+
else:
|
| 383 |
+
batch_encoding["input_ids"] = input_ids_list
|
| 384 |
+
if return_attention_mask and attention_masks is not None:
|
| 385 |
+
batch_encoding["attention_mask"] = attention_masks
|
| 386 |
+
batch_encoding["speech_input_mask"] = speech_input_masks_list
|
| 387 |
+
|
| 388 |
+
# Process speech tensors if present
|
| 389 |
+
if has_speech:
|
| 390 |
+
speech_dict = self.prepare_speech_inputs(
|
| 391 |
+
all_speech_inputs,
|
| 392 |
+
return_tensors=return_tensors,
|
| 393 |
+
)
|
| 394 |
+
batch_encoding["speech_tensors"] = speech_dict["padded_speeches"]
|
| 395 |
+
batch_encoding["speech_masks"] = speech_dict["speech_masks"]
|
| 396 |
+
else:
|
| 397 |
+
batch_encoding["speech_tensors"] = None
|
| 398 |
+
batch_encoding["speech_masks"] = None
|
| 399 |
+
|
| 400 |
+
# Add metadata
|
| 401 |
+
batch_encoding["parsed_scripts"] = [enc["parsed_script"] for enc in encodings]
|
| 402 |
+
batch_encoding["all_speakers_list"] = [enc["all_speakers"] for enc in encodings]
|
| 403 |
+
|
| 404 |
+
return batch_encoding
|
| 405 |
+
|
| 406 |
+
def _create_voice_prompt(
|
| 407 |
+
self,
|
| 408 |
+
speaker_samples: List[Union[str, np.ndarray]]
|
| 409 |
+
) -> Tuple[List[int], List[np.ndarray], List[bool]]:
|
| 410 |
+
"""
|
| 411 |
+
Create voice prompt tokens and process audio samples.
|
| 412 |
+
|
| 413 |
+
Returns:
|
| 414 |
+
tuple: (voice_tokens, voice_speech_inputs, voice_speech_masks)
|
| 415 |
+
"""
|
| 416 |
+
vae_token_id = self.tokenizer.speech_diffusion_id
|
| 417 |
+
|
| 418 |
+
voice_full_tokens = self.tokenizer.encode(' Voice input:\n', add_special_tokens=False)
|
| 419 |
+
voice_speech_inputs = []
|
| 420 |
+
voice_speech_masks = [False] * len(voice_full_tokens)
|
| 421 |
+
|
| 422 |
+
for speaker_id, speaker_audio in enumerate(speaker_samples):
|
| 423 |
+
prefix_tokens = self.tokenizer.encode(f" Speaker {speaker_id}:", add_special_tokens=False)
|
| 424 |
+
|
| 425 |
+
# Process audio
|
| 426 |
+
if isinstance(speaker_audio, str):
|
| 427 |
+
# Load audio from file
|
| 428 |
+
wav = self.audio_processor._load_audio_from_path(speaker_audio)
|
| 429 |
+
else:
|
| 430 |
+
wav = np.array(speaker_audio, dtype=np.float32)
|
| 431 |
+
|
| 432 |
+
# Apply normalization if needed
|
| 433 |
+
if self.db_normalize and self.audio_normalizer:
|
| 434 |
+
wav = self.audio_normalizer(wav)
|
| 435 |
+
|
| 436 |
+
# Calculate token length based on compression ratio
|
| 437 |
+
# if speaker_audio.endswith('.pt') or speaker_audio.endswith('.npy'):
|
| 438 |
+
# vae_tok_len = wav.shape[0]
|
| 439 |
+
# else:
|
| 440 |
+
vae_tok_len = math.ceil(wav.shape[0] / self.speech_tok_compress_ratio)
|
| 441 |
+
|
| 442 |
+
# Build tokens and masks
|
| 443 |
+
speaker_tokens = (prefix_tokens +
|
| 444 |
+
[self.tokenizer.speech_start_id] +
|
| 445 |
+
[vae_token_id] * vae_tok_len +
|
| 446 |
+
[self.tokenizer.speech_end_id] +
|
| 447 |
+
self.tokenizer.encode('\n', add_special_tokens=False))
|
| 448 |
+
|
| 449 |
+
vae_input_mask = ([False] * len(prefix_tokens) +
|
| 450 |
+
[False] +
|
| 451 |
+
[True] * vae_tok_len +
|
| 452 |
+
[False] +
|
| 453 |
+
[False])
|
| 454 |
+
|
| 455 |
+
voice_full_tokens.extend(speaker_tokens)
|
| 456 |
+
voice_speech_masks.extend(vae_input_mask)
|
| 457 |
+
voice_speech_inputs.append(wav)
|
| 458 |
+
|
| 459 |
+
return voice_full_tokens, voice_speech_inputs, voice_speech_masks
|
| 460 |
+
|
| 461 |
+
def prepare_speech_inputs(
|
| 462 |
+
self,
|
| 463 |
+
speech_inputs: List[np.ndarray],
|
| 464 |
+
return_tensors: Optional[Union[str, TensorType]] = None,
|
| 465 |
+
device: Optional[Union[str, torch.device]] = None,
|
| 466 |
+
dtype: Optional[torch.dtype] = None,
|
| 467 |
+
) -> Dict[str, Any]:
|
| 468 |
+
"""
|
| 469 |
+
Prepare speech inputs for model consumption.
|
| 470 |
+
|
| 471 |
+
Args:
|
| 472 |
+
speech_inputs: List of speech arrays
|
| 473 |
+
return_tensors: Output tensor type
|
| 474 |
+
device: Device to place tensors on
|
| 475 |
+
dtype: Data type for tensors
|
| 476 |
+
|
| 477 |
+
Returns:
|
| 478 |
+
Dictionary with padded_speeches and speech_masks
|
| 479 |
+
"""
|
| 480 |
+
if not speech_inputs:
|
| 481 |
+
return {"padded_speeches": None, "speech_masks": None}
|
| 482 |
+
|
| 483 |
+
# Calculate sequence lengths
|
| 484 |
+
vae_tok_seqlens = [math.ceil(s.shape[0] / self.speech_tok_compress_ratio) for s in speech_inputs]
|
| 485 |
+
# vae_tok_seqlens = [math.ceil(s.shape[0] / self.speech_tok_compress_ratio) if s.ndim == 1 else s.shape[0] for s in speech_inputs]
|
| 486 |
+
max_speech_length = max(s.shape[0] for s in speech_inputs)
|
| 487 |
+
|
| 488 |
+
# Pad speeches
|
| 489 |
+
if speech_inputs[0].ndim == 1:
|
| 490 |
+
padded_speeches = np.full((len(speech_inputs), max_speech_length), fill_value=0, dtype=np.float32)
|
| 491 |
+
else:
|
| 492 |
+
padded_speeches = np.full((len(speech_inputs), max_speech_length, speech_inputs[0].shape[-1]), fill_value=0, dtype=np.float32)
|
| 493 |
+
speech_masks = np.zeros((len(speech_inputs), max(vae_tok_seqlens)), dtype=np.bool_)
|
| 494 |
+
|
| 495 |
+
for i, (speech, vae_tok_length) in enumerate(zip(speech_inputs, vae_tok_seqlens)):
|
| 496 |
+
padded_speeches[i, :len(speech)] = speech
|
| 497 |
+
speech_masks[i, :vae_tok_length] = True
|
| 498 |
+
|
| 499 |
+
result = {
|
| 500 |
+
"padded_speeches": padded_speeches,
|
| 501 |
+
"speech_masks": speech_masks,
|
| 502 |
+
}
|
| 503 |
+
|
| 504 |
+
# Convert to tensors if requested
|
| 505 |
+
if return_tensors == "pt":
|
| 506 |
+
result["padded_speeches"] = torch.tensor(padded_speeches, device=device, dtype=dtype or torch.float32)
|
| 507 |
+
result["speech_masks"] = torch.tensor(speech_masks, device=device, dtype=torch.bool)
|
| 508 |
+
|
| 509 |
+
return result
|
| 510 |
+
|
| 511 |
+
def _convert_json_to_script(self, json_file: str) -> str:
|
| 512 |
+
"""
|
| 513 |
+
Convert JSON format to script format.
|
| 514 |
+
Expected JSON format:
|
| 515 |
+
[
|
| 516 |
+
{"speaker": "1", "text": "Hello everyone..."},
|
| 517 |
+
{"speaker": "2", "text": "Great to be here..."}
|
| 518 |
+
]
|
| 519 |
+
"""
|
| 520 |
+
import json
|
| 521 |
+
|
| 522 |
+
with open(json_file, 'r', encoding='utf-8') as f:
|
| 523 |
+
data = json.load(f)
|
| 524 |
+
|
| 525 |
+
if not isinstance(data, list):
|
| 526 |
+
raise ValueError("JSON file must contain a list of speaker entries")
|
| 527 |
+
|
| 528 |
+
script_lines = []
|
| 529 |
+
for item in data:
|
| 530 |
+
if not isinstance(item, dict):
|
| 531 |
+
logger.warning(f"Skipping non-dict entry: {item}")
|
| 532 |
+
continue
|
| 533 |
+
|
| 534 |
+
speaker = item.get('speaker')
|
| 535 |
+
text = item.get('text')
|
| 536 |
+
|
| 537 |
+
if speaker is None or text is None:
|
| 538 |
+
logger.warning(f"Skipping entry missing speaker or text: {item}")
|
| 539 |
+
continue
|
| 540 |
+
|
| 541 |
+
# Ensure speaker ID is valid
|
| 542 |
+
try:
|
| 543 |
+
speaker_id = int(speaker)
|
| 544 |
+
except (ValueError, TypeError):
|
| 545 |
+
logger.warning(f"Invalid speaker ID: {speaker}, skipping entry")
|
| 546 |
+
continue
|
| 547 |
+
|
| 548 |
+
# Clean up text
|
| 549 |
+
text = text.strip()
|
| 550 |
+
if text:
|
| 551 |
+
script_lines.append(f"Speaker {speaker_id}: {text}")
|
| 552 |
+
|
| 553 |
+
if not script_lines:
|
| 554 |
+
raise ValueError("No valid entries found in JSON file")
|
| 555 |
+
|
| 556 |
+
return "\n".join(script_lines)
|
| 557 |
+
|
| 558 |
+
def _convert_text_to_script(self, text_file: str) -> str:
|
| 559 |
+
"""
|
| 560 |
+
Convert text file to script format.
|
| 561 |
+
Handles multiple formats:
|
| 562 |
+
1. Already formatted as "Speaker X: text"
|
| 563 |
+
2. Plain text (assigns to Speaker 1)
|
| 564 |
+
|
| 565 |
+
Handles edge cases like multiple colons in a line.
|
| 566 |
+
"""
|
| 567 |
+
with open(text_file, 'r', encoding='utf-8') as f:
|
| 568 |
+
lines = f.readlines()
|
| 569 |
+
|
| 570 |
+
script_lines = []
|
| 571 |
+
current_speaker = 1
|
| 572 |
+
|
| 573 |
+
for line in lines:
|
| 574 |
+
line = line.strip()
|
| 575 |
+
if not line:
|
| 576 |
+
continue
|
| 577 |
+
|
| 578 |
+
# Try to parse as "Speaker X: text" format
|
| 579 |
+
# Use regex to be more robust
|
| 580 |
+
speaker_match = re.match(r'^Speaker\s+(\d+)\s*:\s*(.*)$', line, re.IGNORECASE)
|
| 581 |
+
|
| 582 |
+
if speaker_match:
|
| 583 |
+
speaker_id = int(speaker_match.group(1))
|
| 584 |
+
text = speaker_match.group(2).strip()
|
| 585 |
+
if text:
|
| 586 |
+
script_lines.append(f"Speaker {speaker_id}: {text}")
|
| 587 |
+
else:
|
| 588 |
+
# Treat as plain text - assign to current speaker
|
| 589 |
+
script_lines.append(f"Speaker {current_speaker}: {line}")
|
| 590 |
+
|
| 591 |
+
if not script_lines:
|
| 592 |
+
raise ValueError("No valid content found in text file")
|
| 593 |
+
|
| 594 |
+
return "\n".join(script_lines)
|
| 595 |
+
|
| 596 |
+
def _parse_script(self, script: str) -> List[Tuple[int, str]]:
|
| 597 |
+
"""Parse script into list of (speaker_id, text) tuples."""
|
| 598 |
+
lines = script.strip().split("\n")
|
| 599 |
+
parsed_lines = []
|
| 600 |
+
speaker_ids = []
|
| 601 |
+
|
| 602 |
+
# First pass: parse all lines and collect speaker IDs
|
| 603 |
+
for line in lines:
|
| 604 |
+
if not line.strip():
|
| 605 |
+
continue
|
| 606 |
+
|
| 607 |
+
# Use regex to handle edge cases like multiple colons
|
| 608 |
+
match = re.match(r'^Speaker\s+(\d+)\s*:\s*(.*)$', line.strip(), re.IGNORECASE)
|
| 609 |
+
|
| 610 |
+
if match:
|
| 611 |
+
speaker_id = int(match.group(1))
|
| 612 |
+
text = ' ' + match.group(2).strip()
|
| 613 |
+
parsed_lines.append((speaker_id, text))
|
| 614 |
+
speaker_ids.append(speaker_id)
|
| 615 |
+
else:
|
| 616 |
+
logger.warning(f"Could not parse line: '{line}'")
|
| 617 |
+
|
| 618 |
+
if not parsed_lines:
|
| 619 |
+
raise ValueError("No valid speaker lines found in script")
|
| 620 |
+
|
| 621 |
+
# Check if we need to normalize speaker IDs (only if all are > 0)
|
| 622 |
+
min_speaker_id = min(speaker_ids)
|
| 623 |
+
if min_speaker_id > 0:
|
| 624 |
+
# Normalize to start from 0
|
| 625 |
+
normalized_lines = []
|
| 626 |
+
for speaker_id, text in parsed_lines:
|
| 627 |
+
normalized_lines.append((speaker_id - 1, text))
|
| 628 |
+
return normalized_lines
|
| 629 |
+
else:
|
| 630 |
+
# Keep original IDs
|
| 631 |
+
return parsed_lines
|
| 632 |
+
|
| 633 |
+
def _merge_inputs(self, text_inputs: BatchEncoding, audio_inputs: Dict) -> BatchEncoding:
|
| 634 |
+
"""Merge text and audio inputs into a single BatchEncoding."""
|
| 635 |
+
# Start with text inputs
|
| 636 |
+
merged = BatchEncoding(text_inputs)
|
| 637 |
+
|
| 638 |
+
# Add audio-specific fields
|
| 639 |
+
if "audio" in audio_inputs:
|
| 640 |
+
merged["speech_inputs"] = audio_inputs["audio"]
|
| 641 |
+
if "streaming" in audio_inputs:
|
| 642 |
+
merged["streaming"] = audio_inputs["streaming"]
|
| 643 |
+
|
| 644 |
+
return merged
|
| 645 |
+
|
| 646 |
+
def batch_decode(self, *args, **kwargs):
|
| 647 |
+
"""
|
| 648 |
+
This method forwards all its arguments to VibeVoiceTextTokenizer's [`~PreTrainedTokenizer.batch_decode`].
|
| 649 |
+
Please refer to the docstring of this method for more information.
|
| 650 |
+
"""
|
| 651 |
+
return self.tokenizer.batch_decode(*args, **kwargs)
|
| 652 |
+
|
| 653 |
+
def decode(self, *args, **kwargs):
|
| 654 |
+
"""
|
| 655 |
+
This method forwards all its arguments to VibeVoiceTextTokenizer's [`~PreTrainedTokenizer.decode`].
|
| 656 |
+
Please refer to the docstring of this method for more information.
|
| 657 |
+
"""
|
| 658 |
+
return self.tokenizer.decode(*args, **kwargs)
|
| 659 |
+
|
| 660 |
+
@property
|
| 661 |
+
def model_input_names(self):
|
| 662 |
+
"""
|
| 663 |
+
Return the list of inputs accepted by the model.
|
| 664 |
+
"""
|
| 665 |
+
tokenizer_input_names = self.tokenizer.model_input_names
|
| 666 |
+
audio_processor_input_names = self.audio_processor.model_input_names
|
| 667 |
+
return list(dict.fromkeys(tokenizer_input_names + audio_processor_input_names + ["speech_inputs", "speech_input_mask"]))
|
| 668 |
+
|
| 669 |
+
def save_audio(self,
|
| 670 |
+
audio: Union[torch.Tensor, np.ndarray, List[Union[torch.Tensor, np.ndarray]]],
|
| 671 |
+
output_path: str = "output.wav",
|
| 672 |
+
sampling_rate: Optional[int] = None,
|
| 673 |
+
normalize: bool = False,
|
| 674 |
+
batch_prefix: str = "audio_",
|
| 675 |
+
) -> str:
|
| 676 |
+
"""
|
| 677 |
+
Save audio data to a file.
|
| 678 |
+
Args:
|
| 679 |
+
audio (Union[torch.Tensor, np.ndarray, List[Union[torch.Tensor, np.ndarray]]]):
|
| 680 |
+
The audio data to save. Can be a single tensor/array or a list of them.
|
| 681 |
+
output_path (str, optional): Path to save the audio file. Defaults to "output.wav".
|
| 682 |
+
sampling_rate (int, optional): Sampling rate for the audio. If None, uses the processor's default.
|
| 683 |
+
normalize (bool, optional): Whether to normalize the audio before saving. Defaults to False.
|
| 684 |
+
batch_prefix (str, optional): Prefix for batch audio files. Defaults to "audio_".
|
| 685 |
+
Returns:
|
| 686 |
+
str: The path to the saved audio file.
|
| 687 |
+
"""
|
| 688 |
+
return self.audio_processor.save_audio(audio, output_path=output_path, sampling_rate=sampling_rate, normalize=normalize, batch_prefix=batch_prefix)
|
| 689 |
+
|
| 690 |
+
__all__ = [
|
| 691 |
+
"VibeVoiceProcessor",
|
| 692 |
+
]
|
vibevoice/processor/vibevoice_streaming_processor.py
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
import warnings
|
| 3 |
+
from typing import List, Optional, Union, Dict, Any, Tuple
|
| 4 |
+
import os
|
| 5 |
+
import re
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
import torch
|
| 9 |
+
|
| 10 |
+
from transformers.tokenization_utils_base import BatchEncoding, PaddingStrategy, PreTokenizedInput, TextInput, TruncationStrategy
|
| 11 |
+
from transformers.utils import TensorType, logging
|
| 12 |
+
from .vibevoice_tokenizer_processor import AudioNormalizer
|
| 13 |
+
|
| 14 |
+
logger = logging.get_logger(__name__)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class VibeVoiceStreamingProcessor:
|
| 18 |
+
r"""
|
| 19 |
+
Constructs a VibeVoice Streaming processor which wraps a VibeVoice tokenizer and audio processor into a single processor.
|
| 20 |
+
|
| 21 |
+
Args:
|
| 22 |
+
tokenizer (`VibeVoiceTextTokenizer` or `VibeVoiceTextTokenizerFast`):
|
| 23 |
+
The tokenizer for text processing.
|
| 24 |
+
audio_processor (`VibeVoiceTokenizerProcessor`):
|
| 25 |
+
The audio processor for speech processing.
|
| 26 |
+
speech_tok_compress_ratio (`int`, *optional*, defaults to 3200):
|
| 27 |
+
The compression ratio for speech tokenization.
|
| 28 |
+
db_normalize (`bool`, *optional*, defaults to True):
|
| 29 |
+
Whether to apply decibel normalization to audio inputs.
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
def __init__(self, tokenizer=None, audio_processor=None, speech_tok_compress_ratio=3200, db_normalize=True, **kwargs):
|
| 33 |
+
self.tokenizer = tokenizer
|
| 34 |
+
self.audio_processor = audio_processor
|
| 35 |
+
self.speech_tok_compress_ratio = speech_tok_compress_ratio
|
| 36 |
+
self.db_normalize = db_normalize
|
| 37 |
+
self.audio_normalizer = AudioNormalizer() if db_normalize else None
|
| 38 |
+
|
| 39 |
+
@classmethod
|
| 40 |
+
def from_pretrained(cls, pretrained_model_name_or_path, **kwargs):
|
| 41 |
+
"""
|
| 42 |
+
Instantiate a VibeVoiceStreamingProcessor from a pretrained VibeVoice Streaming processor.
|
| 43 |
+
|
| 44 |
+
Args:
|
| 45 |
+
pretrained_model_name_or_path (`str` or `os.PathLike`):
|
| 46 |
+
This can be either:
|
| 47 |
+
- a string, the *model id* of a pretrained model
|
| 48 |
+
- a path to a *directory* containing processor config
|
| 49 |
+
|
| 50 |
+
Returns:
|
| 51 |
+
[`VibeVoiceStreamingProcessor`]: The processor object instantiated from pretrained model.
|
| 52 |
+
"""
|
| 53 |
+
import os
|
| 54 |
+
import json
|
| 55 |
+
from transformers.utils import cached_file
|
| 56 |
+
from .vibevoice_tokenizer_processor import VibeVoiceTokenizerProcessor
|
| 57 |
+
from vibevoice.modular.modular_vibevoice_text_tokenizer import (
|
| 58 |
+
VibeVoiceTextTokenizer,
|
| 59 |
+
VibeVoiceTextTokenizerFast
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
# Try to load from local path first, then from HF hub
|
| 63 |
+
config_path = os.path.join(pretrained_model_name_or_path, "preprocessor_config.json")
|
| 64 |
+
config = None
|
| 65 |
+
|
| 66 |
+
if os.path.exists(config_path):
|
| 67 |
+
# Local path exists
|
| 68 |
+
with open(config_path, 'r') as f:
|
| 69 |
+
config = json.load(f)
|
| 70 |
+
else:
|
| 71 |
+
# Try to load from HF hub
|
| 72 |
+
try:
|
| 73 |
+
config_file = cached_file(
|
| 74 |
+
pretrained_model_name_or_path,
|
| 75 |
+
"preprocessor_config.json",
|
| 76 |
+
**kwargs
|
| 77 |
+
)
|
| 78 |
+
with open(config_file, 'r') as f:
|
| 79 |
+
config = json.load(f)
|
| 80 |
+
except Exception as e:
|
| 81 |
+
logger.warning(f"Could not load preprocessor_config.json from {pretrained_model_name_or_path}: {e}")
|
| 82 |
+
logger.warning("Using default configuration")
|
| 83 |
+
config = {
|
| 84 |
+
"speech_tok_compress_ratio": 3200,
|
| 85 |
+
"db_normalize": True,
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
# Extract main processor parameters
|
| 89 |
+
speech_tok_compress_ratio = config.get("speech_tok_compress_ratio", 3200)
|
| 90 |
+
db_normalize = config.get("db_normalize", True)
|
| 91 |
+
|
| 92 |
+
# Load tokenizer - try from model path first, then fall back to Qwen
|
| 93 |
+
language_model_pretrained_name = config.get("language_model_pretrained_name", None) or kwargs.pop("language_model_pretrained_name", "Qwen/Qwen2.5-1.5B")
|
| 94 |
+
logger.info(f"Loading tokenizer from {language_model_pretrained_name}")
|
| 95 |
+
if 'qwen' in language_model_pretrained_name.lower():
|
| 96 |
+
tokenizer = VibeVoiceTextTokenizerFast.from_pretrained(
|
| 97 |
+
language_model_pretrained_name,
|
| 98 |
+
**kwargs
|
| 99 |
+
)
|
| 100 |
+
else:
|
| 101 |
+
raise ValueError(f"Unsupported tokenizer type for {language_model_pretrained_name}. Supported types: Qwen, Llama, Gemma.")
|
| 102 |
+
|
| 103 |
+
# Load audio processor
|
| 104 |
+
if "audio_processor" in config:
|
| 105 |
+
# Create audio processor from config
|
| 106 |
+
audio_config = config["audio_processor"]
|
| 107 |
+
audio_processor = VibeVoiceTokenizerProcessor(
|
| 108 |
+
sampling_rate=audio_config.get("sampling_rate", 24000),
|
| 109 |
+
normalize_audio=audio_config.get("normalize_audio", True),
|
| 110 |
+
target_dB_FS=audio_config.get("target_dB_FS", -25),
|
| 111 |
+
eps=audio_config.get("eps", 1e-6),
|
| 112 |
+
)
|
| 113 |
+
else:
|
| 114 |
+
# Create default audio processor
|
| 115 |
+
audio_processor = VibeVoiceTokenizerProcessor()
|
| 116 |
+
|
| 117 |
+
# Create and return the processor
|
| 118 |
+
return cls(
|
| 119 |
+
tokenizer=tokenizer,
|
| 120 |
+
audio_processor=audio_processor,
|
| 121 |
+
speech_tok_compress_ratio=speech_tok_compress_ratio,
|
| 122 |
+
db_normalize=db_normalize,
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
def save_pretrained(self, save_directory: Union[str, os.PathLike], **kwargs):
|
| 126 |
+
"""
|
| 127 |
+
Save a processor to a directory, so that it can be re-loaded using the
|
| 128 |
+
[`~VibeVoiceStreamingProcessor.from_pretrained`] class method.
|
| 129 |
+
|
| 130 |
+
Args:
|
| 131 |
+
save_directory (`str` or `os.PathLike`):
|
| 132 |
+
Directory where the processor will be saved.
|
| 133 |
+
"""
|
| 134 |
+
import os
|
| 135 |
+
import json
|
| 136 |
+
|
| 137 |
+
os.makedirs(save_directory, exist_ok=True)
|
| 138 |
+
|
| 139 |
+
# Save processor configuration
|
| 140 |
+
processor_config = {
|
| 141 |
+
"processor_class": "VibeVoiceStreamingProcessor",
|
| 142 |
+
"speech_tok_compress_ratio": self.speech_tok_compress_ratio,
|
| 143 |
+
"db_normalize": self.db_normalize,
|
| 144 |
+
"audio_processor": {
|
| 145 |
+
"feature_extractor_type": "VibeVoiceTokenizerProcessor",
|
| 146 |
+
"sampling_rate": getattr(self.audio_processor, 'sampling_rate', 24000),
|
| 147 |
+
"normalize_audio": getattr(self.audio_processor, 'normalize_audio', True),
|
| 148 |
+
"target_dB_FS": getattr(self.audio_processor, 'target_dB_FS', -25),
|
| 149 |
+
"eps": getattr(self.audio_processor, 'eps', 1e-6),
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
config_path = os.path.join(save_directory, "preprocessor_config.json")
|
| 154 |
+
with open(config_path, 'w') as f:
|
| 155 |
+
json.dump(processor_config, f, indent=2)
|
| 156 |
+
|
| 157 |
+
logger.info(f"Processor configuration saved in {config_path}")
|
| 158 |
+
|
| 159 |
+
def __call__(self) -> BatchEncoding:
|
| 160 |
+
"""
|
| 161 |
+
Note:
|
| 162 |
+
This method is intentionally not implemented in the streaming processor.
|
| 163 |
+
Use `process_input_with_cached_prompt` for streaming use cases.
|
| 164 |
+
"""
|
| 165 |
+
raise NotImplementedError(
|
| 166 |
+
"VibeVoiceStreamingProcessor.__call__ is not implemented. "
|
| 167 |
+
"Use process_input_with_cached_prompt for streaming inputs."
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
def process_input_with_cached_prompt(
|
| 171 |
+
self,
|
| 172 |
+
text: Optional[str] = None,
|
| 173 |
+
cached_prompt: Optional[Dict[str, Any]] = None,
|
| 174 |
+
padding: Union[bool, str, PaddingStrategy] = True,
|
| 175 |
+
truncation: Union[bool, str, TruncationStrategy] = False,
|
| 176 |
+
max_length: Optional[int] = None,
|
| 177 |
+
return_tensors: Optional[Union[str, TensorType]] = None,
|
| 178 |
+
return_attention_mask: bool = True,
|
| 179 |
+
**kwargs,
|
| 180 |
+
) -> BatchEncoding:
|
| 181 |
+
"""
|
| 182 |
+
Main method to process one text script based on cached prompt. The function currently only supports single examples.
|
| 183 |
+
|
| 184 |
+
Args:
|
| 185 |
+
text (`str`):
|
| 186 |
+
The input text to process.
|
| 187 |
+
cached_prompt (`Dict[str, Any]`, *optional*):
|
| 188 |
+
The cached prompt to use for processing. It contains the kv cache of the voice prompt.
|
| 189 |
+
padding (`bool`, `str` or `PaddingStrategy`, defaults to `True`):
|
| 190 |
+
Whether to pad sequences to the same length
|
| 191 |
+
truncation (`bool`, `str` or `TruncationStrategy`, defaults to `False`):
|
| 192 |
+
Whether to truncate sequences
|
| 193 |
+
max_length (`int`, *optional*):
|
| 194 |
+
Maximum length of the returned sequences
|
| 195 |
+
return_tensors (`str` or `TensorType`, *optional*):
|
| 196 |
+
If set, will return tensors of a particular framework
|
| 197 |
+
return_attention_mask (`bool`, defaults to `True`):
|
| 198 |
+
Whether to return the attention mask
|
| 199 |
+
|
| 200 |
+
Returns:
|
| 201 |
+
`BatchEncoding`: A BatchEncoding with the following fields:
|
| 202 |
+
- **input_ids** -- List of token id sequences or tensor
|
| 203 |
+
- **attention_mask** -- List of attention masks or tensor
|
| 204 |
+
- **tts_lm_input_ids** -- List of token id sequences or tensor used for TTS LM
|
| 205 |
+
- **tts_lm_attention_mask** -- List of attention masks or tensor used for TTS LM
|
| 206 |
+
- **tts_text_ids** -- List of token id sequences or tensor for TTS text input
|
| 207 |
+
- **speech_tensors** -- Padded speech inputs (if voice_samples provided)
|
| 208 |
+
- **speech_masks** -- Speech masks (if voice_samples provided)
|
| 209 |
+
- **speech_input_mask** -- Boolean masks indicating speech token positions
|
| 210 |
+
"""
|
| 211 |
+
# Only support single example
|
| 212 |
+
texts = [text]
|
| 213 |
+
cached_prompts = [cached_prompt]
|
| 214 |
+
is_batched = False
|
| 215 |
+
|
| 216 |
+
# Process each input
|
| 217 |
+
all_encodings = []
|
| 218 |
+
for text_input, cached_prompt_input in zip(texts, cached_prompts):
|
| 219 |
+
script_tokens = self.tokenizer.encode(text_input.strip() + "\n", add_special_tokens=False)
|
| 220 |
+
input_id_length = cached_prompt_input['lm']['last_hidden_state'].size(1)
|
| 221 |
+
tts_lm_input_id_length = cached_prompt_input['tts_lm']['last_hidden_state'].size(1)
|
| 222 |
+
|
| 223 |
+
# pseudo input ids and masks
|
| 224 |
+
input_ids = [self.tokenizer.pad_id] * input_id_length
|
| 225 |
+
tts_lm_input_ids = [self.tokenizer.pad_id] * tts_lm_input_id_length
|
| 226 |
+
speech_input_mask = [False] * tts_lm_input_id_length
|
| 227 |
+
|
| 228 |
+
encoding = {
|
| 229 |
+
"input_ids": input_ids,
|
| 230 |
+
"tts_lm_input_ids": tts_lm_input_ids,
|
| 231 |
+
"tts_text_ids": script_tokens,
|
| 232 |
+
"speech_inputs": None,
|
| 233 |
+
"speech_input_mask": speech_input_mask,
|
| 234 |
+
}
|
| 235 |
+
all_encodings.append(encoding)
|
| 236 |
+
|
| 237 |
+
# Combine batch
|
| 238 |
+
batch_encoding = self._batch_encode(
|
| 239 |
+
all_encodings,
|
| 240 |
+
padding=padding,
|
| 241 |
+
truncation=truncation,
|
| 242 |
+
max_length=max_length,
|
| 243 |
+
return_tensors=return_tensors,
|
| 244 |
+
return_attention_mask=return_attention_mask,
|
| 245 |
+
)
|
| 246 |
+
|
| 247 |
+
return batch_encoding
|
| 248 |
+
|
| 249 |
+
def _batch_encode(
|
| 250 |
+
self,
|
| 251 |
+
encodings: List[Dict[str, Any]],
|
| 252 |
+
padding: Union[bool, str, PaddingStrategy] = True,
|
| 253 |
+
truncation: Union[bool, str, TruncationStrategy] = False,
|
| 254 |
+
max_length: Optional[int] = None,
|
| 255 |
+
return_tensors: Optional[Union[str, TensorType]] = None,
|
| 256 |
+
return_attention_mask: bool = True,
|
| 257 |
+
) -> BatchEncoding:
|
| 258 |
+
"""Combine multiple encodings into a batch with padding."""
|
| 259 |
+
# Extract input_ids and create attention_mask
|
| 260 |
+
input_ids_list = [enc["input_ids"] for enc in encodings]
|
| 261 |
+
tts_lm_input_ids_list = [enc["tts_lm_input_ids"] for enc in encodings]
|
| 262 |
+
tts_text_ids_list = [enc["tts_text_ids"] for enc in encodings]
|
| 263 |
+
speech_input_masks_list = [enc["speech_input_mask"] for enc in encodings]
|
| 264 |
+
|
| 265 |
+
attention_masks = [[1] * len(ids) for ids in input_ids_list] if return_attention_mask else None
|
| 266 |
+
tts_lm_attention_masks = [[1] * len(ids) for ids in tts_lm_input_ids_list] if return_attention_mask else None
|
| 267 |
+
|
| 268 |
+
# Process speech inputs
|
| 269 |
+
all_speech_inputs = []
|
| 270 |
+
has_speech = False
|
| 271 |
+
for enc in encodings:
|
| 272 |
+
if enc["speech_inputs"] is not None:
|
| 273 |
+
all_speech_inputs.extend(enc["speech_inputs"])
|
| 274 |
+
has_speech = True
|
| 275 |
+
|
| 276 |
+
# Prepare batch encoding
|
| 277 |
+
batch_encoding = BatchEncoding()
|
| 278 |
+
|
| 279 |
+
# Handle tensor conversion
|
| 280 |
+
if return_tensors is not None:
|
| 281 |
+
batch_encoding["input_ids"] = torch.tensor(input_ids_list, dtype=torch.long)
|
| 282 |
+
batch_encoding["tts_lm_input_ids"] = torch.tensor(tts_lm_input_ids_list, dtype=torch.long)
|
| 283 |
+
batch_encoding["tts_text_ids"] = torch.tensor(tts_text_ids_list, dtype=torch.long)
|
| 284 |
+
|
| 285 |
+
if return_attention_mask and attention_masks is not None:
|
| 286 |
+
batch_encoding["attention_mask"] = torch.tensor(attention_masks, dtype=torch.long)
|
| 287 |
+
batch_encoding["tts_lm_attention_mask"] = torch.tensor(tts_lm_attention_masks, dtype=torch.long)
|
| 288 |
+
|
| 289 |
+
batch_encoding["speech_input_mask"] = torch.tensor(speech_input_masks_list, dtype=torch.bool)
|
| 290 |
+
else:
|
| 291 |
+
batch_encoding["input_ids"] = input_ids_list
|
| 292 |
+
batch_encoding["tts_lm_input_ids"] = tts_lm_input_ids_list
|
| 293 |
+
batch_encoding["tts_text_ids"] = tts_text_ids_list
|
| 294 |
+
if return_attention_mask and attention_masks is not None:
|
| 295 |
+
batch_encoding["attention_mask"] = attention_masks
|
| 296 |
+
batch_encoding["tts_lm_attention_mask"] = tts_lm_attention_masks
|
| 297 |
+
batch_encoding["speech_input_mask"] = speech_input_masks_list
|
| 298 |
+
|
| 299 |
+
# Process speech tensors if present
|
| 300 |
+
if has_speech:
|
| 301 |
+
speech_dict = self.prepare_speech_inputs(
|
| 302 |
+
all_speech_inputs,
|
| 303 |
+
return_tensors=return_tensors,
|
| 304 |
+
)
|
| 305 |
+
batch_encoding["speech_tensors"] = speech_dict["padded_speeches"]
|
| 306 |
+
batch_encoding["speech_masks"] = speech_dict["speech_masks"]
|
| 307 |
+
else:
|
| 308 |
+
batch_encoding["speech_tensors"] = None
|
| 309 |
+
batch_encoding["speech_masks"] = None
|
| 310 |
+
|
| 311 |
+
return batch_encoding
|
| 312 |
+
|
| 313 |
+
def prepare_speech_inputs(
|
| 314 |
+
self,
|
| 315 |
+
speech_inputs: List[np.ndarray],
|
| 316 |
+
return_tensors: Optional[Union[str, TensorType]] = None,
|
| 317 |
+
device: Optional[Union[str, torch.device]] = None,
|
| 318 |
+
dtype: Optional[torch.dtype] = None,
|
| 319 |
+
) -> Dict[str, Any]:
|
| 320 |
+
"""
|
| 321 |
+
Prepare speech inputs for model consumption.
|
| 322 |
+
|
| 323 |
+
Args:
|
| 324 |
+
speech_inputs: List of speech arrays
|
| 325 |
+
return_tensors: Output tensor type
|
| 326 |
+
device: Device to place tensors on
|
| 327 |
+
dtype: Data type for tensors
|
| 328 |
+
|
| 329 |
+
Returns:
|
| 330 |
+
Dictionary with padded_speeches and speech_masks
|
| 331 |
+
"""
|
| 332 |
+
if not speech_inputs:
|
| 333 |
+
return {"padded_speeches": None, "speech_masks": None}
|
| 334 |
+
|
| 335 |
+
# Calculate sequence lengths
|
| 336 |
+
vae_tok_seqlens = [math.ceil(s.shape[0] / self.speech_tok_compress_ratio) for s in speech_inputs]
|
| 337 |
+
# vae_tok_seqlens = [math.ceil(s.shape[0] / self.speech_tok_compress_ratio) if s.ndim == 1 else s.shape[0] for s in speech_inputs]
|
| 338 |
+
max_speech_length = max(s.shape[0] for s in speech_inputs)
|
| 339 |
+
|
| 340 |
+
# Pad speeches
|
| 341 |
+
if speech_inputs[0].ndim == 1:
|
| 342 |
+
padded_speeches = np.full((len(speech_inputs), max_speech_length), fill_value=0, dtype=np.float32)
|
| 343 |
+
else:
|
| 344 |
+
padded_speeches = np.full((len(speech_inputs), max_speech_length, speech_inputs[0].shape[-1]), fill_value=0, dtype=np.float32)
|
| 345 |
+
speech_masks = np.zeros((len(speech_inputs), max(vae_tok_seqlens)), dtype=np.bool_)
|
| 346 |
+
|
| 347 |
+
for i, (speech, vae_tok_length) in enumerate(zip(speech_inputs, vae_tok_seqlens)):
|
| 348 |
+
padded_speeches[i, :len(speech)] = speech
|
| 349 |
+
speech_masks[i, :vae_tok_length] = True
|
| 350 |
+
|
| 351 |
+
result = {
|
| 352 |
+
"padded_speeches": padded_speeches,
|
| 353 |
+
"speech_masks": speech_masks,
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
# Convert to tensors if requested
|
| 357 |
+
if return_tensors == "pt":
|
| 358 |
+
result["padded_speeches"] = torch.tensor(padded_speeches, device=device, dtype=dtype or torch.float32)
|
| 359 |
+
result["speech_masks"] = torch.tensor(speech_masks, device=device, dtype=torch.bool)
|
| 360 |
+
|
| 361 |
+
return result
|
| 362 |
+
|
| 363 |
+
def batch_decode(self, *args, **kwargs):
|
| 364 |
+
"""
|
| 365 |
+
This method forwards all its arguments to VibeVoiceTextTokenizer's [`~PreTrainedTokenizer.batch_decode`].
|
| 366 |
+
Please refer to the docstring of this method for more information.
|
| 367 |
+
"""
|
| 368 |
+
return self.tokenizer.batch_decode(*args, **kwargs)
|
| 369 |
+
|
| 370 |
+
def decode(self, *args, **kwargs):
|
| 371 |
+
"""
|
| 372 |
+
This method forwards all its arguments to VibeVoiceTextTokenizer's [`~PreTrainedTokenizer.decode`].
|
| 373 |
+
Please refer to the docstring of this method for more information.
|
| 374 |
+
"""
|
| 375 |
+
return self.tokenizer.decode(*args, **kwargs)
|
| 376 |
+
|
| 377 |
+
@property
|
| 378 |
+
def model_input_names(self):
|
| 379 |
+
"""
|
| 380 |
+
Return the list of inputs accepted by the model.
|
| 381 |
+
"""
|
| 382 |
+
tokenizer_input_names = self.tokenizer.model_input_names
|
| 383 |
+
audio_processor_input_names = self.audio_processor.model_input_names
|
| 384 |
+
return list(dict.fromkeys(tokenizer_input_names + audio_processor_input_names + ["speech_inputs", "speech_input_mask"]))
|
| 385 |
+
|
| 386 |
+
def save_audio(self,
|
| 387 |
+
audio: Union[torch.Tensor, np.ndarray, List[Union[torch.Tensor, np.ndarray]]],
|
| 388 |
+
output_path: str = "output.wav",
|
| 389 |
+
sampling_rate: Optional[int] = None,
|
| 390 |
+
normalize: bool = False,
|
| 391 |
+
batch_prefix: str = "audio_",
|
| 392 |
+
) -> str:
|
| 393 |
+
"""
|
| 394 |
+
Save audio data to a file.
|
| 395 |
+
Args:
|
| 396 |
+
audio (Union[torch.Tensor, np.ndarray, List[Union[torch.Tensor, np.ndarray]]]):
|
| 397 |
+
The audio data to save. Can be a single tensor/array or a list of them.
|
| 398 |
+
output_path (str, optional): Path to save the audio file. Defaults to "output.wav".
|
| 399 |
+
sampling_rate (int, optional): Sampling rate for the audio. If None, uses the processor's default.
|
| 400 |
+
normalize (bool, optional): Whether to normalize the audio before saving. Defaults to False.
|
| 401 |
+
batch_prefix (str, optional): Prefix for batch audio files. Defaults to "audio_".
|
| 402 |
+
Returns:
|
| 403 |
+
str: The path to the saved audio file.
|
| 404 |
+
"""
|
| 405 |
+
return self.audio_processor.save_audio(audio, output_path=output_path, sampling_rate=sampling_rate, normalize=normalize, batch_prefix=batch_prefix)
|
| 406 |
+
|
| 407 |
+
__all__ = [
|
| 408 |
+
"VibeVoiceStreamingProcessor",
|
| 409 |
+
]
|
vibevoice/processor/vibevoice_tokenizer_processor.py
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Processor class for VibeVoice models.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
import json
|
| 7 |
+
import warnings
|
| 8 |
+
from typing import List, Optional, Union, Dict, Any
|
| 9 |
+
|
| 10 |
+
import numpy as np
|
| 11 |
+
import torch
|
| 12 |
+
|
| 13 |
+
from transformers.feature_extraction_utils import FeatureExtractionMixin
|
| 14 |
+
from transformers.utils import logging
|
| 15 |
+
|
| 16 |
+
from .audio_utils import AudioNormalizer
|
| 17 |
+
|
| 18 |
+
logger = logging.get_logger(__name__)
|
| 19 |
+
|
| 20 |
+
# Change from ProcessorMixin to FeatureExtractionMixin which is designed for single components
|
| 21 |
+
class VibeVoiceTokenizerProcessor(FeatureExtractionMixin):
|
| 22 |
+
"""
|
| 23 |
+
Processor for VibeVoice acoustic tokenizer models.
|
| 24 |
+
|
| 25 |
+
This processor handles audio preprocessing for VibeVoice models, including:
|
| 26 |
+
- Audio format conversion (stereo to mono)
|
| 27 |
+
- Optional audio normalization
|
| 28 |
+
- Streaming support for infinite-length audio
|
| 29 |
+
|
| 30 |
+
Args:
|
| 31 |
+
sampling_rate (int, optional): Expected sampling rate. Defaults to 24000.
|
| 32 |
+
normalize_audio (bool, optional): Whether to normalize audio. Defaults to True.
|
| 33 |
+
target_dB_FS (float, optional): Target dB FS for normalization. Defaults to -25.
|
| 34 |
+
eps (float, optional): Small value for numerical stability. Defaults to 1e-6.
|
| 35 |
+
"""
|
| 36 |
+
model_input_names = ["input_features"]
|
| 37 |
+
|
| 38 |
+
def __init__(
|
| 39 |
+
self,
|
| 40 |
+
sampling_rate: int = 24000,
|
| 41 |
+
normalize_audio: bool = True,
|
| 42 |
+
target_dB_FS: float = -25,
|
| 43 |
+
eps: float = 1e-6,
|
| 44 |
+
**kwargs,
|
| 45 |
+
):
|
| 46 |
+
super().__init__(**kwargs)
|
| 47 |
+
|
| 48 |
+
self.sampling_rate = sampling_rate
|
| 49 |
+
self.normalize_audio = normalize_audio
|
| 50 |
+
|
| 51 |
+
# Initialize audio normalizer if needed
|
| 52 |
+
if self.normalize_audio:
|
| 53 |
+
self.normalizer = AudioNormalizer(target_dB_FS=target_dB_FS, eps=eps)
|
| 54 |
+
else:
|
| 55 |
+
self.normalizer = None
|
| 56 |
+
|
| 57 |
+
# Save config
|
| 58 |
+
self.feature_extractor_dict = {
|
| 59 |
+
"sampling_rate": sampling_rate,
|
| 60 |
+
"normalize_audio": normalize_audio,
|
| 61 |
+
"target_dB_FS": target_dB_FS,
|
| 62 |
+
"eps": eps,
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
def _ensure_mono(self, audio: np.ndarray) -> np.ndarray:
|
| 66 |
+
"""
|
| 67 |
+
Convert stereo audio to mono if needed.
|
| 68 |
+
|
| 69 |
+
Args:
|
| 70 |
+
audio (np.ndarray): Input audio array
|
| 71 |
+
|
| 72 |
+
Returns:
|
| 73 |
+
np.ndarray: Mono audio array
|
| 74 |
+
"""
|
| 75 |
+
if len(audio.shape) == 1:
|
| 76 |
+
return audio
|
| 77 |
+
elif len(audio.shape) == 2:
|
| 78 |
+
if audio.shape[0] == 2: # (2, time)
|
| 79 |
+
return np.mean(audio, axis=0)
|
| 80 |
+
elif audio.shape[1] == 2: # (time, 2)
|
| 81 |
+
return np.mean(audio, axis=1)
|
| 82 |
+
else:
|
| 83 |
+
# If one dimension is 1, squeeze it
|
| 84 |
+
if audio.shape[0] == 1:
|
| 85 |
+
return audio.squeeze(0)
|
| 86 |
+
elif audio.shape[1] == 1:
|
| 87 |
+
return audio.squeeze(1)
|
| 88 |
+
else:
|
| 89 |
+
raise ValueError(f"Unexpected audio shape: {audio.shape}")
|
| 90 |
+
else:
|
| 91 |
+
raise ValueError(f"Audio should be 1D or 2D, got shape: {audio.shape}")
|
| 92 |
+
|
| 93 |
+
def _process_single_audio(self, audio: Union[np.ndarray, List[float]]) -> np.ndarray:
|
| 94 |
+
"""
|
| 95 |
+
Process a single audio array.
|
| 96 |
+
|
| 97 |
+
Args:
|
| 98 |
+
audio: Single audio input
|
| 99 |
+
|
| 100 |
+
Returns:
|
| 101 |
+
np.ndarray: Processed audio
|
| 102 |
+
"""
|
| 103 |
+
# Convert to numpy array
|
| 104 |
+
if not isinstance(audio, np.ndarray):
|
| 105 |
+
audio = np.array(audio, dtype=np.float32)
|
| 106 |
+
else:
|
| 107 |
+
audio = audio.astype(np.float32)
|
| 108 |
+
|
| 109 |
+
# Ensure mono
|
| 110 |
+
audio = self._ensure_mono(audio)
|
| 111 |
+
|
| 112 |
+
# Normalize if requested
|
| 113 |
+
if self.normalize_audio and self.normalizer is not None:
|
| 114 |
+
audio = self.normalizer(audio)
|
| 115 |
+
|
| 116 |
+
return audio
|
| 117 |
+
|
| 118 |
+
def __call__(
|
| 119 |
+
self,
|
| 120 |
+
audio: Union[str, np.ndarray, List[float], List[np.ndarray], List[List[float]], List[str]] = None,
|
| 121 |
+
sampling_rate: Optional[int] = None,
|
| 122 |
+
return_tensors: Optional[str] = None,
|
| 123 |
+
**kwargs,
|
| 124 |
+
):
|
| 125 |
+
"""
|
| 126 |
+
Process audio for VibeVoice models.
|
| 127 |
+
|
| 128 |
+
Args:
|
| 129 |
+
audio: Audio input(s) to process. Can be:
|
| 130 |
+
- str: Path to audio file
|
| 131 |
+
- np.ndarray: Audio array
|
| 132 |
+
- List[float]: Audio as list of floats
|
| 133 |
+
- List[np.ndarray]: Batch of audio arrays
|
| 134 |
+
- List[str]: Batch of audio file paths
|
| 135 |
+
sampling_rate (int, optional): Sampling rate of the input audio
|
| 136 |
+
return_tensors (str, optional): Return format ('pt' for PyTorch, 'np' for NumPy)
|
| 137 |
+
|
| 138 |
+
Returns:
|
| 139 |
+
dict: Processed audio inputs with keys:
|
| 140 |
+
- input_features: Audio tensor(s) ready for the model
|
| 141 |
+
"""
|
| 142 |
+
if audio is None:
|
| 143 |
+
raise ValueError("Audio input is required")
|
| 144 |
+
|
| 145 |
+
# Validate sampling rate
|
| 146 |
+
if sampling_rate is not None and sampling_rate != self.sampling_rate:
|
| 147 |
+
logger.warning(
|
| 148 |
+
f"Input sampling rate ({sampling_rate}) differs from expected "
|
| 149 |
+
f"sampling rate ({self.sampling_rate}). Please resample your audio."
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
+
# Handle different input types
|
| 153 |
+
if isinstance(audio, str):
|
| 154 |
+
# Single audio file path
|
| 155 |
+
audio = self._load_audio_from_path(audio)
|
| 156 |
+
is_batched = False
|
| 157 |
+
elif isinstance(audio, list):
|
| 158 |
+
if len(audio) == 0:
|
| 159 |
+
raise ValueError("Empty audio list provided")
|
| 160 |
+
|
| 161 |
+
# Check if it's a list of file paths
|
| 162 |
+
if all(isinstance(item, str) for item in audio):
|
| 163 |
+
# Batch of audio file paths
|
| 164 |
+
audio = [self._load_audio_from_path(path) for path in audio]
|
| 165 |
+
is_batched = True
|
| 166 |
+
else:
|
| 167 |
+
# Check if it's batched audio arrays
|
| 168 |
+
is_batched = isinstance(audio[0], (np.ndarray, list))
|
| 169 |
+
else:
|
| 170 |
+
# Single audio array or list
|
| 171 |
+
is_batched = False
|
| 172 |
+
|
| 173 |
+
# Process audio
|
| 174 |
+
if is_batched:
|
| 175 |
+
processed_audio = [self._process_single_audio(a) for a in audio]
|
| 176 |
+
else:
|
| 177 |
+
processed_audio = [self._process_single_audio(audio)]
|
| 178 |
+
|
| 179 |
+
# Convert to tensors if requested
|
| 180 |
+
if return_tensors == "pt":
|
| 181 |
+
if len(processed_audio) == 1:
|
| 182 |
+
# Create a proper batch dimension (B, T)
|
| 183 |
+
input_features = torch.from_numpy(processed_audio[0]).unsqueeze(0).unsqueeze(1)
|
| 184 |
+
else:
|
| 185 |
+
# For batched input with different lengths, create a batch properly
|
| 186 |
+
input_features = torch.stack([torch.from_numpy(a) for a in processed_audio]).unsqueeze(1)
|
| 187 |
+
elif return_tensors == "np":
|
| 188 |
+
if len(processed_audio) == 1:
|
| 189 |
+
input_features = processed_audio[0][np.newaxis, np.newaxis, :]
|
| 190 |
+
else:
|
| 191 |
+
input_features = np.stack(processed_audio)[:, np.newaxis, :]
|
| 192 |
+
else:
|
| 193 |
+
input_features = processed_audio[0] if len(processed_audio) == 1 else processed_audio
|
| 194 |
+
|
| 195 |
+
outputs = {
|
| 196 |
+
"audio": input_features, # Use "audio" instead of "input_features"
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
return outputs
|
| 200 |
+
|
| 201 |
+
def _load_audio_from_path(self, audio_path: str) -> np.ndarray:
|
| 202 |
+
"""
|
| 203 |
+
Load audio from file path.
|
| 204 |
+
|
| 205 |
+
Args:
|
| 206 |
+
audio_path (str): Path to audio file
|
| 207 |
+
|
| 208 |
+
Returns:
|
| 209 |
+
np.ndarray: Loaded audio array
|
| 210 |
+
"""
|
| 211 |
+
# Get file extension to determine loading method
|
| 212 |
+
file_ext = os.path.splitext(audio_path)[1].lower()
|
| 213 |
+
|
| 214 |
+
if file_ext in ['.wav', '.mp3', '.flac', '.m4a', '.ogg']:
|
| 215 |
+
# Audio file - use librosa
|
| 216 |
+
import librosa
|
| 217 |
+
audio_array, sr = librosa.load(
|
| 218 |
+
audio_path,
|
| 219 |
+
sr=self.sampling_rate,
|
| 220 |
+
mono=True
|
| 221 |
+
)
|
| 222 |
+
return audio_array
|
| 223 |
+
elif file_ext == '.pt':
|
| 224 |
+
# PyTorch tensor file
|
| 225 |
+
audio_tensor = torch.load(audio_path, map_location='cpu', weights_only=True).squeeze()
|
| 226 |
+
if isinstance(audio_tensor, torch.Tensor):
|
| 227 |
+
audio_array = audio_tensor.numpy()
|
| 228 |
+
else:
|
| 229 |
+
audio_array = np.array(audio_tensor)
|
| 230 |
+
return audio_array.astype(np.float32)
|
| 231 |
+
elif file_ext == '.npy':
|
| 232 |
+
# NumPy file
|
| 233 |
+
audio_array = np.load(audio_path)
|
| 234 |
+
return audio_array.astype(np.float32)
|
| 235 |
+
else:
|
| 236 |
+
raise ValueError(
|
| 237 |
+
f"Unsupported file format: {file_ext}. "
|
| 238 |
+
f"Supported formats: .wav, .mp3, .flac, .m4a, .ogg, .pt, .npy, .npz"
|
| 239 |
+
)
|
| 240 |
+
|
| 241 |
+
def preprocess_audio(
|
| 242 |
+
self,
|
| 243 |
+
audio_path_or_array: Union[str, np.ndarray],
|
| 244 |
+
normalize: Optional[bool] = None,
|
| 245 |
+
) -> np.ndarray:
|
| 246 |
+
"""
|
| 247 |
+
Convenience method to preprocess audio from file path or array.
|
| 248 |
+
This method is kept for backward compatibility but __call__ is recommended.
|
| 249 |
+
|
| 250 |
+
Args:
|
| 251 |
+
audio_path_or_array: Path to audio file or numpy array
|
| 252 |
+
normalize: Whether to normalize (overrides default setting)
|
| 253 |
+
|
| 254 |
+
Returns:
|
| 255 |
+
np.ndarray: Preprocessed audio array
|
| 256 |
+
"""
|
| 257 |
+
if isinstance(audio_path_or_array, str):
|
| 258 |
+
audio_array = self._load_audio_from_path(audio_path_or_array)
|
| 259 |
+
else:
|
| 260 |
+
audio_array = np.array(audio_path_or_array, dtype=np.float32)
|
| 261 |
+
|
| 262 |
+
# Override normalization setting if specified
|
| 263 |
+
original_normalize = self.normalize_audio
|
| 264 |
+
if normalize is not None:
|
| 265 |
+
self.normalize_audio = normalize
|
| 266 |
+
|
| 267 |
+
try:
|
| 268 |
+
processed = self._process_single_audio(audio_array)
|
| 269 |
+
finally:
|
| 270 |
+
# Restore original setting
|
| 271 |
+
self.normalize_audio = original_normalize
|
| 272 |
+
|
| 273 |
+
return processed
|
| 274 |
+
|
| 275 |
+
# Override to_dict method for configuration saving
|
| 276 |
+
def to_dict(self) -> Dict[str, Any]:
|
| 277 |
+
"""
|
| 278 |
+
Convert the object to a dict containing all attributes needed for serialization.
|
| 279 |
+
"""
|
| 280 |
+
return self.feature_extractor_dict
|
| 281 |
+
|
| 282 |
+
def save_audio(
|
| 283 |
+
self,
|
| 284 |
+
audio: Union[torch.Tensor, np.ndarray, List[Union[torch.Tensor, np.ndarray]]],
|
| 285 |
+
output_path: str = "output.wav",
|
| 286 |
+
sampling_rate: Optional[int] = None,
|
| 287 |
+
normalize: bool = False,
|
| 288 |
+
batch_prefix: str = "audio_",
|
| 289 |
+
):
|
| 290 |
+
"""
|
| 291 |
+
Save audio data to WAV file(s).
|
| 292 |
+
|
| 293 |
+
Args:
|
| 294 |
+
audio: Audio data to save. Can be:
|
| 295 |
+
- torch.Tensor: PyTorch tensor with shape (B, C, T) or (B, T) or (T)
|
| 296 |
+
- np.ndarray: NumPy array with shape (B, C, T) or (B, T) or (T)
|
| 297 |
+
- List of tensors or arrays
|
| 298 |
+
output_path: Path where to save the audio. If saving multiple files,
|
| 299 |
+
this is treated as a directory and individual files will be saved inside.
|
| 300 |
+
sampling_rate: Sampling rate for the saved audio. Defaults to the processor's rate.
|
| 301 |
+
normalize: Whether to normalize audio before saving.
|
| 302 |
+
batch_prefix: Prefix for batch files when saving multiple audios.
|
| 303 |
+
|
| 304 |
+
Returns:
|
| 305 |
+
List[str]: Paths to the saved audio files.
|
| 306 |
+
"""
|
| 307 |
+
if sampling_rate is None:
|
| 308 |
+
sampling_rate = self.sampling_rate
|
| 309 |
+
|
| 310 |
+
try:
|
| 311 |
+
import soundfile as sf
|
| 312 |
+
except ImportError:
|
| 313 |
+
raise ImportError(
|
| 314 |
+
"soundfile is required to save audio files. "
|
| 315 |
+
"Install it with: pip install soundfile"
|
| 316 |
+
)
|
| 317 |
+
|
| 318 |
+
# Ensure audio is in the right format
|
| 319 |
+
if isinstance(audio, torch.Tensor):
|
| 320 |
+
# Convert PyTorch tensor to numpy
|
| 321 |
+
audio_np = audio.float().detach().cpu().numpy()
|
| 322 |
+
elif isinstance(audio, np.ndarray):
|
| 323 |
+
audio_np = audio
|
| 324 |
+
elif isinstance(audio, list):
|
| 325 |
+
# Handle list of tensors or arrays
|
| 326 |
+
if all(isinstance(a, torch.Tensor) for a in audio):
|
| 327 |
+
audio_np = [a.float().detach().cpu().numpy() for a in audio]
|
| 328 |
+
else:
|
| 329 |
+
audio_np = audio
|
| 330 |
+
else:
|
| 331 |
+
raise ValueError(f"Unsupported audio type: {type(audio)}")
|
| 332 |
+
|
| 333 |
+
saved_paths = []
|
| 334 |
+
|
| 335 |
+
# Handle based on shape or type
|
| 336 |
+
if isinstance(audio_np, list):
|
| 337 |
+
# Multiple separate audios to save
|
| 338 |
+
output_dir = output_path
|
| 339 |
+
|
| 340 |
+
# Ensure output directory exists
|
| 341 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 342 |
+
|
| 343 |
+
# Save each audio
|
| 344 |
+
for i, audio_item in enumerate(audio_np):
|
| 345 |
+
audio_item = self._prepare_audio_for_save(audio_item, normalize)
|
| 346 |
+
file_path = os.path.join(output_dir, f"{batch_prefix}{i}.wav")
|
| 347 |
+
sf.write(file_path, audio_item, sampling_rate)
|
| 348 |
+
saved_paths.append(file_path)
|
| 349 |
+
|
| 350 |
+
else:
|
| 351 |
+
# Handle different dimensions
|
| 352 |
+
if len(audio_np.shape) >= 3: # (B, C, T) or similar
|
| 353 |
+
# Get batch size
|
| 354 |
+
batch_size = audio_np.shape[0]
|
| 355 |
+
|
| 356 |
+
if batch_size > 1:
|
| 357 |
+
# Multiple audios in a batch
|
| 358 |
+
output_dir = output_path
|
| 359 |
+
|
| 360 |
+
# Ensure output directory exists
|
| 361 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 362 |
+
|
| 363 |
+
# Save each audio in the batch
|
| 364 |
+
for i in range(batch_size):
|
| 365 |
+
# Extract single audio and remove channel dim if present
|
| 366 |
+
single_audio = audio_np[i]
|
| 367 |
+
if len(single_audio.shape) > 1:
|
| 368 |
+
if single_audio.shape[0] == 1: # (1, T)
|
| 369 |
+
single_audio = single_audio.squeeze(0)
|
| 370 |
+
|
| 371 |
+
single_audio = self._prepare_audio_for_save(single_audio, normalize)
|
| 372 |
+
file_path = os.path.join(output_dir, f"{batch_prefix}{i}.wav")
|
| 373 |
+
sf.write(file_path, single_audio, sampling_rate)
|
| 374 |
+
saved_paths.append(file_path)
|
| 375 |
+
else:
|
| 376 |
+
# Single audio with batch and channel dims
|
| 377 |
+
audio_item = audio_np.squeeze() # Remove batch and channel dimensions
|
| 378 |
+
audio_item = self._prepare_audio_for_save(audio_item, normalize)
|
| 379 |
+
sf.write(output_path, audio_item, sampling_rate)
|
| 380 |
+
saved_paths.append(output_path)
|
| 381 |
+
else:
|
| 382 |
+
# Single audio without batch dimension
|
| 383 |
+
audio_item = self._prepare_audio_for_save(audio_np, normalize)
|
| 384 |
+
sf.write(output_path, audio_item, sampling_rate)
|
| 385 |
+
saved_paths.append(output_path)
|
| 386 |
+
|
| 387 |
+
return saved_paths
|
| 388 |
+
|
| 389 |
+
def _prepare_audio_for_save(self, audio: np.ndarray, normalize: bool) -> np.ndarray:
|
| 390 |
+
"""
|
| 391 |
+
Prepare audio for saving by ensuring it's the right shape and optionally normalizing.
|
| 392 |
+
|
| 393 |
+
Args:
|
| 394 |
+
audio: Audio data as numpy array
|
| 395 |
+
normalize: Whether to normalize audio
|
| 396 |
+
|
| 397 |
+
Returns:
|
| 398 |
+
np.ndarray: Processed audio ready for saving
|
| 399 |
+
"""
|
| 400 |
+
# Ensure right dimensionality
|
| 401 |
+
if len(audio.shape) > 1 and audio.shape[0] == 1: # (1, T)
|
| 402 |
+
audio = audio.squeeze(0)
|
| 403 |
+
|
| 404 |
+
# Normalize if requested
|
| 405 |
+
if normalize:
|
| 406 |
+
max_val = np.abs(audio).max()
|
| 407 |
+
if max_val > 0:
|
| 408 |
+
audio = audio / max_val
|
| 409 |
+
|
| 410 |
+
return audio
|
| 411 |
+
|
| 412 |
+
|
| 413 |
+
__all__ = ["VibeVoiceTokenizerProcessor", "AudioNormalizer"]
|
vibevoice/schedule/__init__.py
ADDED
|
File without changes
|
vibevoice/schedule/dpm_solver.py
ADDED
|
@@ -0,0 +1,1065 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 TSAIL Team and The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
# DISCLAIMER: This file is strongly influenced by https://github.com/LuChengTHU/dpm-solver
|
| 16 |
+
|
| 17 |
+
import math
|
| 18 |
+
from typing import List, Optional, Tuple, Union
|
| 19 |
+
|
| 20 |
+
import numpy as np
|
| 21 |
+
import torch
|
| 22 |
+
|
| 23 |
+
from diffusers.configuration_utils import ConfigMixin, register_to_config
|
| 24 |
+
from diffusers.utils import deprecate
|
| 25 |
+
from diffusers.utils.torch_utils import randn_tensor
|
| 26 |
+
from diffusers.schedulers.scheduling_utils import KarrasDiffusionSchedulers, SchedulerMixin, SchedulerOutput
|
| 27 |
+
|
| 28 |
+
def betas_for_alpha_bar(
|
| 29 |
+
num_diffusion_timesteps,
|
| 30 |
+
max_beta=0.999,
|
| 31 |
+
alpha_transform_type="cosine",
|
| 32 |
+
):
|
| 33 |
+
"""
|
| 34 |
+
Create a beta schedule that discretizes the given alpha_t_bar function, which defines the cumulative product of
|
| 35 |
+
(1-beta) over time from t = [0,1].
|
| 36 |
+
|
| 37 |
+
Contains a function alpha_bar that takes an argument t and transforms it to the cumulative product of (1-beta) up
|
| 38 |
+
to that part of the diffusion process.
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
Args:
|
| 42 |
+
num_diffusion_timesteps (`int`): the number of betas to produce.
|
| 43 |
+
max_beta (`float`): the maximum beta to use; use values lower than 1 to
|
| 44 |
+
prevent singularities.
|
| 45 |
+
alpha_transform_type (`str`, *optional*, default to `cosine`): the type of noise schedule for alpha_bar.
|
| 46 |
+
Choose from `cosine` or `exp`
|
| 47 |
+
|
| 48 |
+
Returns:
|
| 49 |
+
betas (`np.ndarray`): the betas used by the scheduler to step the model outputs
|
| 50 |
+
"""
|
| 51 |
+
if alpha_transform_type == "cosine":
|
| 52 |
+
|
| 53 |
+
def alpha_bar_fn(t):
|
| 54 |
+
return math.cos((t + 0.008) / 1.008 * math.pi / 2) ** 2
|
| 55 |
+
# return math.cos(t * math.pi / 2 * 0.95) ** 2
|
| 56 |
+
|
| 57 |
+
elif alpha_transform_type == "exp":
|
| 58 |
+
|
| 59 |
+
def alpha_bar_fn(t):
|
| 60 |
+
return math.exp(t * -12.0)
|
| 61 |
+
|
| 62 |
+
elif alpha_transform_type == "cauchy":
|
| 63 |
+
# µ + γ tan (π (0.5 - x)) γ = 1, µ = 3
|
| 64 |
+
# alpha^2 = 1-1/(exp(λ)+1)
|
| 65 |
+
def alpha_bar_fn(t, gamma=1, mu=3):
|
| 66 |
+
snr = mu + gamma * math.tan(math.pi * (0.5 - t) * 0.9)
|
| 67 |
+
return 1 - 1 / (math.exp(snr) + 1.1)
|
| 68 |
+
|
| 69 |
+
elif alpha_transform_type == "laplace":
|
| 70 |
+
# µ − bsgn(0.5 − t) log(1 − 2|t − 0.5|) µ = 0, b = 1
|
| 71 |
+
def alpha_bar_fn(t, mu=0, b=1):
|
| 72 |
+
snr = mu - b * math.copysign(1, 0.5 - t) * math.log(1 - 2 * abs(t - 0.5) * 0.98)
|
| 73 |
+
return 1 - 1 / (math.exp(snr) + 1.02)
|
| 74 |
+
|
| 75 |
+
else:
|
| 76 |
+
raise ValueError(f"Unsupported alpha_transform_type: {alpha_transform_type}")
|
| 77 |
+
|
| 78 |
+
betas = []
|
| 79 |
+
for i in range(num_diffusion_timesteps):
|
| 80 |
+
t1 = i / num_diffusion_timesteps
|
| 81 |
+
t2 = (i + 1) / num_diffusion_timesteps
|
| 82 |
+
betas.append(min(1 - alpha_bar_fn(t2) / alpha_bar_fn(t1), max_beta))
|
| 83 |
+
return torch.tensor(betas, dtype=torch.float32)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
# Copied from diffusers.schedulers.scheduling_ddim.rescale_zero_terminal_snr
|
| 87 |
+
def rescale_zero_terminal_snr(betas):
|
| 88 |
+
"""
|
| 89 |
+
Rescales betas to have zero terminal SNR Based on https://arxiv.org/pdf/2305.08891.pdf (Algorithm 1)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
Args:
|
| 93 |
+
betas (`torch.Tensor`):
|
| 94 |
+
the betas that the scheduler is being initialized with.
|
| 95 |
+
|
| 96 |
+
Returns:
|
| 97 |
+
`torch.Tensor`: rescaled betas with zero terminal SNR
|
| 98 |
+
"""
|
| 99 |
+
# Convert betas to alphas_bar_sqrt
|
| 100 |
+
alphas = 1.0 - betas
|
| 101 |
+
alphas_cumprod = torch.cumprod(alphas, dim=0)
|
| 102 |
+
alphas_bar_sqrt = alphas_cumprod.sqrt()
|
| 103 |
+
|
| 104 |
+
# Store old values.
|
| 105 |
+
alphas_bar_sqrt_0 = alphas_bar_sqrt[0].clone()
|
| 106 |
+
alphas_bar_sqrt_T = alphas_bar_sqrt[-1].clone()
|
| 107 |
+
|
| 108 |
+
# Shift so the last timestep is zero.
|
| 109 |
+
alphas_bar_sqrt -= alphas_bar_sqrt_T
|
| 110 |
+
|
| 111 |
+
# Scale so the first timestep is back to the old value.
|
| 112 |
+
alphas_bar_sqrt *= alphas_bar_sqrt_0 / (alphas_bar_sqrt_0 - alphas_bar_sqrt_T)
|
| 113 |
+
|
| 114 |
+
# Convert alphas_bar_sqrt to betas
|
| 115 |
+
alphas_bar = alphas_bar_sqrt**2 # Revert sqrt
|
| 116 |
+
alphas = alphas_bar[1:] / alphas_bar[:-1] # Revert cumprod
|
| 117 |
+
alphas = torch.cat([alphas_bar[0:1], alphas])
|
| 118 |
+
betas = 1 - alphas
|
| 119 |
+
|
| 120 |
+
return betas
|
| 121 |
+
|
| 122 |
+
class DPMSolverMultistepScheduler(SchedulerMixin, ConfigMixin):
|
| 123 |
+
"""
|
| 124 |
+
`DPMSolverMultistepScheduler` is a fast dedicated high-order solver for diffusion ODEs.
|
| 125 |
+
|
| 126 |
+
This model inherits from [`SchedulerMixin`] and [`ConfigMixin`]. Check the superclass documentation for the generic
|
| 127 |
+
methods the library implements for all schedulers such as loading and saving.
|
| 128 |
+
|
| 129 |
+
Args:
|
| 130 |
+
num_train_timesteps (`int`, defaults to 1000):
|
| 131 |
+
The number of diffusion steps to train the model.
|
| 132 |
+
beta_start (`float`, defaults to 0.0001):
|
| 133 |
+
The starting `beta` value of inference.
|
| 134 |
+
beta_end (`float`, defaults to 0.02):
|
| 135 |
+
The final `beta` value.
|
| 136 |
+
beta_schedule (`str`, defaults to `"linear"`):
|
| 137 |
+
The beta schedule, a mapping from a beta range to a sequence of betas for stepping the model. Choose from
|
| 138 |
+
`linear`, `scaled_linear`, or `squaredcos_cap_v2`.
|
| 139 |
+
trained_betas (`np.ndarray`, *optional*):
|
| 140 |
+
Pass an array of betas directly to the constructor to bypass `beta_start` and `beta_end`.
|
| 141 |
+
solver_order (`int`, defaults to 2):
|
| 142 |
+
The DPMSolver order which can be `1` or `2` or `3`. It is recommended to use `solver_order=2` for guided
|
| 143 |
+
sampling, and `solver_order=3` for unconditional sampling.
|
| 144 |
+
prediction_type (`str`, defaults to `epsilon`, *optional*):
|
| 145 |
+
Prediction type of the scheduler function; can be `epsilon` (predicts the noise of the diffusion process),
|
| 146 |
+
`sample` (directly predicts the noisy sample`) or `v_prediction` (see section 2.4 of [Imagen
|
| 147 |
+
Video](https://imagen.research.google/video/paper.pdf) paper).
|
| 148 |
+
thresholding (`bool`, defaults to `False`):
|
| 149 |
+
Whether to use the "dynamic thresholding" method. This is unsuitable for latent-space diffusion models such
|
| 150 |
+
as Stable Diffusion.
|
| 151 |
+
dynamic_thresholding_ratio (`float`, defaults to 0.995):
|
| 152 |
+
The ratio for the dynamic thresholding method. Valid only when `thresholding=True`.
|
| 153 |
+
sample_max_value (`float`, defaults to 1.0):
|
| 154 |
+
The threshold value for dynamic thresholding. Valid only when `thresholding=True` and
|
| 155 |
+
`algorithm_type="dpmsolver++"`.
|
| 156 |
+
algorithm_type (`str`, defaults to `dpmsolver++`):
|
| 157 |
+
Algorithm type for the solver; can be `dpmsolver`, `dpmsolver++`, `sde-dpmsolver` or `sde-dpmsolver++`. The
|
| 158 |
+
`dpmsolver` type implements the algorithms in the [DPMSolver](https://huggingface.co/papers/2206.00927)
|
| 159 |
+
paper, and the `dpmsolver++` type implements the algorithms in the
|
| 160 |
+
[DPMSolver++](https://huggingface.co/papers/2211.01095) paper. It is recommended to use `dpmsolver++` or
|
| 161 |
+
`sde-dpmsolver++` with `solver_order=2` for guided sampling like in Stable Diffusion.
|
| 162 |
+
solver_type (`str`, defaults to `midpoint`):
|
| 163 |
+
Solver type for the second-order solver; can be `midpoint` or `heun`. The solver type slightly affects the
|
| 164 |
+
sample quality, especially for a small number of steps. It is recommended to use `midpoint` solvers.
|
| 165 |
+
lower_order_final (`bool`, defaults to `True`):
|
| 166 |
+
Whether to use lower-order solvers in the final steps. Only valid for < 15 inference steps. This can
|
| 167 |
+
stabilize the sampling of DPMSolver for steps < 15, especially for steps <= 10.
|
| 168 |
+
euler_at_final (`bool`, defaults to `False`):
|
| 169 |
+
Whether to use Euler's method in the final step. It is a trade-off between numerical stability and detail
|
| 170 |
+
richness. This can stabilize the sampling of the SDE variant of DPMSolver for small number of inference
|
| 171 |
+
steps, but sometimes may result in blurring.
|
| 172 |
+
use_karras_sigmas (`bool`, *optional*, defaults to `False`):
|
| 173 |
+
Whether to use Karras sigmas for step sizes in the noise schedule during the sampling process. If `True`,
|
| 174 |
+
the sigmas are determined according to a sequence of noise levels {σi}.
|
| 175 |
+
use_lu_lambdas (`bool`, *optional*, defaults to `False`):
|
| 176 |
+
Whether to use the uniform-logSNR for step sizes proposed by Lu's DPM-Solver in the noise schedule during
|
| 177 |
+
the sampling process. If `True`, the sigmas and time steps are determined according to a sequence of
|
| 178 |
+
`lambda(t)`.
|
| 179 |
+
final_sigmas_type (`str`, defaults to `"zero"`):
|
| 180 |
+
The final `sigma` value for the noise schedule during the sampling process. If `"sigma_min"`, the final
|
| 181 |
+
sigma is the same as the last sigma in the training schedule. If `zero`, the final sigma is set to 0.
|
| 182 |
+
lambda_min_clipped (`float`, defaults to `-inf`):
|
| 183 |
+
Clipping threshold for the minimum value of `lambda(t)` for numerical stability. This is critical for the
|
| 184 |
+
cosine (`squaredcos_cap_v2`) noise schedule.
|
| 185 |
+
variance_type (`str`, *optional*):
|
| 186 |
+
Set to "learned" or "learned_range" for diffusion models that predict variance. If set, the model's output
|
| 187 |
+
contains the predicted Gaussian variance.
|
| 188 |
+
timestep_spacing (`str`, defaults to `"linspace"`):
|
| 189 |
+
The way the timesteps should be scaled. Refer to Table 2 of the [Common Diffusion Noise Schedules and
|
| 190 |
+
Sample Steps are Flawed](https://huggingface.co/papers/2305.08891) for more information.
|
| 191 |
+
steps_offset (`int`, defaults to 0):
|
| 192 |
+
An offset added to the inference steps, as required by some model families.
|
| 193 |
+
rescale_betas_zero_snr (`bool`, defaults to `False`):
|
| 194 |
+
Whether to rescale the betas to have zero terminal SNR. This enables the model to generate very bright and
|
| 195 |
+
dark samples instead of limiting it to samples with medium brightness. Loosely related to
|
| 196 |
+
[`--offset_noise`](https://github.com/huggingface/diffusers/blob/74fd735eb073eb1d774b1ab4154a0876eb82f055/examples/dreambooth/train_dreambooth.py#L506).
|
| 197 |
+
"""
|
| 198 |
+
|
| 199 |
+
_compatibles = [e.name for e in KarrasDiffusionSchedulers]
|
| 200 |
+
order = 1
|
| 201 |
+
|
| 202 |
+
@register_to_config
|
| 203 |
+
def __init__(
|
| 204 |
+
self,
|
| 205 |
+
num_train_timesteps: int = 1000,
|
| 206 |
+
beta_start: float = 0.0001,
|
| 207 |
+
beta_end: float = 0.02,
|
| 208 |
+
beta_schedule: str = "linear",
|
| 209 |
+
trained_betas: Optional[Union[np.ndarray, List[float]]] = None,
|
| 210 |
+
solver_order: int = 2,
|
| 211 |
+
prediction_type: str = "epsilon",
|
| 212 |
+
thresholding: bool = False,
|
| 213 |
+
dynamic_thresholding_ratio: float = 0.995,
|
| 214 |
+
sample_max_value: float = 1.0,
|
| 215 |
+
algorithm_type: str = "dpmsolver++",
|
| 216 |
+
solver_type: str = "midpoint",
|
| 217 |
+
lower_order_final: bool = True,
|
| 218 |
+
euler_at_final: bool = False,
|
| 219 |
+
use_karras_sigmas: Optional[bool] = False,
|
| 220 |
+
use_lu_lambdas: Optional[bool] = False,
|
| 221 |
+
final_sigmas_type: Optional[str] = "zero", # "zero", "sigma_min"
|
| 222 |
+
lambda_min_clipped: float = -float("inf"),
|
| 223 |
+
variance_type: Optional[str] = None,
|
| 224 |
+
timestep_spacing: str = "linspace",
|
| 225 |
+
steps_offset: int = 0,
|
| 226 |
+
rescale_betas_zero_snr: bool = False,
|
| 227 |
+
):
|
| 228 |
+
if algorithm_type in ["dpmsolver", "sde-dpmsolver"]:
|
| 229 |
+
deprecation_message = f"algorithm_type {algorithm_type} is deprecated and will be removed in a future version. Choose from `dpmsolver++` or `sde-dpmsolver++` instead"
|
| 230 |
+
deprecate("algorithm_types dpmsolver and sde-dpmsolver", "1.0.0", deprecation_message)
|
| 231 |
+
|
| 232 |
+
if trained_betas is not None:
|
| 233 |
+
self.betas = torch.tensor(trained_betas, dtype=torch.float32)
|
| 234 |
+
elif beta_schedule == "linear":
|
| 235 |
+
self.betas = torch.linspace(beta_start, beta_end, num_train_timesteps, dtype=torch.float32)
|
| 236 |
+
elif beta_schedule == "scaled_linear":
|
| 237 |
+
# this schedule is very specific to the latent diffusion model.
|
| 238 |
+
self.betas = torch.linspace(beta_start**0.5, beta_end**0.5, num_train_timesteps, dtype=torch.float32) ** 2
|
| 239 |
+
elif beta_schedule == "squaredcos_cap_v2" or beta_schedule == "cosine":
|
| 240 |
+
# Glide cosine schedule
|
| 241 |
+
self.betas = betas_for_alpha_bar(num_train_timesteps, alpha_transform_type="cosine")
|
| 242 |
+
elif beta_schedule == "cauchy":
|
| 243 |
+
self.betas = betas_for_alpha_bar(num_train_timesteps, alpha_transform_type="cauchy")
|
| 244 |
+
elif beta_schedule == "laplace":
|
| 245 |
+
self.betas = betas_for_alpha_bar(num_train_timesteps, alpha_transform_type="laplace")
|
| 246 |
+
else:
|
| 247 |
+
raise NotImplementedError(f"{beta_schedule} is not implemented for {self.__class__}")
|
| 248 |
+
|
| 249 |
+
if rescale_betas_zero_snr:
|
| 250 |
+
self.betas = rescale_zero_terminal_snr(self.betas)
|
| 251 |
+
|
| 252 |
+
self.alphas = 1.0 - self.betas
|
| 253 |
+
self.alphas_cumprod = torch.cumprod(self.alphas, dim=0)
|
| 254 |
+
|
| 255 |
+
if rescale_betas_zero_snr:
|
| 256 |
+
# Close to 0 without being 0 so first sigma is not inf
|
| 257 |
+
# FP16 smallest positive subnormal works well here
|
| 258 |
+
self.alphas_cumprod[-1] = 2**-24
|
| 259 |
+
|
| 260 |
+
# Currently we only support VP-type noise schedule
|
| 261 |
+
self.alpha_t = torch.sqrt(self.alphas_cumprod)
|
| 262 |
+
self.sigma_t = torch.sqrt(1 - self.alphas_cumprod)
|
| 263 |
+
self.lambda_t = torch.log(self.alpha_t) - torch.log(self.sigma_t)
|
| 264 |
+
self.sigmas = ((1 - self.alphas_cumprod) / self.alphas_cumprod) ** 0.5
|
| 265 |
+
|
| 266 |
+
# standard deviation of the initial noise distribution
|
| 267 |
+
self.init_noise_sigma = 1.0
|
| 268 |
+
|
| 269 |
+
# settings for DPM-Solver
|
| 270 |
+
if algorithm_type not in ["dpmsolver", "dpmsolver++", "sde-dpmsolver", "sde-dpmsolver++"]:
|
| 271 |
+
if algorithm_type == "deis":
|
| 272 |
+
self.register_to_config(algorithm_type="dpmsolver++")
|
| 273 |
+
else:
|
| 274 |
+
raise NotImplementedError(f"{algorithm_type} is not implemented for {self.__class__}")
|
| 275 |
+
|
| 276 |
+
if solver_type not in ["midpoint", "heun"]:
|
| 277 |
+
if solver_type in ["logrho", "bh1", "bh2"]:
|
| 278 |
+
self.register_to_config(solver_type="midpoint")
|
| 279 |
+
else:
|
| 280 |
+
raise NotImplementedError(f"{solver_type} is not implemented for {self.__class__}")
|
| 281 |
+
|
| 282 |
+
if algorithm_type not in ["dpmsolver++", "sde-dpmsolver++"] and final_sigmas_type == "zero":
|
| 283 |
+
raise ValueError(
|
| 284 |
+
f"`final_sigmas_type` {final_sigmas_type} is not supported for `algorithm_type` {algorithm_type}. Please choose `sigma_min` instead."
|
| 285 |
+
)
|
| 286 |
+
|
| 287 |
+
# settable values
|
| 288 |
+
self.num_inference_steps = None
|
| 289 |
+
timesteps = np.linspace(0, num_train_timesteps - 1, num_train_timesteps, dtype=np.float32)[::-1].copy()
|
| 290 |
+
self.timesteps = torch.from_numpy(timesteps)
|
| 291 |
+
self.model_outputs = [None] * solver_order
|
| 292 |
+
self.lower_order_nums = 0
|
| 293 |
+
self._step_index = None
|
| 294 |
+
self._begin_index = None
|
| 295 |
+
self.sigmas = self.sigmas.to("cpu") # to avoid too much CPU/GPU communication
|
| 296 |
+
|
| 297 |
+
@property
|
| 298 |
+
def step_index(self):
|
| 299 |
+
"""
|
| 300 |
+
The index counter for current timestep. It will increase 1 after each scheduler step.
|
| 301 |
+
"""
|
| 302 |
+
return self._step_index
|
| 303 |
+
|
| 304 |
+
@property
|
| 305 |
+
def begin_index(self):
|
| 306 |
+
"""
|
| 307 |
+
The index for the first timestep. It should be set from pipeline with `set_begin_index` method.
|
| 308 |
+
"""
|
| 309 |
+
return self._begin_index
|
| 310 |
+
|
| 311 |
+
def set_begin_index(self, begin_index: int = 0):
|
| 312 |
+
"""
|
| 313 |
+
Sets the begin index for the scheduler. This function should be run from pipeline before the inference.
|
| 314 |
+
|
| 315 |
+
Args:
|
| 316 |
+
begin_index (`int`):
|
| 317 |
+
The begin index for the scheduler.
|
| 318 |
+
"""
|
| 319 |
+
self._begin_index = begin_index
|
| 320 |
+
|
| 321 |
+
def set_timesteps(
|
| 322 |
+
self,
|
| 323 |
+
num_inference_steps: int = None,
|
| 324 |
+
device: Union[str, torch.device] = None,
|
| 325 |
+
timesteps: Optional[List[int]] = None,
|
| 326 |
+
):
|
| 327 |
+
"""
|
| 328 |
+
Sets the discrete timesteps used for the diffusion chain (to be run before inference).
|
| 329 |
+
|
| 330 |
+
Args:
|
| 331 |
+
num_inference_steps (`int`):
|
| 332 |
+
The number of diffusion steps used when generating samples with a pre-trained model.
|
| 333 |
+
device (`str` or `torch.device`, *optional*):
|
| 334 |
+
The device to which the timesteps should be moved to. If `None`, the timesteps are not moved.
|
| 335 |
+
timesteps (`List[int]`, *optional*):
|
| 336 |
+
Custom timesteps used to support arbitrary timesteps schedule. If `None`, timesteps will be generated
|
| 337 |
+
based on the `timestep_spacing` attribute. If `timesteps` is passed, `num_inference_steps` and `sigmas`
|
| 338 |
+
must be `None`, and `timestep_spacing` attribute will be ignored.
|
| 339 |
+
"""
|
| 340 |
+
if num_inference_steps is None and timesteps is None:
|
| 341 |
+
raise ValueError("Must pass exactly one of `num_inference_steps` or `timesteps`.")
|
| 342 |
+
if num_inference_steps is not None and timesteps is not None:
|
| 343 |
+
raise ValueError("Can only pass one of `num_inference_steps` or `custom_timesteps`.")
|
| 344 |
+
if timesteps is not None and self.config.use_karras_sigmas:
|
| 345 |
+
raise ValueError("Cannot use `timesteps` with `config.use_karras_sigmas = True`")
|
| 346 |
+
if timesteps is not None and self.config.use_lu_lambdas:
|
| 347 |
+
raise ValueError("Cannot use `timesteps` with `config.use_lu_lambdas = True`")
|
| 348 |
+
|
| 349 |
+
if timesteps is not None:
|
| 350 |
+
timesteps = np.array(timesteps).astype(np.int64)
|
| 351 |
+
else:
|
| 352 |
+
# Clipping the minimum of all lambda(t) for numerical stability.
|
| 353 |
+
# This is critical for cosine (squaredcos_cap_v2) noise schedule.
|
| 354 |
+
clipped_idx = torch.searchsorted(torch.flip(self.lambda_t, [0]), self.config.lambda_min_clipped)
|
| 355 |
+
last_timestep = ((self.config.num_train_timesteps - clipped_idx).numpy()).item()
|
| 356 |
+
|
| 357 |
+
# "linspace", "leading", "trailing" corresponds to annotation of Table 2. of https://arxiv.org/abs/2305.08891
|
| 358 |
+
if self.config.timestep_spacing == "linspace":
|
| 359 |
+
timesteps = (
|
| 360 |
+
np.linspace(0, last_timestep - 1, num_inference_steps + 1)
|
| 361 |
+
.round()[::-1][:-1]
|
| 362 |
+
.copy()
|
| 363 |
+
.astype(np.int64)
|
| 364 |
+
)
|
| 365 |
+
elif self.config.timestep_spacing == "leading":
|
| 366 |
+
step_ratio = last_timestep // (num_inference_steps + 1)
|
| 367 |
+
# creates integer timesteps by multiplying by ratio
|
| 368 |
+
# casting to int to avoid issues when num_inference_step is power of 3
|
| 369 |
+
timesteps = (
|
| 370 |
+
(np.arange(0, num_inference_steps + 1) * step_ratio).round()[::-1][:-1].copy().astype(np.int64)
|
| 371 |
+
)
|
| 372 |
+
timesteps += self.config.steps_offset
|
| 373 |
+
elif self.config.timestep_spacing == "trailing":
|
| 374 |
+
step_ratio = self.config.num_train_timesteps / num_inference_steps
|
| 375 |
+
# creates integer timesteps by multiplying by ratio
|
| 376 |
+
# casting to int to avoid issues when num_inference_step is power of 3
|
| 377 |
+
timesteps = np.arange(last_timestep, 0, -step_ratio).round().copy().astype(np.int64)
|
| 378 |
+
timesteps -= 1
|
| 379 |
+
else:
|
| 380 |
+
raise ValueError(
|
| 381 |
+
f"{self.config.timestep_spacing} is not supported. Please make sure to choose one of 'linspace', 'leading' or 'trailing'."
|
| 382 |
+
)
|
| 383 |
+
|
| 384 |
+
sigmas = np.array(((1 - self.alphas_cumprod) / self.alphas_cumprod) ** 0.5)
|
| 385 |
+
log_sigmas = np.log(sigmas)
|
| 386 |
+
|
| 387 |
+
if self.config.use_karras_sigmas:
|
| 388 |
+
sigmas = np.flip(sigmas).copy()
|
| 389 |
+
sigmas = self._convert_to_karras(in_sigmas=sigmas, num_inference_steps=num_inference_steps)
|
| 390 |
+
timesteps = np.array([self._sigma_to_t(sigma, log_sigmas) for sigma in sigmas]).round()
|
| 391 |
+
elif self.config.use_lu_lambdas:
|
| 392 |
+
lambdas = np.flip(log_sigmas.copy())
|
| 393 |
+
lambdas = self._convert_to_lu(in_lambdas=lambdas, num_inference_steps=num_inference_steps)
|
| 394 |
+
sigmas = np.exp(lambdas)
|
| 395 |
+
timesteps = np.array([self._sigma_to_t(sigma, log_sigmas) for sigma in sigmas]).round()
|
| 396 |
+
else:
|
| 397 |
+
sigmas = np.interp(timesteps, np.arange(0, len(sigmas)), sigmas)
|
| 398 |
+
|
| 399 |
+
if self.config.final_sigmas_type == "sigma_min":
|
| 400 |
+
sigma_last = ((1 - self.alphas_cumprod[0]) / self.alphas_cumprod[0]) ** 0.5
|
| 401 |
+
elif self.config.final_sigmas_type == "zero":
|
| 402 |
+
sigma_last = 0
|
| 403 |
+
else:
|
| 404 |
+
raise ValueError(
|
| 405 |
+
f"`final_sigmas_type` must be one of 'zero', or 'sigma_min', but got {self.config.final_sigmas_type}"
|
| 406 |
+
)
|
| 407 |
+
|
| 408 |
+
sigmas = np.concatenate([sigmas, [sigma_last]]).astype(np.float32)
|
| 409 |
+
|
| 410 |
+
self.sigmas = torch.from_numpy(sigmas)
|
| 411 |
+
self.timesteps = torch.from_numpy(timesteps).to(device=device, dtype=torch.int64)
|
| 412 |
+
|
| 413 |
+
self.num_inference_steps = len(timesteps)
|
| 414 |
+
|
| 415 |
+
self.model_outputs = [
|
| 416 |
+
None,
|
| 417 |
+
] * self.config.solver_order
|
| 418 |
+
self.lower_order_nums = 0
|
| 419 |
+
|
| 420 |
+
# add an index counter for schedulers that allow duplicated timesteps
|
| 421 |
+
self._step_index = None
|
| 422 |
+
self._begin_index = None
|
| 423 |
+
self.sigmas = self.sigmas.to("cpu") # to avoid too much CPU/GPU communication
|
| 424 |
+
|
| 425 |
+
# Copied from diffusers.schedulers.scheduling_ddpm.DDPMScheduler._threshold_sample
|
| 426 |
+
def _threshold_sample(self, sample: torch.Tensor) -> torch.Tensor:
|
| 427 |
+
"""
|
| 428 |
+
"Dynamic thresholding: At each sampling step we set s to a certain percentile absolute pixel value in xt0 (the
|
| 429 |
+
prediction of x_0 at timestep t), and if s > 1, then we threshold xt0 to the range [-s, s] and then divide by
|
| 430 |
+
s. Dynamic thresholding pushes saturated pixels (those near -1 and 1) inwards, thereby actively preventing
|
| 431 |
+
pixels from saturation at each step. We find that dynamic thresholding results in significantly better
|
| 432 |
+
photorealism as well as better image-text alignment, especially when using very large guidance weights."
|
| 433 |
+
|
| 434 |
+
https://arxiv.org/abs/2205.11487
|
| 435 |
+
"""
|
| 436 |
+
dtype = sample.dtype
|
| 437 |
+
batch_size, channels, *remaining_dims = sample.shape
|
| 438 |
+
|
| 439 |
+
if dtype not in (torch.float32, torch.float64):
|
| 440 |
+
sample = sample.float() # upcast for quantile calculation, and clamp not implemented for cpu half
|
| 441 |
+
|
| 442 |
+
# Flatten sample for doing quantile calculation along each image
|
| 443 |
+
sample = sample.reshape(batch_size, channels * np.prod(remaining_dims))
|
| 444 |
+
|
| 445 |
+
abs_sample = sample.abs() # "a certain percentile absolute pixel value"
|
| 446 |
+
|
| 447 |
+
s = torch.quantile(abs_sample, self.config.dynamic_thresholding_ratio, dim=1)
|
| 448 |
+
s = torch.clamp(
|
| 449 |
+
s, min=1, max=self.config.sample_max_value
|
| 450 |
+
) # When clamped to min=1, equivalent to standard clipping to [-1, 1]
|
| 451 |
+
s = s.unsqueeze(1) # (batch_size, 1) because clamp will broadcast along dim=0
|
| 452 |
+
sample = torch.clamp(sample, -s, s) / s # "we threshold xt0 to the range [-s, s] and then divide by s"
|
| 453 |
+
|
| 454 |
+
sample = sample.reshape(batch_size, channels, *remaining_dims)
|
| 455 |
+
sample = sample.to(dtype)
|
| 456 |
+
|
| 457 |
+
return sample
|
| 458 |
+
|
| 459 |
+
# Copied from diffusers.schedulers.scheduling_euler_discrete.EulerDiscreteScheduler._sigma_to_t
|
| 460 |
+
def _sigma_to_t(self, sigma, log_sigmas):
|
| 461 |
+
# get log sigma
|
| 462 |
+
log_sigma = np.log(np.maximum(sigma, 1e-10))
|
| 463 |
+
|
| 464 |
+
# get distribution
|
| 465 |
+
dists = log_sigma - log_sigmas[:, np.newaxis]
|
| 466 |
+
|
| 467 |
+
# get sigmas range
|
| 468 |
+
low_idx = np.cumsum((dists >= 0), axis=0).argmax(axis=0).clip(max=log_sigmas.shape[0] - 2)
|
| 469 |
+
high_idx = low_idx + 1
|
| 470 |
+
|
| 471 |
+
low = log_sigmas[low_idx]
|
| 472 |
+
high = log_sigmas[high_idx]
|
| 473 |
+
|
| 474 |
+
# interpolate sigmas
|
| 475 |
+
w = (low - log_sigma) / (low - high)
|
| 476 |
+
w = np.clip(w, 0, 1)
|
| 477 |
+
|
| 478 |
+
# transform interpolation to time range
|
| 479 |
+
t = (1 - w) * low_idx + w * high_idx
|
| 480 |
+
t = t.reshape(sigma.shape)
|
| 481 |
+
return t
|
| 482 |
+
|
| 483 |
+
def _sigma_to_alpha_sigma_t(self, sigma):
|
| 484 |
+
alpha_t = 1 / ((sigma**2 + 1) ** 0.5)
|
| 485 |
+
sigma_t = sigma * alpha_t
|
| 486 |
+
|
| 487 |
+
return alpha_t, sigma_t
|
| 488 |
+
|
| 489 |
+
# Copied from diffusers.schedulers.scheduling_euler_discrete.EulerDiscreteScheduler._convert_to_karras
|
| 490 |
+
def _convert_to_karras(self, in_sigmas: torch.Tensor, num_inference_steps) -> torch.Tensor:
|
| 491 |
+
"""Constructs the noise schedule of Karras et al. (2022)."""
|
| 492 |
+
|
| 493 |
+
# Hack to make sure that other schedulers which copy this function don't break
|
| 494 |
+
# TODO: Add this logic to the other schedulers
|
| 495 |
+
if hasattr(self.config, "sigma_min"):
|
| 496 |
+
sigma_min = self.config.sigma_min
|
| 497 |
+
else:
|
| 498 |
+
sigma_min = None
|
| 499 |
+
|
| 500 |
+
if hasattr(self.config, "sigma_max"):
|
| 501 |
+
sigma_max = self.config.sigma_max
|
| 502 |
+
else:
|
| 503 |
+
sigma_max = None
|
| 504 |
+
|
| 505 |
+
sigma_min = sigma_min if sigma_min is not None else in_sigmas[-1].item()
|
| 506 |
+
sigma_max = sigma_max if sigma_max is not None else in_sigmas[0].item()
|
| 507 |
+
|
| 508 |
+
rho = 7.0 # 7.0 is the value used in the paper
|
| 509 |
+
ramp = np.linspace(0, 1, num_inference_steps)
|
| 510 |
+
min_inv_rho = sigma_min ** (1 / rho)
|
| 511 |
+
max_inv_rho = sigma_max ** (1 / rho)
|
| 512 |
+
sigmas = (max_inv_rho + ramp * (min_inv_rho - max_inv_rho)) ** rho
|
| 513 |
+
return sigmas
|
| 514 |
+
|
| 515 |
+
def _convert_to_lu(self, in_lambdas: torch.Tensor, num_inference_steps) -> torch.Tensor:
|
| 516 |
+
"""Constructs the noise schedule of Lu et al. (2022)."""
|
| 517 |
+
|
| 518 |
+
lambda_min: float = in_lambdas[-1].item()
|
| 519 |
+
lambda_max: float = in_lambdas[0].item()
|
| 520 |
+
|
| 521 |
+
rho = 1.0 # 1.0 is the value used in the paper
|
| 522 |
+
ramp = np.linspace(0, 1, num_inference_steps)
|
| 523 |
+
min_inv_rho = lambda_min ** (1 / rho)
|
| 524 |
+
max_inv_rho = lambda_max ** (1 / rho)
|
| 525 |
+
lambdas = (max_inv_rho + ramp * (min_inv_rho - max_inv_rho)) ** rho
|
| 526 |
+
return lambdas
|
| 527 |
+
|
| 528 |
+
def convert_model_output(
|
| 529 |
+
self,
|
| 530 |
+
model_output: torch.Tensor,
|
| 531 |
+
*args,
|
| 532 |
+
sample: torch.Tensor = None,
|
| 533 |
+
**kwargs,
|
| 534 |
+
) -> torch.Tensor:
|
| 535 |
+
"""
|
| 536 |
+
Convert the model output to the corresponding type the DPMSolver/DPMSolver++ algorithm needs. DPM-Solver is
|
| 537 |
+
designed to discretize an integral of the noise prediction model, and DPM-Solver++ is designed to discretize an
|
| 538 |
+
integral of the data prediction model.
|
| 539 |
+
|
| 540 |
+
<Tip>
|
| 541 |
+
|
| 542 |
+
The algorithm and model type are decoupled. You can use either DPMSolver or DPMSolver++ for both noise
|
| 543 |
+
prediction and data prediction models.
|
| 544 |
+
|
| 545 |
+
</Tip>
|
| 546 |
+
|
| 547 |
+
Args:
|
| 548 |
+
model_output (`torch.Tensor`):
|
| 549 |
+
The direct output from the learned diffusion model.
|
| 550 |
+
sample (`torch.Tensor`):
|
| 551 |
+
A current instance of a sample created by the diffusion process.
|
| 552 |
+
|
| 553 |
+
Returns:
|
| 554 |
+
`torch.Tensor`:
|
| 555 |
+
The converted model output.
|
| 556 |
+
"""
|
| 557 |
+
timestep = args[0] if len(args) > 0 else kwargs.pop("timestep", None)
|
| 558 |
+
if sample is None:
|
| 559 |
+
if len(args) > 1:
|
| 560 |
+
sample = args[1]
|
| 561 |
+
else:
|
| 562 |
+
raise ValueError("missing `sample` as a required keyword argument")
|
| 563 |
+
if timestep is not None:
|
| 564 |
+
deprecate(
|
| 565 |
+
"timesteps",
|
| 566 |
+
"1.0.0",
|
| 567 |
+
"Passing `timesteps` is deprecated and has no effect as model output conversion is now handled via an internal counter `self.step_index`",
|
| 568 |
+
)
|
| 569 |
+
|
| 570 |
+
# DPM-Solver++ needs to solve an integral of the data prediction model.
|
| 571 |
+
if self.config.algorithm_type in ["dpmsolver++", "sde-dpmsolver++"]:
|
| 572 |
+
if self.config.prediction_type == "epsilon":
|
| 573 |
+
# DPM-Solver and DPM-Solver++ only need the "mean" output.
|
| 574 |
+
if self.config.variance_type in ["learned", "learned_range"]:
|
| 575 |
+
model_output = model_output[:, :3]
|
| 576 |
+
sigma = self.sigmas[self.step_index]
|
| 577 |
+
alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma)
|
| 578 |
+
x0_pred = (sample - sigma_t * model_output) / alpha_t
|
| 579 |
+
elif self.config.prediction_type == "sample":
|
| 580 |
+
x0_pred = model_output
|
| 581 |
+
elif self.config.prediction_type == "v_prediction":
|
| 582 |
+
sigma = self.sigmas[self.step_index]
|
| 583 |
+
alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma)
|
| 584 |
+
x0_pred = alpha_t * sample - sigma_t * model_output
|
| 585 |
+
else:
|
| 586 |
+
raise ValueError(
|
| 587 |
+
f"prediction_type given as {self.config.prediction_type} must be one of `epsilon`, `sample`, or"
|
| 588 |
+
" `v_prediction` for the DPMSolverMultistepScheduler."
|
| 589 |
+
)
|
| 590 |
+
|
| 591 |
+
if self.config.thresholding:
|
| 592 |
+
x0_pred = self._threshold_sample(x0_pred)
|
| 593 |
+
|
| 594 |
+
return x0_pred
|
| 595 |
+
|
| 596 |
+
# DPM-Solver needs to solve an integral of the noise prediction model.
|
| 597 |
+
elif self.config.algorithm_type in ["dpmsolver", "sde-dpmsolver"]:
|
| 598 |
+
if self.config.prediction_type == "epsilon":
|
| 599 |
+
# DPM-Solver and DPM-Solver++ only need the "mean" output.
|
| 600 |
+
if self.config.variance_type in ["learned", "learned_range"]:
|
| 601 |
+
epsilon = model_output[:, :3]
|
| 602 |
+
else:
|
| 603 |
+
epsilon = model_output
|
| 604 |
+
elif self.config.prediction_type == "sample":
|
| 605 |
+
sigma = self.sigmas[self.step_index]
|
| 606 |
+
alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma)
|
| 607 |
+
epsilon = (sample - alpha_t * model_output) / sigma_t
|
| 608 |
+
elif self.config.prediction_type == "v_prediction":
|
| 609 |
+
sigma = self.sigmas[self.step_index]
|
| 610 |
+
alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma)
|
| 611 |
+
epsilon = alpha_t * model_output + sigma_t * sample
|
| 612 |
+
else:
|
| 613 |
+
raise ValueError(
|
| 614 |
+
f"prediction_type given as {self.config.prediction_type} must be one of `epsilon`, `sample`, or"
|
| 615 |
+
" `v_prediction` for the DPMSolverMultistepScheduler."
|
| 616 |
+
)
|
| 617 |
+
|
| 618 |
+
if self.config.thresholding:
|
| 619 |
+
sigma = self.sigmas[self.step_index]
|
| 620 |
+
alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma)
|
| 621 |
+
x0_pred = (sample - sigma_t * epsilon) / alpha_t
|
| 622 |
+
x0_pred = self._threshold_sample(x0_pred)
|
| 623 |
+
epsilon = (sample - alpha_t * x0_pred) / sigma_t
|
| 624 |
+
|
| 625 |
+
return epsilon
|
| 626 |
+
|
| 627 |
+
def dpm_solver_first_order_update(
|
| 628 |
+
self,
|
| 629 |
+
model_output: torch.Tensor,
|
| 630 |
+
*args,
|
| 631 |
+
sample: torch.Tensor = None,
|
| 632 |
+
noise: Optional[torch.Tensor] = None,
|
| 633 |
+
**kwargs,
|
| 634 |
+
) -> torch.Tensor:
|
| 635 |
+
"""
|
| 636 |
+
One step for the first-order DPMSolver (equivalent to DDIM).
|
| 637 |
+
|
| 638 |
+
Args:
|
| 639 |
+
model_output (`torch.Tensor`):
|
| 640 |
+
The direct output from the learned diffusion model.
|
| 641 |
+
sample (`torch.Tensor`):
|
| 642 |
+
A current instance of a sample created by the diffusion process.
|
| 643 |
+
|
| 644 |
+
Returns:
|
| 645 |
+
`torch.Tensor`:
|
| 646 |
+
The sample tensor at the previous timestep.
|
| 647 |
+
"""
|
| 648 |
+
timestep = args[0] if len(args) > 0 else kwargs.pop("timestep", None)
|
| 649 |
+
prev_timestep = args[1] if len(args) > 1 else kwargs.pop("prev_timestep", None)
|
| 650 |
+
if sample is None:
|
| 651 |
+
if len(args) > 2:
|
| 652 |
+
sample = args[2]
|
| 653 |
+
else:
|
| 654 |
+
raise ValueError(" missing `sample` as a required keyword argument")
|
| 655 |
+
if timestep is not None:
|
| 656 |
+
deprecate(
|
| 657 |
+
"timesteps",
|
| 658 |
+
"1.0.0",
|
| 659 |
+
"Passing `timesteps` is deprecated and has no effect as model output conversion is now handled via an internal counter `self.step_index`",
|
| 660 |
+
)
|
| 661 |
+
|
| 662 |
+
if prev_timestep is not None:
|
| 663 |
+
deprecate(
|
| 664 |
+
"prev_timestep",
|
| 665 |
+
"1.0.0",
|
| 666 |
+
"Passing `prev_timestep` is deprecated and has no effect as model output conversion is now handled via an internal counter `self.step_index`",
|
| 667 |
+
)
|
| 668 |
+
|
| 669 |
+
sigma_t, sigma_s = self.sigmas[self.step_index + 1], self.sigmas[self.step_index]
|
| 670 |
+
alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma_t)
|
| 671 |
+
alpha_s, sigma_s = self._sigma_to_alpha_sigma_t(sigma_s)
|
| 672 |
+
lambda_t = torch.log(alpha_t) - torch.log(sigma_t)
|
| 673 |
+
lambda_s = torch.log(alpha_s) - torch.log(sigma_s)
|
| 674 |
+
|
| 675 |
+
h = lambda_t - lambda_s
|
| 676 |
+
if self.config.algorithm_type == "dpmsolver++":
|
| 677 |
+
x_t = (sigma_t / sigma_s) * sample - (alpha_t * (torch.exp(-h) - 1.0)) * model_output
|
| 678 |
+
elif self.config.algorithm_type == "dpmsolver":
|
| 679 |
+
x_t = (alpha_t / alpha_s) * sample - (sigma_t * (torch.exp(h) - 1.0)) * model_output
|
| 680 |
+
elif self.config.algorithm_type == "sde-dpmsolver++":
|
| 681 |
+
assert noise is not None
|
| 682 |
+
x_t = (
|
| 683 |
+
(sigma_t / sigma_s * torch.exp(-h)) * sample
|
| 684 |
+
+ (alpha_t * (1 - torch.exp(-2.0 * h))) * model_output
|
| 685 |
+
+ sigma_t * torch.sqrt(1.0 - torch.exp(-2 * h)) * noise
|
| 686 |
+
)
|
| 687 |
+
elif self.config.algorithm_type == "sde-dpmsolver":
|
| 688 |
+
assert noise is not None
|
| 689 |
+
x_t = (
|
| 690 |
+
(alpha_t / alpha_s) * sample
|
| 691 |
+
- 2.0 * (sigma_t * (torch.exp(h) - 1.0)) * model_output
|
| 692 |
+
+ sigma_t * torch.sqrt(torch.exp(2 * h) - 1.0) * noise
|
| 693 |
+
)
|
| 694 |
+
return x_t
|
| 695 |
+
|
| 696 |
+
def multistep_dpm_solver_second_order_update(
|
| 697 |
+
self,
|
| 698 |
+
model_output_list: List[torch.Tensor],
|
| 699 |
+
*args,
|
| 700 |
+
sample: torch.Tensor = None,
|
| 701 |
+
noise: Optional[torch.Tensor] = None,
|
| 702 |
+
**kwargs,
|
| 703 |
+
) -> torch.Tensor:
|
| 704 |
+
"""
|
| 705 |
+
One step for the second-order multistep DPMSolver.
|
| 706 |
+
|
| 707 |
+
Args:
|
| 708 |
+
model_output_list (`List[torch.Tensor]`):
|
| 709 |
+
The direct outputs from learned diffusion model at current and latter timesteps.
|
| 710 |
+
sample (`torch.Tensor`):
|
| 711 |
+
A current instance of a sample created by the diffusion process.
|
| 712 |
+
|
| 713 |
+
Returns:
|
| 714 |
+
`torch.Tensor`:
|
| 715 |
+
The sample tensor at the previous timestep.
|
| 716 |
+
"""
|
| 717 |
+
timestep_list = args[0] if len(args) > 0 else kwargs.pop("timestep_list", None)
|
| 718 |
+
prev_timestep = args[1] if len(args) > 1 else kwargs.pop("prev_timestep", None)
|
| 719 |
+
if sample is None:
|
| 720 |
+
if len(args) > 2:
|
| 721 |
+
sample = args[2]
|
| 722 |
+
else:
|
| 723 |
+
raise ValueError(" missing `sample` as a required keyword argument")
|
| 724 |
+
if timestep_list is not None:
|
| 725 |
+
deprecate(
|
| 726 |
+
"timestep_list",
|
| 727 |
+
"1.0.0",
|
| 728 |
+
"Passing `timestep_list` is deprecated and has no effect as model output conversion is now handled via an internal counter `self.step_index`",
|
| 729 |
+
)
|
| 730 |
+
|
| 731 |
+
if prev_timestep is not None:
|
| 732 |
+
deprecate(
|
| 733 |
+
"prev_timestep",
|
| 734 |
+
"1.0.0",
|
| 735 |
+
"Passing `prev_timestep` is deprecated and has no effect as model output conversion is now handled via an internal counter `self.step_index`",
|
| 736 |
+
)
|
| 737 |
+
|
| 738 |
+
sigma_t, sigma_s0, sigma_s1 = (
|
| 739 |
+
self.sigmas[self.step_index + 1],
|
| 740 |
+
self.sigmas[self.step_index],
|
| 741 |
+
self.sigmas[self.step_index - 1],
|
| 742 |
+
)
|
| 743 |
+
|
| 744 |
+
alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma_t)
|
| 745 |
+
alpha_s0, sigma_s0 = self._sigma_to_alpha_sigma_t(sigma_s0)
|
| 746 |
+
alpha_s1, sigma_s1 = self._sigma_to_alpha_sigma_t(sigma_s1)
|
| 747 |
+
|
| 748 |
+
lambda_t = torch.log(alpha_t) - torch.log(sigma_t)
|
| 749 |
+
lambda_s0 = torch.log(alpha_s0) - torch.log(sigma_s0)
|
| 750 |
+
lambda_s1 = torch.log(alpha_s1) - torch.log(sigma_s1)
|
| 751 |
+
|
| 752 |
+
m0, m1 = model_output_list[-1], model_output_list[-2]
|
| 753 |
+
|
| 754 |
+
h, h_0 = lambda_t - lambda_s0, lambda_s0 - lambda_s1
|
| 755 |
+
r0 = h_0 / h
|
| 756 |
+
D0, D1 = m0, (1.0 / r0) * (m0 - m1)
|
| 757 |
+
if self.config.algorithm_type == "dpmsolver++":
|
| 758 |
+
# See https://arxiv.org/abs/2211.01095 for detailed derivations
|
| 759 |
+
if self.config.solver_type == "midpoint":
|
| 760 |
+
x_t = (
|
| 761 |
+
(sigma_t / sigma_s0) * sample
|
| 762 |
+
- (alpha_t * (torch.exp(-h) - 1.0)) * D0
|
| 763 |
+
- 0.5 * (alpha_t * (torch.exp(-h) - 1.0)) * D1
|
| 764 |
+
)
|
| 765 |
+
elif self.config.solver_type == "heun":
|
| 766 |
+
x_t = (
|
| 767 |
+
(sigma_t / sigma_s0) * sample
|
| 768 |
+
- (alpha_t * (torch.exp(-h) - 1.0)) * D0
|
| 769 |
+
+ (alpha_t * ((torch.exp(-h) - 1.0) / h + 1.0)) * D1
|
| 770 |
+
)
|
| 771 |
+
elif self.config.algorithm_type == "dpmsolver":
|
| 772 |
+
# See https://arxiv.org/abs/2206.00927 for detailed derivations
|
| 773 |
+
if self.config.solver_type == "midpoint":
|
| 774 |
+
x_t = (
|
| 775 |
+
(alpha_t / alpha_s0) * sample
|
| 776 |
+
- (sigma_t * (torch.exp(h) - 1.0)) * D0
|
| 777 |
+
- 0.5 * (sigma_t * (torch.exp(h) - 1.0)) * D1
|
| 778 |
+
)
|
| 779 |
+
elif self.config.solver_type == "heun":
|
| 780 |
+
x_t = (
|
| 781 |
+
(alpha_t / alpha_s0) * sample
|
| 782 |
+
- (sigma_t * (torch.exp(h) - 1.0)) * D0
|
| 783 |
+
- (sigma_t * ((torch.exp(h) - 1.0) / h - 1.0)) * D1
|
| 784 |
+
)
|
| 785 |
+
elif self.config.algorithm_type == "sde-dpmsolver++":
|
| 786 |
+
assert noise is not None
|
| 787 |
+
if self.config.solver_type == "midpoint":
|
| 788 |
+
x_t = (
|
| 789 |
+
(sigma_t / sigma_s0 * torch.exp(-h)) * sample
|
| 790 |
+
+ (alpha_t * (1 - torch.exp(-2.0 * h))) * D0
|
| 791 |
+
+ 0.5 * (alpha_t * (1 - torch.exp(-2.0 * h))) * D1
|
| 792 |
+
+ sigma_t * torch.sqrt(1.0 - torch.exp(-2 * h)) * noise
|
| 793 |
+
)
|
| 794 |
+
elif self.config.solver_type == "heun":
|
| 795 |
+
x_t = (
|
| 796 |
+
(sigma_t / sigma_s0 * torch.exp(-h)) * sample
|
| 797 |
+
+ (alpha_t * (1 - torch.exp(-2.0 * h))) * D0
|
| 798 |
+
+ (alpha_t * ((1.0 - torch.exp(-2.0 * h)) / (-2.0 * h) + 1.0)) * D1
|
| 799 |
+
+ sigma_t * torch.sqrt(1.0 - torch.exp(-2 * h)) * noise
|
| 800 |
+
)
|
| 801 |
+
elif self.config.algorithm_type == "sde-dpmsolver":
|
| 802 |
+
assert noise is not None
|
| 803 |
+
if self.config.solver_type == "midpoint":
|
| 804 |
+
x_t = (
|
| 805 |
+
(alpha_t / alpha_s0) * sample
|
| 806 |
+
- 2.0 * (sigma_t * (torch.exp(h) - 1.0)) * D0
|
| 807 |
+
- (sigma_t * (torch.exp(h) - 1.0)) * D1
|
| 808 |
+
+ sigma_t * torch.sqrt(torch.exp(2 * h) - 1.0) * noise
|
| 809 |
+
)
|
| 810 |
+
elif self.config.solver_type == "heun":
|
| 811 |
+
x_t = (
|
| 812 |
+
(alpha_t / alpha_s0) * sample
|
| 813 |
+
- 2.0 * (sigma_t * (torch.exp(h) - 1.0)) * D0
|
| 814 |
+
- 2.0 * (sigma_t * ((torch.exp(h) - 1.0) / h - 1.0)) * D1
|
| 815 |
+
+ sigma_t * torch.sqrt(torch.exp(2 * h) - 1.0) * noise
|
| 816 |
+
)
|
| 817 |
+
return x_t
|
| 818 |
+
|
| 819 |
+
def multistep_dpm_solver_third_order_update(
|
| 820 |
+
self,
|
| 821 |
+
model_output_list: List[torch.Tensor],
|
| 822 |
+
*args,
|
| 823 |
+
sample: torch.Tensor = None,
|
| 824 |
+
**kwargs,
|
| 825 |
+
) -> torch.Tensor:
|
| 826 |
+
"""
|
| 827 |
+
One step for the third-order multistep DPMSolver.
|
| 828 |
+
|
| 829 |
+
Args:
|
| 830 |
+
model_output_list (`List[torch.Tensor]`):
|
| 831 |
+
The direct outputs from learned diffusion model at current and latter timesteps.
|
| 832 |
+
sample (`torch.Tensor`):
|
| 833 |
+
A current instance of a sample created by diffusion process.
|
| 834 |
+
|
| 835 |
+
Returns:
|
| 836 |
+
`torch.Tensor`:
|
| 837 |
+
The sample tensor at the previous timestep.
|
| 838 |
+
"""
|
| 839 |
+
|
| 840 |
+
timestep_list = args[0] if len(args) > 0 else kwargs.pop("timestep_list", None)
|
| 841 |
+
prev_timestep = args[1] if len(args) > 1 else kwargs.pop("prev_timestep", None)
|
| 842 |
+
if sample is None:
|
| 843 |
+
if len(args) > 2:
|
| 844 |
+
sample = args[2]
|
| 845 |
+
else:
|
| 846 |
+
raise ValueError(" missing`sample` as a required keyword argument")
|
| 847 |
+
if timestep_list is not None:
|
| 848 |
+
deprecate(
|
| 849 |
+
"timestep_list",
|
| 850 |
+
"1.0.0",
|
| 851 |
+
"Passing `timestep_list` is deprecated and has no effect as model output conversion is now handled via an internal counter `self.step_index`",
|
| 852 |
+
)
|
| 853 |
+
|
| 854 |
+
if prev_timestep is not None:
|
| 855 |
+
deprecate(
|
| 856 |
+
"prev_timestep",
|
| 857 |
+
"1.0.0",
|
| 858 |
+
"Passing `prev_timestep` is deprecated and has no effect as model output conversion is now handled via an internal counter `self.step_index`",
|
| 859 |
+
)
|
| 860 |
+
|
| 861 |
+
sigma_t, sigma_s0, sigma_s1, sigma_s2 = (
|
| 862 |
+
self.sigmas[self.step_index + 1],
|
| 863 |
+
self.sigmas[self.step_index],
|
| 864 |
+
self.sigmas[self.step_index - 1],
|
| 865 |
+
self.sigmas[self.step_index - 2],
|
| 866 |
+
)
|
| 867 |
+
|
| 868 |
+
alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma_t)
|
| 869 |
+
alpha_s0, sigma_s0 = self._sigma_to_alpha_sigma_t(sigma_s0)
|
| 870 |
+
alpha_s1, sigma_s1 = self._sigma_to_alpha_sigma_t(sigma_s1)
|
| 871 |
+
alpha_s2, sigma_s2 = self._sigma_to_alpha_sigma_t(sigma_s2)
|
| 872 |
+
|
| 873 |
+
lambda_t = torch.log(alpha_t) - torch.log(sigma_t)
|
| 874 |
+
lambda_s0 = torch.log(alpha_s0) - torch.log(sigma_s0)
|
| 875 |
+
lambda_s1 = torch.log(alpha_s1) - torch.log(sigma_s1)
|
| 876 |
+
lambda_s2 = torch.log(alpha_s2) - torch.log(sigma_s2)
|
| 877 |
+
|
| 878 |
+
m0, m1, m2 = model_output_list[-1], model_output_list[-2], model_output_list[-3]
|
| 879 |
+
|
| 880 |
+
h, h_0, h_1 = lambda_t - lambda_s0, lambda_s0 - lambda_s1, lambda_s1 - lambda_s2
|
| 881 |
+
r0, r1 = h_0 / h, h_1 / h
|
| 882 |
+
D0 = m0
|
| 883 |
+
D1_0, D1_1 = (1.0 / r0) * (m0 - m1), (1.0 / r1) * (m1 - m2)
|
| 884 |
+
D1 = D1_0 + (r0 / (r0 + r1)) * (D1_0 - D1_1)
|
| 885 |
+
D2 = (1.0 / (r0 + r1)) * (D1_0 - D1_1)
|
| 886 |
+
if self.config.algorithm_type == "dpmsolver++":
|
| 887 |
+
# See https://arxiv.org/abs/2206.00927 for detailed derivations
|
| 888 |
+
x_t = (
|
| 889 |
+
(sigma_t / sigma_s0) * sample
|
| 890 |
+
- (alpha_t * (torch.exp(-h) - 1.0)) * D0
|
| 891 |
+
+ (alpha_t * ((torch.exp(-h) - 1.0) / h + 1.0)) * D1
|
| 892 |
+
- (alpha_t * ((torch.exp(-h) - 1.0 + h) / h**2 - 0.5)) * D2
|
| 893 |
+
)
|
| 894 |
+
elif self.config.algorithm_type == "dpmsolver":
|
| 895 |
+
# See https://arxiv.org/abs/2206.00927 for detailed derivations
|
| 896 |
+
x_t = (
|
| 897 |
+
(alpha_t / alpha_s0) * sample
|
| 898 |
+
- (sigma_t * (torch.exp(h) - 1.0)) * D0
|
| 899 |
+
- (sigma_t * ((torch.exp(h) - 1.0) / h - 1.0)) * D1
|
| 900 |
+
- (sigma_t * ((torch.exp(h) - 1.0 - h) / h**2 - 0.5)) * D2
|
| 901 |
+
)
|
| 902 |
+
return x_t
|
| 903 |
+
|
| 904 |
+
def index_for_timestep(self, timestep, schedule_timesteps=None):
|
| 905 |
+
if schedule_timesteps is None:
|
| 906 |
+
schedule_timesteps = self.timesteps
|
| 907 |
+
|
| 908 |
+
index_candidates = (schedule_timesteps == timestep).nonzero()
|
| 909 |
+
|
| 910 |
+
if len(index_candidates) == 0:
|
| 911 |
+
step_index = len(self.timesteps) - 1
|
| 912 |
+
# The sigma index that is taken for the **very** first `step`
|
| 913 |
+
# is always the second index (or the last index if there is only 1)
|
| 914 |
+
# This way we can ensure we don't accidentally skip a sigma in
|
| 915 |
+
# case we start in the middle of the denoising schedule (e.g. for image-to-image)
|
| 916 |
+
elif len(index_candidates) > 1:
|
| 917 |
+
step_index = index_candidates[1].item()
|
| 918 |
+
else:
|
| 919 |
+
step_index = index_candidates[0].item()
|
| 920 |
+
|
| 921 |
+
return step_index
|
| 922 |
+
|
| 923 |
+
def _init_step_index(self, timestep):
|
| 924 |
+
"""
|
| 925 |
+
Initialize the step_index counter for the scheduler.
|
| 926 |
+
"""
|
| 927 |
+
|
| 928 |
+
if self.begin_index is None:
|
| 929 |
+
if isinstance(timestep, torch.Tensor):
|
| 930 |
+
timestep = timestep.to(self.timesteps.device)
|
| 931 |
+
self._step_index = self.index_for_timestep(timestep)
|
| 932 |
+
else:
|
| 933 |
+
self._step_index = self._begin_index
|
| 934 |
+
|
| 935 |
+
def step(
|
| 936 |
+
self,
|
| 937 |
+
model_output: torch.Tensor,
|
| 938 |
+
timestep: int,
|
| 939 |
+
sample: torch.Tensor,
|
| 940 |
+
generator=None,
|
| 941 |
+
variance_noise: Optional[torch.Tensor] = None,
|
| 942 |
+
return_dict: bool = True,
|
| 943 |
+
) -> Union[SchedulerOutput, Tuple]:
|
| 944 |
+
"""
|
| 945 |
+
Predict the sample from the previous timestep by reversing the SDE. This function propagates the sample with
|
| 946 |
+
the multistep DPMSolver.
|
| 947 |
+
|
| 948 |
+
Args:
|
| 949 |
+
model_output (`torch.Tensor`):
|
| 950 |
+
The direct output from learned diffusion model.
|
| 951 |
+
timestep (`int`):
|
| 952 |
+
The current discrete timestep in the diffusion chain.
|
| 953 |
+
sample (`torch.Tensor`):
|
| 954 |
+
A current instance of a sample created by the diffusion process.
|
| 955 |
+
generator (`torch.Generator`, *optional*):
|
| 956 |
+
A random number generator.
|
| 957 |
+
variance_noise (`torch.Tensor`):
|
| 958 |
+
Alternative to generating noise with `generator` by directly providing the noise for the variance
|
| 959 |
+
itself. Useful for methods such as [`LEdits++`].
|
| 960 |
+
return_dict (`bool`):
|
| 961 |
+
Whether or not to return a [`~schedulers.scheduling_utils.SchedulerOutput`] or `tuple`.
|
| 962 |
+
|
| 963 |
+
Returns:
|
| 964 |
+
[`~schedulers.scheduling_utils.SchedulerOutput`] or `tuple`:
|
| 965 |
+
If return_dict is `True`, [`~schedulers.scheduling_utils.SchedulerOutput`] is returned; otherwise, a
|
| 966 |
+
tuple is returned where the first element is the sample tensor.
|
| 967 |
+
|
| 968 |
+
"""
|
| 969 |
+
if self.num_inference_steps is None:
|
| 970 |
+
raise ValueError(
|
| 971 |
+
"Number of inference steps is 'None', you need to run 'set_timesteps' after creating the scheduler"
|
| 972 |
+
)
|
| 973 |
+
|
| 974 |
+
if self.step_index is None:
|
| 975 |
+
self._init_step_index(timestep)
|
| 976 |
+
|
| 977 |
+
# Improve numerical stability for small number of steps
|
| 978 |
+
lower_order_final = (self.step_index == len(self.timesteps) - 1) and (
|
| 979 |
+
self.config.euler_at_final
|
| 980 |
+
or (self.config.lower_order_final and len(self.timesteps) < 15)
|
| 981 |
+
or self.config.final_sigmas_type == "zero"
|
| 982 |
+
)
|
| 983 |
+
lower_order_second = (
|
| 984 |
+
(self.step_index == len(self.timesteps) - 2) and self.config.lower_order_final and len(self.timesteps) < 15
|
| 985 |
+
)
|
| 986 |
+
|
| 987 |
+
model_output = self.convert_model_output(model_output, sample=sample)
|
| 988 |
+
for i in range(self.config.solver_order - 1):
|
| 989 |
+
self.model_outputs[i] = self.model_outputs[i + 1]
|
| 990 |
+
self.model_outputs[-1] = model_output
|
| 991 |
+
|
| 992 |
+
# Upcast to avoid precision issues when computing prev_sample
|
| 993 |
+
sample = sample.to(torch.float32)
|
| 994 |
+
if self.config.algorithm_type in ["sde-dpmsolver", "sde-dpmsolver++"] and variance_noise is None:
|
| 995 |
+
noise = randn_tensor(
|
| 996 |
+
model_output.shape, generator=generator, device=model_output.device, dtype=torch.float32
|
| 997 |
+
)
|
| 998 |
+
elif self.config.algorithm_type in ["sde-dpmsolver", "sde-dpmsolver++"]:
|
| 999 |
+
noise = variance_noise.to(device=model_output.device, dtype=torch.float32)
|
| 1000 |
+
else:
|
| 1001 |
+
noise = None
|
| 1002 |
+
|
| 1003 |
+
if self.config.solver_order == 1 or self.lower_order_nums < 1 or lower_order_final:
|
| 1004 |
+
prev_sample = self.dpm_solver_first_order_update(model_output, sample=sample, noise=noise)
|
| 1005 |
+
elif self.config.solver_order == 2 or self.lower_order_nums < 2 or lower_order_second:
|
| 1006 |
+
prev_sample = self.multistep_dpm_solver_second_order_update(self.model_outputs, sample=sample, noise=noise)
|
| 1007 |
+
else:
|
| 1008 |
+
prev_sample = self.multistep_dpm_solver_third_order_update(self.model_outputs, sample=sample)
|
| 1009 |
+
|
| 1010 |
+
if self.lower_order_nums < self.config.solver_order:
|
| 1011 |
+
self.lower_order_nums += 1
|
| 1012 |
+
|
| 1013 |
+
# Cast sample back to expected dtype
|
| 1014 |
+
prev_sample = prev_sample.to(model_output.dtype)
|
| 1015 |
+
|
| 1016 |
+
# upon completion increase step index by one
|
| 1017 |
+
self._step_index += 1
|
| 1018 |
+
|
| 1019 |
+
if not return_dict:
|
| 1020 |
+
return (prev_sample,)
|
| 1021 |
+
|
| 1022 |
+
return SchedulerOutput(prev_sample=prev_sample)
|
| 1023 |
+
|
| 1024 |
+
def add_noise(
|
| 1025 |
+
self,
|
| 1026 |
+
original_samples: torch.Tensor,
|
| 1027 |
+
noise: torch.Tensor,
|
| 1028 |
+
timesteps: torch.IntTensor,
|
| 1029 |
+
) -> torch.Tensor:
|
| 1030 |
+
# Make sure sigmas and timesteps have the same device and dtype as original_samples
|
| 1031 |
+
# alpha_t = self.alpha_t.to(device=original_samples.device, dtype=original_samples.dtype)
|
| 1032 |
+
# sigma_t = self.sigma_t.to(device=original_samples.device, dtype=original_samples.dtype)
|
| 1033 |
+
alpha_t = self.alpha_t.to(original_samples.device).to(original_samples.dtype)
|
| 1034 |
+
sigma_t = self.sigma_t.to(original_samples.device).to(original_samples.dtype)
|
| 1035 |
+
timesteps = timesteps.to(original_samples.device)
|
| 1036 |
+
alpha_t = alpha_t[timesteps].flatten()
|
| 1037 |
+
while len(alpha_t.shape) < len(original_samples.shape):
|
| 1038 |
+
alpha_t = alpha_t.unsqueeze(-1)
|
| 1039 |
+
|
| 1040 |
+
sigma_t = sigma_t[timesteps].flatten()
|
| 1041 |
+
while len(sigma_t.shape) < len(original_samples.shape):
|
| 1042 |
+
sigma_t = sigma_t.unsqueeze(-1)
|
| 1043 |
+
noisy_samples = alpha_t * original_samples + sigma_t * noise
|
| 1044 |
+
return noisy_samples
|
| 1045 |
+
|
| 1046 |
+
def get_velocity(self, original_samples: torch.Tensor, noise: torch.Tensor, timesteps: torch.IntTensor) -> torch.Tensor:
|
| 1047 |
+
# alpha_t = self.alpha_t.to(device=original_samples.device, dtype=original_samples.dtype)
|
| 1048 |
+
# sigma_t = self.sigma_t.to(device=original_samples.device, dtype=original_samples.dtype)
|
| 1049 |
+
alpha_t = self.alpha_t.to(original_samples.device).to(original_samples.dtype)
|
| 1050 |
+
sigma_t = self.sigma_t.to(original_samples.device).to(original_samples.dtype)
|
| 1051 |
+
|
| 1052 |
+
timesteps = timesteps.to(original_samples.device)
|
| 1053 |
+
alpha_t = alpha_t[timesteps].flatten()
|
| 1054 |
+
while len(alpha_t.shape) < len(original_samples.shape):
|
| 1055 |
+
alpha_t = alpha_t.unsqueeze(-1)
|
| 1056 |
+
|
| 1057 |
+
sigma_t = sigma_t[timesteps].flatten()
|
| 1058 |
+
while len(sigma_t.shape) < len(original_samples.shape):
|
| 1059 |
+
sigma_t = sigma_t.unsqueeze(-1)
|
| 1060 |
+
|
| 1061 |
+
velocity = alpha_t * noise - sigma_t * original_samples
|
| 1062 |
+
return velocity
|
| 1063 |
+
|
| 1064 |
+
def __len__(self):
|
| 1065 |
+
return self.config.num_train_timesteps
|
vibevoice/schedule/timestep_sampler.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
import torch
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class UniformSampler:
|
| 6 |
+
def __init__(self, timesteps = 1000):
|
| 7 |
+
self.timesteps = timesteps
|
| 8 |
+
def sample(self, batch_size, device):
|
| 9 |
+
return torch.randint(0, self.timesteps, (batch_size,), device=device)
|
| 10 |
+
|
| 11 |
+
class LogitNormalSampler:
|
| 12 |
+
def __init__(self, timesteps = 1000, m = 0, s = 1):
|
| 13 |
+
self.timesteps = timesteps
|
| 14 |
+
timesteps = torch.linspace(0, 1, timesteps)
|
| 15 |
+
logit = torch.log(timesteps / (1 - timesteps))
|
| 16 |
+
self.prob = torch.exp(-0.5 * (logit - m) ** 2 / s ** 2) / (s * math.sqrt(2 * math.pi))
|
| 17 |
+
def sample(self, batch_size, device):
|
| 18 |
+
return torch.multinomial(self.prob, batch_size, replacement=True).to(device)
|
| 19 |
+
|
vibevoice/scripts/__init__.py
ADDED
|
File without changes
|
why1.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:efb88a55718e58e28d7fd1767dde6c392269313ae5a7de5a11b229e9d2abe580
|
| 3 |
+
size 364844
|