Files
SoundGame/tools/stable-audio-3/optimized/tflite/README.md
T
SoundGame Dev f0f0a9b50e 初始提交:3 款纯音频游戏 + 共享引擎 + 完整文档与资产管线
- 共享引擎(app/src/main):GestureEngine 6 手势识别、AudioEngine 三层音频
  (ExoPlayer BGM 交叉淡入 / 对白 ducking / SoundPool 低延迟)、Game/GameEngine
  状态机接口、MainActivity 全屏封面 + TalkBack
- 《声命线》storyline:末世互动广播剧,3 幕 8 抉择点 4 结局
- 《声东击西》reflex:听声辨位反应,12 波战斗 + BGM 三档 BPM 变速
- 《暗室》escape:3x3 网格密室逃脱,钥匙→铁柜→保险丝→电箱→大门谜题链
- 文档:setup(从零搭建)/ architecture(架构)/ engine-api(引擎 API)/
  asset-pipeline(资产流水线)/ games/*(三款游戏设计文档)
- 资产管线:Qwen3-TTS 对白 + 程序合成音效/BGM + Mureka BGM 的 gen 脚本与台账,
  全部 ogg 资产入库可复现
- env.d 环境脚本:android/tts 入库;sfx/mureka 含密钥不入库,仅提交 .example.sh 模板
2026-08-29 13:00:34 +08:00

417 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# sa3_tflite — Stable Audio 3 on CPU via LiteRT / TFLite
Portable CPU inference for **Stable Audio 3** — the LiteRT/TFLite sibling of the
[MLX](../mlx) (Apple Silicon) and [TensorRT](../tensorRT) (NVIDIA GPU) releases.
No PyTorch, transformers, or stable-audio-tools at runtime — just `ai_edge_litert`
(LiteRT) driving fully self-contained `.tflite` graphs through the XNNPACK CPU
delegate. Runs anywhere LiteRT runs: **macOS / Linux / Windows, x86 / ARM**
(Windows is x64-only — see [Windows](#windows)).
## Quick Install
One line on a fresh machine — installs everything and plays back ~30 seconds of
"Impending tribal, epic orchestral buildup":
```bash
curl -LsSf https://raw.githubusercontent.com/Stability-AI/stable-audio-3/main/optimized/tflite/bootstrap.sh | bash
```
Windows — stock PowerShell (no bash/git needed; the curl|bash line above requires Git Bash or WSL on Windows):
```powershell
irm https://raw.githubusercontent.com/Stability-AI/stable-audio-3/main/optimized/tflite/bootstrap.ps1 | iex
```
Already cloned the repo? Run from inside `optimized/tflite/`:
```bash
# macOS / Linux / Windows Git Bash
./install.sh # one-time setup
./sa3 --prompt "Impending tribal, epic orchestral buildup" --play # generates + plays
```
```bat
:: Windows cmd / PowerShell
install.bat
sa3.bat --prompt "Impending tribal, epic orchestral buildup" --play
:: PowerShell users can use .\sa3.ps1 instead of sa3.bat — same flags, and
:: Ctrl-C exits cleanly (cmd's "Terminate batch job (Y/N)?" prompt is a .bat quirk)
```
## Three models, four modes
| `--dit` | model | best for |
|------------|--------------------|--------------------------------|
| `sm-music` | sa3-sm-music (50 M block) | fast music generation |
| `sm-sfx` | sa3-sm-sfx (50 M block) | sound effects |
| `medium` | sa3-medium-ARC (1.4 B) | higher-quality music, slower |
| mode | flags | example |
|------------------|-----------------------------------------------|----------------------------------|
| text-to-audio | `--prompt P` | new clip from a description |
| audio-to-audio | `--prompt P --init-audio IN.wav --init-noise-level σ` | variation of an existing clip |
| inpainting | `--prompt P --init-audio IN.wav --inpaint-range "S,E"` | regenerate one section, keep rest |
| CFG + negative | `--cfg 3.0 --negative-prompt P_NEG` | steer toward / away from prompts |
```
prompt ─▶ T5Gemma encoder ─▶ DiT pingpong sampler ─▶ SAME-S/L decoder ─▶ WAV
▲
optional: encoder + init audio (audio-to-audio / inpaint)
```
## Precision variants (`--precision`)
Same flag as the TensorRT CLI's `--precision`; the values use the wXaY naming
(weights/activations bit-widths). One flag switches the DiT, the codec decoder,
and (for audio-to-audio / inpainting) the encoder together; T5Gemma stays
single-precision. All
variants keep the full feature set: variable length (any `--seconds`, odd or even
latent counts) and variable batch (batched or sequential CFG). Missing files
lazy-download from HuggingFace on first use.
| `--precision` | legacy name | size (sm DiT / medium DiT / codecs) | quality | CPU speed |
|---------------|-------------|-------------------------------------|---------|-----------|
| `fp32` (default) | — | 1.8 GB / 5.8 GB / 0.2–1.8 GB | reference | 1× — on CPU this is *also* the fast choice |
| `w16a32` | fp16 | 0.9 / 2.9 / 0.1–0.9 GB | ≈lossless (fp16 weights, fp32 activations; 62–75 dB per-forward) | 1.5–3× slower, model-dependent (XNNPACK dequantizes per-matmul) |
| `w8a32` | woint8 | 0.45 / 1.5 / 0.05–0.5 GB | GPTQ int8 weights — codecs transparent (40–46 dB); DiT gives a *different but plausible* sample | ≈fp32 |
| `w8a8-dyn` | dynint8 | 0.45 / 1.5 / 0.05–0.5 GB | lowest (int8 weights + activations, per-invoke dynamic scales) | fastest (~1.2–1.3×) |
*(Names follow the wXaY convention — weight/activation bit-widths, as in LLM releases:
quantization here touches the FULLY_CONNECTED weights; activations stay fp32 except
`w8a8-dyn`, whose int8×int8 matmuls are what make it the only faster-than-fp32 variant.
All bit-widths are per-channel weight grids; "16" is fp16 — there is no int16 variant.
Speed factors measured on an Apple M4 Pro MacBook Pro, XNNPACK, 8 threads.)*
Two things worth knowing, both counter-intuitive:
- **Quantization buys *size*, not speed, on CPU.** Weight-only int8 dequantizes
to fp32 before each matmul, so they run at fp32 speed; fp16 is *slower* than fp32.
Only `dynint8` (quantized activations → true int8 matmuls) is faster.
- **DiT quantization error compounds.** The 8-step sampler is chaotically sensitive:
per-step weight error turns into a *different* (still plausible) sample rather than
a noisy version of the fp32 one. Judge DiT precisions by ear; decoder precisions
by PSNR (they run once on a fixed latent).
One CFG note: batched and sequential CFG are bit-identical for `fp32`/`w8a32` and
inaudibly different (~80 dB) for `w16a32`, but under `w8a8-dyn` the batch=2
invoke shares activation-quantization scales across the cond/uncond rows, so batched
CFG yields a *different plausible sample* than sequential. Pass `--no-cfg-batched`
with `w8a8-dyn` when you need run-to-run reproducibility against sequential baselines.
```bash
# quarter-size models, same speed and near-identical quality on the decoder side
./sa3 --prompt "lofi house loop" --dit sm-music --decoder same-s --precision w8a32
# fastest CPU inference (quality tradeoff)
./sa3 --prompt "lofi house loop" --dit sm-music --decoder same-s --precision w8a8-dyn
```
**Encoder variants** (used by audio-to-audio / inpainting) exist at the same
precisions; int8 weights are GPTQ-calibrated on real audio, like the DiT and codec
(measured latent PSNR vs fp32, same-s / same-l): `w16a32` 66 / 71 dB (≈lossless),
`w8a32` 36 / 46 dB, `w8a8-dyn` 24 / 30 dB. For the most quality-sensitive
inpainting you can still pin `--encoder-precision fp32`.
`--dit-precision` / `--decoder-precision` / `--encoder-precision` override the shared
flag per component — useful because they react to quantization very differently (the
codec's precision maps directly to audio fidelity; the DiT's changes *which* sample
you get; the encoder's affects a2a/inpaint latents):
```bash
# fastest DiT, reference-quality codec
./sa3 --prompt "lofi house loop" --dit sm-music --decoder same-s \
--precision fp32 --dit-precision w8a8-dyn
```
## Install
```bash
./install.sh
```
`install.sh` is uv-based. On a fresh machine it will:
1. Install [uv](https://github.com/astral-sh/uv) via the official curl
installer if it's missing (prompts y/N; `-y` skips the prompt).
2. Create a project-local `.venv/` with managed Python 3.11.
3. `uv pip install` the runtime deps into the venv (much faster than pip).
4. Ask which DiT bundles to download from HuggingFace
(`stabilityai/stable-audio-3-optimized`). Each pick pulls its matching
audio codec; T5Gemma (the shared text encoder) is downloaded once.
Already-present weights are skipped.
End-to-end on a fresh machine: **~10 seconds** + weight downloads.
> Don't want to pre-pick bundles? Skip install entirely and just run
> `./sa3 --prompt …` — any missing model file is downloaded from HF on
> first use and symlinked into `models/tflite/` from the HuggingFace cache.
Portable CPU (no GPU required). Python 3.9+. `./install.sh --python 3.12` to
pin a different Python.
### Windows
The stack runs natively on Windows x64 — no WSL needed. `ai-edge-litert`
ships `win_amd64` wheels for Python **3.10–3.13** (x64 only; no Windows-ARM
wheels), so install a Python in that range from
[python.org](https://www.python.org/downloads/) (check *"Add python.exe to
PATH"*), then from `optimized\tflite\`:
```bat
install.bat :: one-time setup (python -m venv + pip)
sa3.bat --prompt "lofi house loop" --dit sm-music --decoder same-s
```
`install.bat` / `sa3.bat` are the Windows twins of `./install.sh` / `./sa3`
(plain venv + pip; uv not required). Notes:
- **ffmpeg is optional** — only needed for mp3 / 24-bit / non-44.1 kHz
`--init-audio` inputs: `winget install ffmpeg` (or `choco install ffmpeg`).
- **Symlinks**: downloaded weights are exposed via symlinks where possible;
without Developer Mode, Windows disallows creating them, so the downloader
automatically falls back to a hardlink (zero-copy) or a plain copy. No
action needed either way.
- **Long paths**: the HuggingFace cache nests deeply — if downloads fail with
path-length errors, enable Windows long paths once (admin PowerShell):
`Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name LongPathsEnabled -Value 1`
(the `LongPathsEnabled` registry switch), then restart the terminal.
- `--play` uses the stdlib `winsound` player (macOS uses `afplay`).
## Run
`./sa3` is a thin shell wrapper around `.venv/bin/python scripts/sa3_tflite.py
"$@"` that prompts to run `./install.sh` if uv or `.venv/` isn't set up.
```bash
# Text-to-audio
./sa3 --prompt "lofi house loop" --dit sm-music --decoder same-s --out lofi.wav
# Sound effects
./sa3 --prompt "footsteps on gravel" --dit sm-sfx --decoder same-s --out steps.wav
# Higher-quality music (medium DiT, chunked SAME-L decode)
./sa3 --prompt "A beautiful piano arpeggio grows into a cinematic climax" \
--dit medium --decoder same-l --seconds 30 --out piano.wav
# Audio-to-audio variation (σmax 0.4-0.8 typical)
./sa3 --prompt "jazz fusion with electric piano" --dit sm-music --decoder same-s \
--init-audio funk.wav --init-noise-level 0.7 --out funk_jazz.wav
# Inpaint seconds 4-7
./sa3 --prompt "explosive drum break" --dit sm-music --decoder same-s \
--init-audio funk.wav --inpaint-range "4,7" --out funk_drums.wav
# CFG + negative prompt
./sa3 --prompt "ambient drone" --cfg 3.0 --negative-prompt "drums, vocals" \
--dit sm-music --decoder same-s --out drone.wav
# Generate + play immediately (afplay/winsound/aplay; Ctrl-C stops both)
./sa3 --prompt "rainforest" --dit sm-sfx --decoder same-s --play
# All options + categorised examples
./sa3 --help
```
Omit `--dit` / `--decoder` for an interactive arrow-key picker. Omit
`--prompt` for a stdin prompt. Relative `--out` paths land in `output/`
(auto-created); absolute paths are honoured as-is. The output path is
printed prominently as a `▸ saved` line at the end of each run.
Use `--threads` to control the XNNPACK CPU thread count (default 8).
## Web UI (gradio)
A browser UI over the same TFLite backend — every generation mode plus a
spectrogram player, a history panel, and playback options (auto-play, infinite
radio with pre-generation, loop, hotswap, MP3/WAV save).
```bash
./sa3-gradio # sm-music, opens a public share link
./sa3-gradio --dit medium # start on the medium DiT
./sa3-gradio --no-share # local-only (http://localhost:7860)
./sa3-gradio --port 7871 # pick the port
```
`./sa3-gradio` is a wrapper around `.venv/bin/python scripts/sa3_gradio.py`; on
first run it offers to install the UI-only extras (`gradio`, `pillow`,
`soundfile`) into `.venv`. In the browser:
- **Precision** dropdown next to the model picker — `fp32` (default) / `w16a32`
/ `w8a32` / `w8a8-dyn`, applied to the DiT + codec.
- **LoRA** panel — upload a `.safetensors` adapter or pick one from
`loras/<model>/` (e.g. `loras/sa3-medium/`), set its strength, stack several;
settings are remembered per model. The panel is disabled under a quantized
precision (see the LoRA note above) and has no per-step range (frozen graph).
- **Audio-to-audio / inpainting** — upload a reference clip; inpainting adds a
start/end range. Both are combinable.
Generated WAV/MP3s land in `output/gradio/`. First use of a model/precision
loads its weights (cached after).
### Without the wrapper
```bash
.venv/bin/python scripts/sa3_tflite.py --prompt "..." --dit medium --decoder same-l
# or, after `source .venv/bin/activate`:
python scripts/sa3_tflite.py --prompt "..." --dit medium --decoder same-l
```
## Speed & memory
This is a **CPU** path — it trades the GPU releases' speed for portability. The
small models comfortably beat realtime on a modern laptop CPU; `medium` is slower
(its DiT is ~5.8 GB fp32 and it chunk-decodes SAME-L). Use ≥ ~20 s clips: very
short clips have too few latent tokens for the sampler to settle into a coherent
loop. Throughput scales with `--threads` up to your physical core count (4–8 is
the usual sweet spot; more threads on a short model adds overhead).
For sub-realtime latency on a supported device, prefer the GPU siblings:
[MLX](../mlx) on Apple Silicon, [TensorRT](../tensorRT) on NVIDIA.
## Flag reference
| Flag | Default | Notes |
|-----------------------|----------|-----------------------------------------------------------------------|
| `--prompt` | (asks) | Text prompt; empty string = unconditional |
| `--negative-prompt` | — | CFG uncond branch; only used when `--cfg ≠ 1.0` |
| `--dit` | (asks) | `sm-music`, `sm-sfx`, or `medium` |
| `--decoder` | (asks) | `same-s` (pairs with sm-*) or `same-l` (pairs with medium) |
| `--seconds` | 30 | Output length (use ≥ ~20 s) |
| `--steps` | 8 | Pingpong sampler steps; 1 = single forward (fastest), 8 = sweet spot |
| `--seed` | random | Set for reproducibility; the chosen seed is printed at the end |
| `--cfg` | 1.0 | Guidance scale; 1.0 = off, >1 toward prompt, <1 toward uncond. ≠1 runs cond+uncond each step |
| `--apg` | 1.0 | Adaptive Projected Guidance; only matters when `--cfg ≠ 1` |
| `--cfg-batched` | on | When `--cfg ≠ 1`, run cond+uncond as one batch=2 invoke on the variable-batch DiT (~7–29% faster on Apple-Silicon AMX). `--no-cfg-batched` → sequential batch=1 dual-pass. Bit-identical at `fp32`/`w8a32` — see [Precision variants](#precision-variants---precision) for `w16a32`/`w8a8-dyn` |
| `--lora` | — | A `.safetensors` LoRA adapter (SA3-native/underfit or PEFT) merged into the DiT, optional `strength=S`; repeat to stack. Requires `--dit-precision fp32` or `w16a32`. See [LoRA](#lora) |
| `--lora-strength` | 1.0 | Default strength for `--lora` adapters without their own `strength=` |
| `--init-audio` | — | WAV (any format via ffmpeg) input for audio-to-audio / inpaint |
| `--init-noise-level` | 1.0 | σmax; 0.4–0.8 typical for variation, 1.0 = full regen, >1 = overshoot |
| `--inpaint-range` | — | `START,END` seconds; regenerate that span, keep the rest |
| `--threads` | 8 | XNNPACK CPU threads (all TFLite models run on CPU) |
| `--free-models` | on | Free each model after its last use; `--no-free-models` keeps them resident |
| `--out` / `-o` | (auto) | Relative → `output/<file>`; absolute → as-is. 16-bit PCM stereo @ 44.1 kHz, trimmed to exactly `--seconds` |
| `--play` | off | After writing, play the WAV: `afplay` (macOS) / `winsound` (Windows) / `aplay` (Linux); Ctrl-C stops both |
All `.tflite` models are **fp32** except T5Gemma, which is **fp16** (numerically
lossless there). There is no dtype knob: on CPU, int8/fp16 weights buy size, not
speed (XNNPACK dequantizes to fp32 to matmul), and int8 costs quality on the DiT
— so this release ships the fp32 graphs directly. (See "Notes on the design".)
## LoRA
Apply a LoRA finetune by merging it into the DiT's weight buffers at load —
same adapters and semantics as the MLX backend's `--lora`:
```bash
# one adapter
./sa3 --dit medium --decoder same-l --prompt "progressive metal" \
--lora plini-sa3-380.safetensors
# per-adapter strength; stack several
./sa3 --dit medium --decoder same-l --prompt "..." \
--lora a.safetensors strength=0.8 --lora b.safetensors strength=0.5
```
The adapter is a `.safetensors` file (SA3-native `train_lora.py` / underfit
output, or a PEFT adapter directory) — pickle `.ckpt/.pt` is refused. All nine
adapter types (lora, dora-rows/cols, bora, the four -xs) are supported; the base
must match `--dit`. The merge is written into a cached copy of the DiT under
`models/tflite/lora_cache/` (keyed by adapter + strength content hash) and reused
on repeat runs, so the ~5–15 s patch cost is paid once. A medium fp32 cache entry
is ~5.4 GB — delete `lora_cache/` to reclaim.
Requires `--dit-precision fp32` (default) or `w16a32`. **LoRA on the quantized
DiTs (`w8a32` / `w8a8-dyn` / `w4a32`) isn't figured out yet** — those store
weights as GPTQ-calibrated int8, so merging would mean dequantize → add the LoRA
delta → re-quantize, and a naive re-quant throws away the GPTQ error-feedback
grid (you'd get a model that's both LoRA-adapted *and* degraded). Doing it well
needs a GPTQ pass over the merged weights; until then `--lora` refuses quantized
precisions (the CLI errors, the web UI disables the LoRA panel). Use fp32/w16a32
for LoRA — on CPU fp32 is the fast-and-accurate choice anyway, so this is rarely
a real constraint. **Per-step gating (`steps=`) is also MLX-only** — a frozen
TFLite graph merges weights once at load; use `optimized/mlx` for step-gated LoRA.
## Files
```
sa3_tflite/
├── sa3 ← shell wrapper (use this)
├── install.sh ← uv bootstrap (run once)
├── sa3.bat / install.bat ← Windows twins of sa3 / install.sh
├── bootstrap.sh ← one-line curl installer
├── README.md
├── requirements.txt ← ai_edge_litert, numpy, sentencepiece, soundfile, huggingface_hub
├── output/ ← default landing zone for generated WAVs
├── scripts/
│ ├── sa3_tflite.py ← orchestrator CLI (invoked by ./sa3)
│ ├── weights.py ← weights manifest + HF auto-download
│ ├── examples.py ← shared examples block (--help + post-install)
│ └── install.py ← install.sh's Python half (bundle picker)
└── models/
├── tokenizer.model ← SentencePiece model, BUNDLED (~4 MB; T5Gemma tflite is encoder-only)
├── defs/
│ └── tflite_pipeline.py ← Tokenizer + T5Gemma front-end + pingpong schedule + sampler + WAV
└── tflite/ ← .tflite models (auto-downloaded; ~2.3 GB small, ~9.5 GB medium)
├── t5gemma/encoder_fp16.tflite 564 MB text encoder (fp16)
├── sa3-sm-music/dit_fp32.tflite 1.8 GB small music DiT (conditioner baked in)
├── sa3-sm-sfx/dit_fp32.tflite 1.8 GB small sfx DiT (conditioner baked in)
├── sa3-m/dit_fp32.tflite 5.8 GB medium DiT (conditioner baked in)
├── same-s/{enc,dec}_fp32.tflite ~220 MB each shared sm-* codec
└── same-l/{enc,dec}_fp32.tflite ~1.8 GB each medium codec
```
The DiT graphs are **baked-I/O**: the conditioner (prompt-padding + seconds
embedder) and the patch/unpatch are compiled into the graph, so the DiT takes the
raw T5Gemma output directly and the decoder emits audio directly. The two small
DiTs share the SAME-S codec (bit-exact between checkpoints), so only one set of
small-codec files is shipped.
## Auto-download from HuggingFace
Model files aren't bundled — they're pulled from
`stabilityai/stable-audio-3-optimized` (under `tflite/…`) on first use and
symlinked into `models/tflite/` from the HF cache. No duplication. Anonymous
downloads work but are rate-limited; `huggingface-cli login` with a free read-only
token lifts the cap. The SentencePiece tokenizer (`models/tokenizer.model`, ~4 MB)
is the one weight that IS committed, since the `.tflite` T5Gemma is encoder-only.
## Notes on the design
- **Baked-I/O varlen graphs.** Each `.tflite` is a single self-contained graph
with the conditioner and patch/unpatch in-graph, accepting a variable sequence
length — so one file serves any `--seconds`. The DiT is a 6-input graph
(`x, t, t5_hidden, t5_mask, seconds, local_add_cond`); feed raw T5 outputs and
the in-graph conditioner handles prompt-padding + seconds-embedding.
- **fp32 everywhere (except fp16 T5Gemma).** On CPU, quantizing buys size, not
speed — XNNPACK dequantizes int8/fp16 weights to fp32 to matmul, so fp16 is
actually *slower* and int8 gives no speedup. And the DiT will not go int8 at
quality: per-step error compounds over the 8 chaotic sampling steps into a
*different* (still plausible) sample, not a degraded one. So this release ships
the fp32 graphs directly. T5Gemma fp16 is the sole exception — it's numerically
lossless there and halves that file.
- **Monotonic audio-to-audio schedule.** The pingpong schedule applies the LogSNR
shift to the normalized `[1→0]` grid, then scales by σmax, so audio-to-audio
(σmax < 1) stays monotonically decreasing while keeping all N distilled steps.
σmax = 1.0 (text-to-audio) is bit-identical to the classic schedule.
- **SAME-L chunked decode.** The SAME-L decoder's dense sliding-window-attention
mask is O(T²), so long clips are decoded in overlap-8 windows of 64 latent
tokens (the throughput optimum) and stitched. SAME-S has a narrow receptive
field and decodes whole.
- **CFG (`--cfg ≠ 1`)** combines a cond and an uncond velocity in denoised space
(optional APG). The canonical DiT is **variable-batch**, so by default cond+uncond
run as **one batch=2 invoke per step** (`--cfg-batched`) — ~7–29% faster on
Apple-Silicon (the AMX matrix unit amortizes the weight loads across both rows;
measured on an M4 Pro MacBook Pro).
`--no-cfg-batched` falls back to a sequential batch=1 dual-pass (like the TensorRT
release, whose engine is static-batch=1); the two are bit-identical at `fp32`/`w8a32`
(~80 dB at `w16a32`; `w8a8-dyn` diverges by design — batch-shared activation scales).
## License & attribution
Model weights derived from Stability AI's Stable Audio 3 checkpoints.
T5Gemma text encoder from Google.
Use of the Stable Audio 3 weights is governed by the **Stability AI
Community License**. Please refer to the full terms at
<https://stability.ai/license>.