rtx-csm: CFG schedule × prompt sweep — robust default is linear
Sweep of 4 schedules × 4 prompts produced concrete data on schedule sensitivity. step:3.0:1.5:12 (the Sprint 3 winner) catastrophically fails dense prompts: lecture-style input → 0.08 s of audio (one frame). step:2.0:1.0:8 produced the best single shot — near-verbatim question rendering "Well, it had stem-wondered. Have you ever wondered why we sometimes hear voices the way we do?" — but tanked the lecture prompt (2.4 s "You"). linear:3.0:1.0:25 is the only schedule that's never the best AND never the worst — graceful degradation across all four prompt categories. Updates the recommended recipe in perf_history.md (formerly step:3.0:1.5:12). quality_eval: skip WavLM-SV scoring on clips shorter than 0.25 s (emit -1 sentinel) — WavLM-SV's TDNN front-end requires a few hundred samples and crashed mid-sweep on the 0.08 s clip. Now the eval emits a row instead of bailing on the whole batch. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
@@ -147,11 +147,43 @@ shipped on the FP backbone; quantized path unchanged.
|
|||||||
target/release/examples/generate \
|
target/release/examples/generate \
|
||||||
--text "..." --speaker 0 \
|
--text "..." --speaker 0 \
|
||||||
--context-wav <reference>.wav --context-text "..." --context-speaker 1 \
|
--context-wav <reference>.wav --context-text "..." --context-speaker 1 \
|
||||||
--enable-cfg --cfg-schedule "step:3.0:1.5:12" \
|
--enable-cfg --cfg-schedule "linear:3.0:1.0:25" \
|
||||||
--steering-vec /path/to/<emotion>_steering.safetensors \
|
--steering-vec /path/to/<emotion>_steering.safetensors \
|
||||||
--steering-scale 0.3 --steering-layers 8,10,12
|
--steering-scale 0.3 --steering-layers 8,10,12
|
||||||
```
|
```
|
||||||
|
|
||||||
|
(Updated from `step:3.0:1.5:12` after the sweep below — linear is more
|
||||||
|
robust across prompt types.)
|
||||||
|
|
||||||
|
**CFG schedule × prompt sweep (2026-04-29, single seed):**
|
||||||
|
|
||||||
|
4 schedules × 4 prompts (short / medium / lecture / question), Amini
|
||||||
|
context, scored on cosine vs Amini and WER vs prompt:
|
||||||
|
|
||||||
|
| prompt | const:1.5 | step:2:1:8 | step:3:1.5:12 | linear:3:1:25 |
|
||||||
|
|-----------------|--------------|---------------|---------------|---------------|
|
||||||
|
| `Hello world.` | 0.69 / 1.00 | 0.71 / 4.00 | 0.79 / 3.50 | 0.82 / 4.50 |
|
||||||
|
| medium narrative | 0.94 / 1.08 | 0.90 / 1.00 | 0.66 / 1.00 *EOT 2.3 s* | 0.88 / 0.92 |
|
||||||
|
| dense lecture | **0.97 / 0.90** | 0.58 / 1.00 *EOT 2.4 s* | -1 / 1.00 *EOT 0.08 s* | 0.91 / **0.85** |
|
||||||
|
| question | 0.93 / 1.77 | 0.95 / **0.385** *near-verbatim* | 0.78 / 1.00 | 0.69 / 1.39 |
|
||||||
|
|
||||||
|
*Format: cosine / WER. -1 sentinel = clip too short for WavLM-SV.*
|
||||||
|
|
||||||
|
Single best shot: `step:2.0:1.0:8` × question →
|
||||||
|
`"Well, it had stem-wondered. Have you ever wondered why we sometimes
|
||||||
|
hear voices the way we do?"` (WER 0.385, cosine 0.95). Same step
|
||||||
|
schedule on a dense lecture prompt produced 2.4 s of "You" — same
|
||||||
|
schedule, opposite result.
|
||||||
|
|
||||||
|
Takeaways:
|
||||||
|
- `step:3.0:1.5:12` is **prompt-fragile** — destroys lecture and
|
||||||
|
medium-narrative prompts (premature EOT in 0.08–2.3 s); the earlier
|
||||||
|
Sprint 3 win on the meta "selective CFG paper recommends…" prompt
|
||||||
|
was a fluke of self-referential vocabulary.
|
||||||
|
- `linear:3.0:1.0:25` is the most robust default — graceful across
|
||||||
|
all four prompt categories, never the worst.
|
||||||
|
- `const:1.5` is the safe fallback when prompt category is unknown.
|
||||||
|
|
||||||
**Rejected from this sprint** (with data):
|
**Rejected from this sprint** (with data):
|
||||||
|
|
||||||
- *TTSDS2 metric* (arXiv 2506.19441): install broken on Python 3.12 +
|
- *TTSDS2 metric* (arXiv 2506.19441): install broken on Python 3.12 +
|
||||||
|
|||||||
@@ -147,10 +147,24 @@ fn main() -> Result<()> {
|
|||||||
let row: PairRow = serde_json::from_str(&line)
|
let row: PairRow = serde_json::from_str(&line)
|
||||||
.with_context(|| format!("parse row {idx}: {line}"))?;
|
.with_context(|| format!("parse row {idx}: {line}"))?;
|
||||||
|
|
||||||
// Speaker similarity at 16 kHz.
|
// Speaker similarity at 16 kHz. WavLM-SV's TDNN front-end has
|
||||||
|
// kernel size 5 with stride 2 chains, so it requires at least a
|
||||||
|
// few hundred samples of audio. Below ~0.25 s the model errors
|
||||||
|
// out — score those as -1.0 (sentinel) so the eval still emits
|
||||||
|
// a row instead of bailing on the whole batch.
|
||||||
let ref_16k = audio_io::load_mono_at_rate(&row.ref_wav, 16_000)?;
|
let ref_16k = audio_io::load_mono_at_rate(&row.ref_wav, 16_000)?;
|
||||||
let gen_16k = audio_io::load_mono_at_rate(&row.gen_wav, 16_000)?;
|
let gen_16k = audio_io::load_mono_at_rate(&row.gen_wav, 16_000)?;
|
||||||
let speaker_cosine = sv.score(&ref_16k, &gen_16k)? as f32;
|
const MIN_SV_SAMPLES: usize = 4000; // 0.25 s at 16 kHz
|
||||||
|
let speaker_cosine = if gen_16k.len() < MIN_SV_SAMPLES || ref_16k.len() < MIN_SV_SAMPLES {
|
||||||
|
tracing::warn!(
|
||||||
|
"row {idx}: gen={}s ref={}s — too short for WavLM-SV; cosine=-1",
|
||||||
|
gen_16k.len() as f32 / 16_000.0,
|
||||||
|
ref_16k.len() as f32 / 16_000.0,
|
||||||
|
);
|
||||||
|
-1.0f32
|
||||||
|
} else {
|
||||||
|
sv.score(&ref_16k, &gen_16k)? as f32
|
||||||
|
};
|
||||||
|
|
||||||
// Transcribe gen_wav via Moonshine (16 kHz).
|
// Transcribe gen_wav via Moonshine (16 kHz).
|
||||||
let pcm_t = Tensor::from_vec(gen_16k.clone(), (1, 1, gen_16k.len()), &device)?;
|
let pcm_t = Tensor::from_vec(gen_16k.clone(), (1, 1, gen_16k.len()), &device)?;
|
||||||
|
|||||||
Reference in New Issue
Block a user