mamba-cot-tts / code /scripts /gen_fish_cot_samples.py
maxpicy's picture
Add files using upload-large-folder tool
6e802a4 verified
Raw History Blame Contribute Delete
71.7 kB
# gen_fish_cot_samples.py: The generator: text -> think block (self-written, ground truth, or none) -> Fish S2
# codes, with the plan window, plan injection, the silence guard and the loop breaker.
import argparse
import json
import os as _os_rtf
import time as _time_rtf
CODEC_FPS = float(_os_rtf.environ.get("MVC_CODEC_FPS", "21.53"))
import os
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import List, NamedTuple, Optional
import torch
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO))
sys.path.insert(0, str(REPO / "scripts"))
FISH_CB = [4096] + [1024] * 9
PLAN_STATUS_OK = "ok"
PLAN_STATUS_ABSENT = "absent"
PLAN_STATUS_EMPTY = "empty"
PLAN_STATUS_UNTERMINATED = "unterminated"
PLAN_STATUS_MALFORMED = "malformed"
PLAN_BIN_ORDER = ("pitch", "duration", "energy")
class PlanIdMaps(NamedTuple):
start: int
end: int
word: int
pitch: dict
duration: dict
energy: dict
def plan_id_maps(reg) -> PlanIdMaps:
if not getattr(reg, "plan_enabled", False):
raise SystemExit(
"the tokenizer in this process has no P²-CoT plan block — export "
"MVC_ENABLE_PLAN_TOKENS=1 before running (it must also match the "
"checkpoint: the gate shifts every speech-token id).")
return PlanIdMaps(
start=reg.plan_start_id, end=reg.plan_end_id, word=reg.plan_word_sep_id,
pitch={t: b for b, t in enumerate(reg.pitch_ids)},
duration={t: b for b, t in enumerate(reg.duration_ids)},
energy={t: b for b, t in enumerate(reg.energy_ids)},
)
@dataclass
class ParsedPlan:
words: List[dict] = field(default_factory=list)
status: str = PLAN_STATUS_ABSENT
reason: str = ""
@property
def ok(self) -> bool:
return self.status == PLAN_STATUS_OK
@property
def n_words(self) -> int:
return len(self.words)
def plan_region(ids, reg) -> List[int]:
ids = [int(t) for t in ids]
if reg.think_start_id not in ids:
return []
lo = len(ids) - 1 - ids[::-1].index(reg.think_start_id)
body = ids[lo + 1:]
if reg.think_end_id in body:
body = body[:body.index(reg.think_end_id)]
return body
def parse_plan_tokens(ids, maps: PlanIdMaps) -> ParsedPlan:
ids = [int(t) for t in ids]
if maps.start not in ids:
return ParsedPlan([], PLAN_STATUS_ABSENT, "no <PLAN> token in the block")
lo = ids.index(maps.start)
body = ids[lo + 1:]
if maps.start in body:
return ParsedPlan([], PLAN_STATUS_MALFORMED,
"more than one <PLAN> block in the think region")
terminated = maps.end in body
if terminated:
body = body[:body.index(maps.end)]
def _name(tid: int) -> str:
for fam, table in (("pitch", maps.pitch), ("duration", maps.duration),
("energy", maps.energy)):
if tid in table:
return f"{fam} bin {table[tid]}"
if tid == maps.word:
return "[PW]"
if tid == maps.end:
return "</PLAN>"
return "non-plan token"
words: List[dict] = []
i, n = 0, len(body)
while i < n:
if body[i] != maps.word:
return ParsedPlan(
words, PLAN_STATUS_MALFORMED,
f"word {len(words)}: expected [PW], got id {body[i]} "
f"({_name(body[i])}) at plan token {i}")
i += 1
pitch = None
if i < n and body[i] in maps.pitch:
pitch = maps.pitch[body[i]]
i += 1
if i >= n or body[i] not in maps.duration:
got = "end of block" if i >= n else f"id {body[i]} ({_name(body[i])})"
return ParsedPlan(words, PLAN_STATUS_MALFORMED,
f"word {len(words)}: expected [D:*], got {got}")
dur = maps.duration[body[i]]
i += 1
if i >= n or body[i] not in maps.energy:
got = "end of block" if i >= n else f"id {body[i]} ({_name(body[i])})"
return ParsedPlan(words, PLAN_STATUS_MALFORMED,
f"word {len(words)}: expected [E:*], got {got}")
words.append({"pitch_bin": pitch, "dur_bin": dur,
"energy_bin": maps.energy[body[i]]})
i += 1
if not terminated:
return ParsedPlan(
words, PLAN_STATUS_UNTERMINATED,
f"<PLAN> never closed ({len(words)} word(s) parsed before the block "
f"ended) — ΣD is truncated, so the window it implies is too short")
if not words:
return ParsedPlan([], PLAN_STATUS_EMPTY, "<PLAN></PLAN> carries no words")
return ParsedPlan(words, PLAN_STATUS_OK, "")
def plan_dur_frames(words) -> List[float]:
import plan_extract as pe
q = [{"pitch_bin": (pe.PITCH_UNVOICED_BIN if w["pitch_bin"] is None
else int(w["pitch_bin"])),
"dur_bin": int(w["dur_bin"]), "energy_bin": int(w["energy_bin"])}
for w in words]
return [float(x["dur_hat_frames"]) for x in pe.dequantise_plan(q)]
def plan_sum_frames(words) -> float:
return float(sum(plan_dur_frames(words)))
def plan_token_string(words) -> str:
import plan_extract as pe
q = [{"pitch_bin": (pe.PITCH_UNVOICED_BIN if w["pitch_bin"] is None
else int(w["pitch_bin"])),
"dur_bin": int(w["dur_bin"]), "energy_bin": int(w["energy_bin"])}
for w in words]
return pe.plan_to_tokens(q, wrap=True)
class DecodeWindow(NamedTuple):
min_fr: int
max_fr: int
source: str
sum_frames: Optional[float]
fallback: bool
t_hat: Optional[float] = None
def resolve_decode_window(parsed: Optional[ParsedPlan], base_min: int,
base_max: int, base_source: str, *,
dur_lo: float, dur_hi: float, use_plan: bool,
floor: int = 20,
speech_ratio: float = 1.0) -> DecodeWindow:
if not use_plan:
return DecodeWindow(base_min, base_max, base_source, None, False, None)
if parsed is None or not parsed.ok:
return DecodeWindow(base_min, base_max, base_source, None, True, None)
if not (speech_ratio > 0.0):
raise ValueError(f"speech_ratio must be > 0, got {speech_ratio!r}")
sigma_d = plan_sum_frames(parsed.words)
t_hat = sigma_d / speech_ratio
min_fr = max(floor, int(dur_lo * t_hat))
max_fr = max(min_fr + 5, int(dur_hi * t_hat))
return DecodeWindow(min_fr, max_fr, "plan", sigma_d, False, t_hat)
def resolve_plan_injection(on: bool, off: bool) -> bool:
if off and on:
print("[plan] --plan_injection_off: the injection channel is OFF for "
"this cell (prefix-only consumption, master plan R3).", flush=True)
return bool(on) and not bool(off)
def _row_window(row: dict, field: str, val_path: str, j: int) -> tuple:
w = row.get(field)
if w is None:
raise SystemExit(
f"{val_path} row {j} has no {field!r} field, but --plan_window_field "
f"{field} was requested. Every row of a flip/D-PLAN cell must carry "
f"the SAME window its control was decoded under (scripts/"
f"plan_flip_probe.py build writes `plan_window`); falling back "
f"row-by-row would mix two decode regimes in one cell.")
if not isinstance(w, (list, tuple)) or len(w) != 2:
raise SystemExit(f"{val_path} row {j}: {field!r} must be "
f"[min_frames, max_frames], got {w!r}")
try:
lo, hi = int(w[0]), int(w[1])
except (TypeError, ValueError):
raise SystemExit(f"{val_path} row {j}: {field!r}={w!r} is not a pair of "
f"integers") from None
if lo < 1 or hi <= lo:
raise SystemExit(f"{val_path} row {j}: {field!r}=[{lo}, {hi}] is not a "
f"usable window (need 1 <= min < max)")
return lo, hi
def _norm_plan_word(w, i: int, where: str) -> dict:
from config import DURATION_BINS, ENERGY_BINS, PITCH_BINS
if isinstance(w, (list, tuple)):
if len(w) != 3:
raise SystemExit(f"{where} word {i}: a compact word must be "
f"[{', '.join(PLAN_BIN_ORDER)}], got {list(w)}")
w = dict(zip(("pitch_bin", "dur_bin", "energy_bin"), w))
if not isinstance(w, dict):
raise SystemExit(f"{where} word {i}: expected an object, got "
f"{type(w).__name__}")
def _req(key, hi):
if w.get(key) is None:
raise SystemExit(f"{where} word {i}: missing {key!r} "
f"(keys={sorted(w)[:8]})")
v = int(w[key])
if not 0 <= v < hi:
raise SystemExit(f"{where} word {i}: {key}={v} out of range [0,{hi})")
return v
pb = w.get("pitch_bin")
if pb is None or int(pb) == PITCH_BINS:
pitch = None
else:
pitch = int(pb)
if not 0 <= pitch < PITCH_BINS:
raise SystemExit(f"{where} word {i}: pitch_bin={pitch} out of range "
f"[0,{PITCH_BINS}] ({PITCH_BINS} = unvoiced)")
return {"pitch_bin": pitch, "dur_bin": _req("dur_bin", DURATION_BINS),
"energy_bin": _req("energy_bin", ENERGY_BINS)}
def plan_words_from_obj(obj, *, where: str) -> List[dict]:
if isinstance(obj, str):
import plan_extract as pe
try:
raw = pe.tokens_to_plan(obj)
except Exception as e:
raise SystemExit(f"{where}: unparseable plan string ({e})")
elif isinstance(obj, dict):
if obj.get("ok") is False:
raise SystemExit(
f"{where}: this plan was REJECTED by the extractor "
f"(flags={obj.get('flags')}, reason={obj.get('reason')!r}). "
f"Fix or drop the row; do not condition on a plan the audio "
f"does not follow.")
raw = obj.get("words") if obj.get("words") is not None else obj.get("plan")
if raw is None:
raise SystemExit(f"{where}: object has neither 'words' nor 'plan' "
f"(keys={sorted(obj)[:8]})")
return plan_words_from_obj(raw, where=where)
else:
raw = obj
if not isinstance(raw, (list, tuple)) or not raw:
raise SystemExit(f"{where}: empty or non-list plan "
f"({type(raw).__name__})")
return [_norm_plan_word(w, i, where) for i, w in enumerate(raw)]
def load_forced_plans(path: str) -> dict:
rows, ordered = {}, []
with open(path, encoding="utf-8") as f:
for ln, line in enumerate(f, 1):
line = line.strip()
if not line:
continue
try:
d = json.loads(line)
except json.JSONDecodeError as e:
raise SystemExit(f"{path}:{ln} is not valid JSON: {e}")
if not isinstance(d, dict):
raise SystemExit(f"{path}:{ln} is not a JSON object")
if "row" in d and d["row"] is not None:
key = int(d["row"])
if key in rows:
raise SystemExit(f"{path}:{ln} repeats row {key} — the "
f"forced plan for a row must be unique")
rows[key] = d
ordered.append(d)
if not ordered:
raise SystemExit(f"{path} has no plan rows")
if rows and len(rows) != len(ordered):
raise SystemExit(f"{path} mixes rows with and without a 'row' key "
f"({len(rows)} of {len(ordered)}) — pick one")
return {"by_row": rows, "ordered": ordered}
def forced_plan_for(forced: dict, row_index: int, order_index: int, path: str):
if forced["by_row"]:
if row_index not in forced["by_row"]:
raise SystemExit(
f"--force_plan {path} has no entry for val row {row_index}. "
f"Every generated row must carry the plan it was supposed to "
f"be given, or the arm is a mixture of two conditions.")
return forced["by_row"][row_index]
if order_index >= len(forced["ordered"]):
raise SystemExit(
f"--force_plan {path} holds {len(forced['ordered'])} plans but the "
f"selection reached index {order_index}. Add a 'row' key to each "
f"line to bind plans to val row indices instead of file order.")
return forced["ordered"][order_index]
def plan_prefix_ids(reg, words) -> List[int]:
ids = [reg.plan_start_id]
for w in words:
ids.append(reg.plan_word_sep_id)
if w["pitch_bin"] is not None:
ids.append(reg.pitch_bin_to_id(w["pitch_bin"]))
ids.append(reg.duration_bin_to_id(w["dur_bin"]))
ids.append(reg.energy_bin_to_id(w["energy_bin"]))
ids.append(reg.plan_end_id)
return ids
def plan_bins_tensor(words, device=None):
from model import PLAN_BIN_ABSENT, PLAN_BIN_CHANNELS
if tuple(PLAN_BIN_CHANNELS) != PLAN_BIN_ORDER:
raise RuntimeError(
f"model.PLAN_BIN_CHANNELS is {PLAN_BIN_CHANNELS} but this script "
f"builds columns as {PLAN_BIN_ORDER} — a transposed plan tensor "
f"trains/decodes silently and looks like 'the plan does nothing'.")
rows = [[(PLAN_BIN_ABSENT if w["pitch_bin"] is None else int(w["pitch_bin"])),
int(w["dur_bin"]), int(w["energy_bin"])] for w in words]
return torch.tensor([rows], dtype=torch.long, device=device)
def _speaker_inject(model, speaker_id, device):
speaker_emb = inject = None
if speaker_id is not None and getattr(model, "use_speaker_conditioning", False):
sid = torch.tensor([speaker_id], dtype=torch.long, device=device)
speaker_emb = model.speaker_encoder(sid)
if getattr(model, "use_speaker_input", False):
inject = model.speaker_input_proj(speaker_emb)
return speaker_emb, inject
@torch.no_grad()
def think_budget(max_think, prompt):
if max_think and max_think > 0:
return int(max_think)
return max(120, 6 * len((prompt or "").split()) + 64)
def gen_reasoning(model, prefix, reg, max_think, temp, top_k, speaker_id, device):
import test_depth_checkpoint as tdc
NL = tdc.NUM_LEVELS
backbone = model.backbone.backbone
emb = backbone.embedding
speaker_emb, inject = _speaker_inject(model, speaker_id, device)
ids = prefix.clone().to(device)
codes = torch.full((1, ids.shape[1], NL), -1, dtype=torch.long, device=device)
think_end = reg.think_end_id
gen_toks = []
for _ in range(max_think):
if inject is not None:
emb.set_speaker(inject)
emb.set_codes(codes)
h = backbone(ids)
if speaker_emb is not None:
h = model.adaln(h, speaker_emb)
logits = model.backbone.lm_head(h[:, -1, :])[0].float()
for _bad in (reg.audio_start_id, reg.audio_end_id, reg.eos_id,
reg.bos_id, reg.pad_id, reg.user_prompt_id,
reg.think_start_id):
logits[_bad] = float("-inf")
logits[reg.speech_token_id_min:reg.speech_token_id_max + 1] = float("-inf")
if temp and temp > 0:
probs = torch.softmax(logits / temp, dim=-1)
if top_k and top_k > 0:
v, ix = torch.topk(probs, min(top_k, probs.numel()))
probs = torch.zeros_like(probs).scatter_(0, ix, v)
probs = probs / probs.sum()
tok = int(torch.multinomial(probs, 1).item())
else:
tok = int(logits.argmax().item())
gen_toks.append(tok)
col = torch.full((1, 1, NL), -1, dtype=torch.long, device=device)
ids = torch.cat([ids, torch.tensor([[tok]], device=device)], dim=1)
codes = torch.cat([codes, col], dim=1)
if tok == think_end:
break
if not gen_toks or gen_toks[-1] != think_end:
print(f" [WARN] reasoning hit max_think={max_think} without </THINK> "
f"-> degenerate prefix", flush=True)
ids = torch.cat([ids, torch.tensor([[think_end]], device=device)], dim=1)
reasoning_str = model.tokenizer.decode([t for t in gen_toks if t != think_end])
return ids, reasoning_str
def predict_frames(dm, row):
t = row.get("prompt") or ""
p = (row.get("pace") or "normal").strip().lower()
x = [1.0, float(len(t.split())), float(len(t)),
1.0 if p == "slow" else 0.0, 1.0 if p == "fast" else 0.0]
c = dm["coef"]
if len(c) != len(x):
raise ValueError(f"duration model has {len(c)} coefs, features give "
f"{len(x)} — regenerate it with fit_duration_model.py")
return max(20.0, sum(a * b for a, b in zip(c, x)))
def cot_text(row, cot_mode):
if cot_mode in ("none", "plan"):
return ""
tags = (f"EMO={row.get('emotion_label') or 'neutral'} "
f"PACE={row.get('pace') or 'normal'} "
f"PITCH={row.get('pitch') or 'normal'}")
if cot_mode in ("tags", "tags+plan"):
return tags
prose = (row.get("reasoning") or "").strip()
if cot_mode == "full":
return f"{prose} {tags}" if prose else tags
return prose
def f0_row(row, args):
if not (args.prompt_tags or args.force_emotion or args.force_pace
or args.force_pitch):
return row, None
r = dict(row)
if args.force_emotion:
r["emotion_label"] = args.force_emotion
if args.force_pace:
r["pace"] = args.force_pace
if args.force_pitch:
r["pitch"] = args.force_pitch
tag = cot_text(r, "tags")
if args.prompt_tags:
r["prompt"] = f"{row['prompt']} {tag}"
return r, tag
def build_prefix(mode, model, reg, row, max_think, think_temp, think_top_k,
speaker_id, device, cot_mode="prose", plan_ids=None):
tok = model.tokenizer
prompt_ids = tok.encode(row["prompt"], add_special_tokens=False)
base = [reg.bos_id, reg.user_prompt_id] + prompt_ids
if plan_ids and mode != "oracle":
raise ValueError(f"--force_plan / an oracle plan cannot be given to "
f"--mode {mode}: the plan would have nowhere to go "
f"(nocot has no <THINK> block) or would compete with "
f"the model's own program (self).")
if mode == "nocot":
ids = torch.tensor([base + [reg.audio_start_id]], dtype=torch.long)
return ids, None
if mode == "oracle":
rtext = cot_text(row, cot_mode)
r_ids = tok.encode(rtext, add_special_tokens=False)
ids = torch.tensor([base + [reg.think_start_id] + r_ids
+ list(plan_ids or [])
+ [reg.think_end_id, reg.audio_start_id]],
dtype=torch.long)
return ids, rtext
if mode == "self":
think_prefix = torch.tensor([base + [reg.think_start_id]], dtype=torch.long)
ids, rstr = gen_reasoning(model, think_prefix, reg, max_think,
think_temp, think_top_k, speaker_id, device)
ids = torch.cat(
[ids, torch.tensor([[reg.audio_start_id]], device=ids.device)], dim=1)
return ids, rstr
raise ValueError(mode)
def _teacher_forced_cache_check(model, reg, args):
import test_depth_checkpoint as tdc
backbone = model.backbone.backbone
emb = backbone.embedding
NL = tdc.NUM_LEVELS
dev = args.device
rows = [json.loads(l) for l in open(args.tf_check, encoding="utf-8") if l.strip()]
summary = []
for r in rows:
prefix, _ = build_prefix("nocot", model, reg, r, think_budget(args.max_think, r.get("prompt")), args.think_temp,
args.think_top_k, r.get("speaker_id"), dev)
prefix = prefix.to(dev)
levels = [r["speech_tokens"]] + r["residual_codes"]
T = len(levels[0])
frames = [[int(levels[k][t]) for k in range(NL)] for t in range(T)]
spk = r.get("speaker_id"); speaker_emb = None; inject = None
if spk is not None and getattr(model, "use_speaker_conditioning", False):
sid = torch.tensor([int(spk)], dtype=torch.long, device=dev)
speaker_emb = model.speaker_encoder(sid)
if getattr(model, "use_speaker_input", False):
inject = model.speaker_input_proj(speaker_emb)
placeholder = reg.audio_start_id
P = prefix.shape[1]
def ids_codes(t):
ids = torch.cat([prefix, torch.full((1, t), placeholder, dtype=torch.long, device=dev)], dim=1)
codes = torch.full((1, P + t, NL), -1, dtype=torch.long, device=dev)
for i in range(t):
codes[0, P + i] = torch.tensor(frames[i], device=dev)
return ids, codes
def head(h):
if speaker_emb is not None:
h = model.adaln(h, speaker_emb)
last = h[:, -1, :]
return last.float().clone(), model.backbone.lm_head(last).float()[0].clone()
last_re, logit_re, last_ca, logit_ca = [], [], [], []
with torch.no_grad():
for t in range(T):
ids, codes = ids_codes(t)
if inject is not None:
emb.set_speaker(inject)
emb.set_codes(codes)
a, b = head(backbone(ids)); last_re.append(a); logit_re.append(b)
ip = tdc._fresh_inference_cache(P + T + 8); n_fed = 0
for t in range(T):
ids, codes = ids_codes(t)
lo = n_fed
if inject is not None:
emb.set_speaker(inject)
emb.set_codes(codes[:, lo:])
ip.seqlen_offset = n_fed
h = backbone(ids[:, lo:], inference_params=ip)
n_fed = ids.shape[1]
a, b = head(h); last_ca.append(a); logit_ca.append(b)
fr_re = [model.depth_module.generate_frame(a) for a in last_re]
fr_ca = [model.depth_module.generate_frame(a) for a in last_ca]
rel = [float((a - b).norm() / (a.norm() + 1e-9)) for a, b in zip(last_re, last_ca)]
lm_mism = []
for t, (la, lb) in enumerate(zip(logit_re, logit_ca)):
if int(la.argmax()) != int(lb.argmax()):
top2 = torch.topk(la, 2).values
lm_mism.append({"t": t, "top2_gap": float(top2[0] - top2[1]),
"max_abs_logit_diff": float((la - lb).abs().max())})
fr_mism = [t for t in range(T) if list(fr_re[t]) != list(fr_ca[t])]
rec = {"row": r.get("row"), "frames": T, "max_rel_hidden": max(rel),
"mean_rel_hidden": sum(rel) / len(rel), "lmhead_argmax_mismatch": len(lm_mism),
"lmhead_mismatches": lm_mism[:10], "depth_frame_mismatch": len(fr_mism),
"depth_frame_mismatch_first": fr_mism[:10]}
summary.append(rec)
print(f"[tf_check] row {rec['row']}: frames={T} max_rel_hidden={rec['max_rel_hidden']:.2e} "
f"mean_rel={rec['mean_rel_hidden']:.2e} lmhead_argmax_mismatch={len(lm_mism)}/{T} "
f"depth_frame_mismatch={len(fr_mism)}/{T}"
+ (f" first_depth_mismatch_t={fr_mism[0]}" if fr_mism else ""), flush=True)
with open(args.out, "w", encoding="utf-8") as f:
json.dump(summary, f, indent=1)
print(f"[tf_check] wrote {args.out}", flush=True)
ARM_E_TEMPS = (0.2, [0.2, 0.2, 0.3, 0.9, 1.3, 1.6, 1.9, 2.2, 2.5])
H_TEMPS = (0.7, [0.80, 0.7, 0.7, 0.7, 0.7, 0.7, 0.7, 0.7, 0.7])
def resolve_decode_temps(args):
explicit = args.temp_semantic is not None and args.acoustic_temps is not None
h_family = (getattr(args, "rep_penalty_levels", None) is not None or (getattr(args, "ras_tau", 0.0) or 0.0) > 0
or getattr(args, "ras_tau_levels", None) is not None)
if h_family and not explicit and not getattr(args, "allow_default_temps", False):
raise SystemExit(
"decode temperatures not given: --rep_penalty_levels / --ras_tau belong to decode H, whose temperatures are "
"--temp_semantic 0.7 --acoustic_temps 0.80 0.7 0.7 0.7 0.7 0.7 0.7 0.7 0.7. Pass them explicitly (or "
"--allow_default_temps to reproduce a pre-2026-09-28 cold cell).")
if args.temp_semantic is None:
args.temp_semantic = ARM_E_TEMPS[0]
if args.acoustic_temps is None:
args.acoustic_temps = list(ARM_E_TEMPS[1])
args.temps_explicit = explicit
return args
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--mode", required=True, choices=["gt", "nocot", "oracle", "self"])
ap.add_argument("--cot_mode", default="prose",
choices=["prose", "tags", "none", "plan", "tags+plan", "full"],
help="MUST match the arm. Only affects --mode oracle, which "
"otherwise injects the prose into every arm. The last "
"three are the P²-CoT tiers (C1/C2/C3): they put a "
"<PLAN> program in the block, which needs a plan for "
"the row (sidecar `--plan_field`, or --force_plan).")
ap.add_argument("--checkpoint", help="required for nocot/oracle/self")
ap.add_argument("--val", default="data/s2_qwen_cot_fish_val.jsonl")
ap.add_argument("--out", required=True)
ap.add_argument("--rows", type=int, nargs="*", default=None,
help="explicit 0-based val row indices; default = --limit head")
ap.add_argument("--limit", type=int, default=10)
ap.add_argument("--max_words", type=int, default=0,
help="skip prompts longer than this. Decode cost is ~T^2 "
"(full-sequence forward per frame) and min_frames is "
"6*words, so long prompts are quadratically expensive; "
"0 = no filter.")
ap.add_argument("--model_name", default="state-spaces/mamba2-1.3b")
ap.add_argument("--rep_penalty", type=float, default=1.0,
help="windowed repetition penalty per codebook (1.0 = off). "
"Every reference AR-TTS uses one: fish-speech 1.1-1.2 "
"over 16 frames, CosyVoice-2 RAS over 10, IndexTTS 10.0. "
"The fan IS token repetition; this is the mechanism "
"that suppresses it WITHOUT cooling the temperature.")
ap.add_argument("--rep_penalty_levels", type=float, nargs=10, default=None,
help="PER-LEVEL penalties (10 values, cb0..cb9), overriding "
"--rep_penalty. The residual fan concentrates in cb1 "
"(flip2 1.58 vs GT 0.85) while cb4-9 already sit near "
"GT, so a global rise would tax levels that are fine.")
ap.add_argument("--ras_tau", type=float, default=0.0,
help="Repetition Aware Sampling threshold (VALL-E 2 Alg.1; "
"0 = off). After nucleus sampling, if the drawn token "
"already occupies >= tau of this level's last "
"--ras_win frames, REDRAW from the full untruncated "
"distribution. CosyVoice-2 ships tau=0.1 over win 10.")
ap.add_argument("--ras_tau_levels", type=float, nargs=10, default=None,
help="per-level RAS thresholds (cb0..cb9), overriding "
"--ras_tau — our fan is localized to cb1.")
ap.add_argument("--ras_win", type=int, default=10)
ap.add_argument("--rep_win", type=int, default=16,
help="repetition-penalty window in frames (fish uses 16).")
ap.add_argument("--loop_break_flipk", type=float, default=0.0,
help="cb0 LOOP-BREAKER trigger: trailing-window flipK%% "
"(period-2..8 cycle rate) at/above which the kick "
"engages. 0 = off (default; every frozen battery DEC "
"is unchanged). 5.0 is the ear-validated threshold "
"(2026-08-23 map: warble clips 5-29, clean/GT <2). "
"The loop is the tremolo+early-cutoff mechanism: "
"rep_penalty 1.2 and RAS cannot break it at "
"temp_semantic 0.2 (peaked distribution).")
ap.add_argument("--loop_break_win", type=int, default=32,
help="trailing cb0 window (frames) for the loop metric.")
ap.add_argument("--loop_break_temp", type=float, default=0.6,
help="cb0 temp while inside a detected loop (kick).")
ap.add_argument("--loop_break_rep", type=float, default=2.0,
help="cb0 rep_penalty while inside a detected loop.")
ap.add_argument("--loop_break_stop_after", type=int, default=0,
help="force a clean stop after this many CONSECUTIVE "
"kicked frames past min_frames (0 = never): a loop "
"that survives the kick ends at a frame boundary "
"instead of vamping to the window cap.")
ap.add_argument("--silence_guard_codes", default="",
help="SILENCE GUARD (ledger S200): path to a JSON list of "
"cb0 PAUSE codes (data/silence_codes_fish_s2.json). "
"Empty = off (default; every frozen DEC unchanged). The "
"'cuts off / fails to speak' clips are the semantic "
"stream collapsing into these codes for 30-240 frames "
"where real speech never exceeds 9.")
ap.add_argument("--silence_guard_budget", type=int, default=10,
help="mid-utterance: after this many CONSECUTIVE pause-set "
"frames, mask the set on the next frame so the head "
"must re-engage with the injected word. 10 > GT max 9.")
ap.add_argument("--silence_guard_stop_after", type=int, default=10,
help="at the plan's LAST word: a pause run this long is the "
"end of the utterance -> clean stop (0 = never).")
ap.add_argument("--silence_guard_stop_noplan", action="store_true",
help="also apply the stop rule when there is NO plan "
"schedule (base / noplan cells), VAD-style.")
ap.add_argument("--silence_guard_retries", type=int, default=0,
help="v2 REWIND (S201): on a spent mid-utterance budget, "
"discard the whole pause run and re-sample it from the "
"last speech frame with the pause set masked for "
"`budget` frames and cb0 at --silence_guard_retry_temp; "
"up to this many times per row. 0 = v1 (single-frame "
"mask), bit-identical to before.")
ap.add_argument("--silence_guard_retry_temp", type=float, default=0.6,
help="cb0 temperature while re-sampling a rewound segment.")
ap.add_argument("--onset_frames", type=int, default=0,
help="damp the ACOUSTIC temperatures for this many frames at "
"audio start. flip2 peaks in the first 20%% of every "
"generated utterance (5.4-8.2%% vs GT 1.26%%) because "
"there is no acoustic history yet. 0 = off.")
ap.add_argument("--onset_temp_scale", type=float, default=1.0,
help="multiplier applied to acoustic temps during the onset "
"window (0.0 = argmax, which also tests whether the "
"onset fan is a SAMPLING artifact or the model itself).")
ap.add_argument("--hybrid_attention_top_k", type=int, default=0,
help="rebuild the top-K attention graft before loading. MUST "
"match the checkpoint: a hybrid ckpt loaded into a pure "
"model has shape-mismatched attention projections, and "
"those layers would fall back to PRETRAINED Mamba weights "
"while the rest are trained (now a hard error).")
ap.add_argument("--backbone", choices=["mamba", "transformer"], default="mamba",
help="must match the checkpoint. 'transformer' also requires "
"--model_name EleutherAI/pythia-1.4b. Mismatching these "
"now raises in load_checkpoint rather than degrading to "
"base text weights, but set them correctly regardless.")
ap.add_argument("--num_speakers", type=int, default=8192)
ap.add_argument("--speaker_dim", type=int, default=256)
ap.add_argument("--depth_dim", type=int, default=1024)
ap.add_argument("--depth_layers", type=int, default=2)
ap.add_argument("--depth_arch", choices=["gru", "mamba2", "transformer"], default="gru",
help="MUST match the checkpoint's depth head.")
ap.add_argument("--depth_heads", type=int, default=8)
ap.add_argument("--depth_feedback", choices=["all", "semantic"],
default="semantic",
help="MUST match training. fish-recipe arms use 'all' (MCF).")
ap.add_argument("--decode_feedback", choices=["all", "semantic", "argmax"],
default=None,
help="what the TEMPORAL STREAM is fed at decode; defaults "
"to --depth_feedback (i.e. match training). 'argmax' "
"is decode-time STATE DECOUPLING: emit the sampled "
"frame but feed the MODE back, so the recurrence sees "
"near-manifold inputs instead of codes drawn at temps "
"up to 2.5 while training was teacher-forced on "
"ground truth (the S11 mechanism, inference side). "
"Separate from --depth_feedback because that one also "
"configures the model's teacher-forced masking and "
"must keep matching the checkpoint.")
ap.add_argument("--depth_cond", choices=["add", "prefix"], default="add",
help="MUST match training. fish-recipe arms use 'prefix'.")
ap.add_argument("--device", default="cuda")
ap.add_argument("--dtype", choices=["fp32", "bf16"], default="fp32")
ap.add_argument("--candidates", type=int, default=1,
help="best-of-N: emit N sampled candidates per row (rank "
"them by WER downstream). Decode is stochastic, and "
"each candidate reseeds so draws are independent.")
ap.add_argument("--seed", type=int, default=1234)
ap.add_argument("--force_speaker_id", type=int, default=None,
help="override the row's speaker_id. Needed to test the "
"STAGE-1 base fairly: ids 4200-4204 are Stage-2-only "
"(Stage-1 trained 0-3955), so scoring the base on them "
"would confound 'untrained embedding' with 'collapse'.")
ap.add_argument("--max_frames", type=int, default=400)
ap.add_argument("--tf_check", default="",
help="DIAGNOSTIC: a generated JSONL whose rows are teacher-forced through both the "
"recompute and the cached backbone paths; per-frame hidden-state and logit "
"agreement is written to --out and the run exits (pbs/cached_identity.pbs phase 2).")
ap.add_argument("--cached", action="store_true",
help="Cached-state decode (2026-09-11): carry Mamba2 conv/ssm states, the hybrid "
"KV cache and the transformer past_key_values across frames and feed one "
"position per step, instead of re-running the whole prefix every frame. "
"Numerics-equivalent to the default path, NOT bit-identical (S212: hidden <=3e-3 rel, "
"LM-head argmax 360/360; token-identical on mamba2-130m in tests); a battery adopts it "
"for ALL compared arms or none (prereg Amendment 7, pbs/p2cot_eval.pbs CACHED=1). "
"Off by default: every banked result used the recompute path.")
ap.add_argument("--force_frames", type=int, default=0,
help="EFFICIENCY SWEEP ONLY (external_benchmarks.md §3.4): pin the decode to exactly N "
"frames (min_frames = max_frames = N), overriding every derived window, so "
"rtf_lm / peak_mem_gb are measured at a chosen generated length. Use with "
"--mode nocot; a plan schedule would be desynchronised from a forced length.")
ap.add_argument("--min_frames_per_word", type=float, default=6.0)
ap.add_argument("--duration_model", default=None,
help="JSON from scripts/fit_duration_model.py. When set, "
"min/max frames come from the predicted duration "
"instead of the flat 6-frames-per-word floor and the "
"fixed --max_frames cap.")
ap.add_argument("--dur_lo", type=float, default=0.6,
help="min_frames = dur_lo * predicted. Below 1.0 so the "
"stop head can still end a row the predictor "
"overestimated.")
ap.add_argument("--dur_hi", type=float, default=1.5,
help="hard cap = dur_hi * predicted. This is what removes "
"the runaway; 1.5 leaves room for the ~30%% of rows "
"the predictor misses by more than 20%%.")
ap.add_argument("--temp_semantic", type=float, default=None)
ap.add_argument("--acoustic_temps", type=float, nargs="*", default=None)
ap.add_argument("--allow_default_temps", action="store_true",
help="decode on the arm-E ladder even though H-family controls (--rep_penalty_levels / --ras_tau) "
"are set, i.e. reproduce a cell decoded before 2026-09-28. Never for new cells.")
ap.add_argument("--top_k", type=int, default=64)
ap.add_argument("--top_p", type=float, default=0.0,
help="nucleus sampling (0 = off). Reference AR-TTS systems "
"all use it: fish-speech 0.7-0.8, CosyVoice-2 0.8, "
"IndexTTS 0.8. generate_frame already implements it; "
"only the CLI flag was missing.")
ap.add_argument("--stop_threshold", type=float, default=0.95)
ap.add_argument("--dump_stop_probs", action="store_true",
help="record p(stop) for EVERY frame into the output row as "
"`stop_probs` (+ `stop_argmax`). Combined with a "
"threshold that never fires (--stop_threshold 1.1) "
"this makes one run yield the exact clip length for "
"every (threshold, dur_lo) pair, because the stop "
"check is a pure break that consumes no randomness. "
"Consumed by scripts/stop_threshold_sweep.py.")
ap.add_argument("--antifan_bias", type=float, default=0.0)
ap.add_argument("--antifan_levels", type=int, nargs="*", default=None)
ap.add_argument("--scale_cap", type=float, default=0.0,
help="UNBOUNDED decode with the frame cap scaled to the "
"predicted duration (e.g. 3.0 = 3x). A flat 400 cap "
"means ~5x on a 14-word line and ~1.5x on a 35-word "
"one, so 'at-cap' is not comparable across prompt "
"lengths (S141). Needs --duration_model_path.")
ap.add_argument("--duration_model_path", default=None,
help="predictor for --scale_cap ONLY; does not bound")
ap.add_argument("--prompt_tags", action="store_true",
help="append the EMO/PACE/PITCH tag TEXT to the prompt "
"region. For probing a base model that has never seen "
"a <THINK> block; the record keeps the original prompt "
"so WER still catches the model speaking the tag.")
ap.add_argument("--force_emotion", default=None,
help="override emotion_label on every row (tag-flip probe)")
ap.add_argument("--force_pace", default=None,
help="override pace on every row (tag-flip probe)")
ap.add_argument("--force_pitch", default=None,
help="override pitch on every row (tag-flip probe)")
ap.add_argument("--think_temp", type=float, default=0.7)
ap.add_argument("--think_top_k", type=int, default=50)
ap.add_argument("--max_think", type=int, default=120,
help="<THINK> token budget per row; 0 = AUTO (6 x words + 64, floor 120). The fixed "
"default caps a self-authored plan at ~28 words - use 0 on any set with longer texts.")
ap.add_argument("--plan_window", action="store_true",
help="derive the decode window from the PLAN the model "
"emitted (or was given): min/max frames = "
"--plan_dur_lo/--plan_dur_hi times ΣD̂ of its "
"dequantised word durations, replacing the "
"text-regression --duration_model for that row. S155: "
"the stop threshold is inert and the duration FLOOR is "
"the lever, so this is the mechanism that ties clip "
"length to the model's stated intent. A row whose plan "
"is missing or malformed keeps the old window and says "
"so (plan_status / plan_window_fallback).")
ap.add_argument("--plan_injection", action="store_true",
help="feed the plan into the AUDIO block through the "
"duration-scheduled input-injection channel (§3.2), "
"scheduled from the model's OWN emitted durations. "
"Requires a checkpoint trained with --plan_injection: "
"a zero-init injector is refused rather than run as a "
"silent no-op.")
ap.add_argument("--plan_injection_off", action="store_true",
help="cancel --plan_injection for THIS cell, keeping every "
"other plan flag identical. The prefix-only condition "
"(master plan R3): if injection is the only path to "
"the program, 'the plan was consumed' is trivially "
"true, so the honest headline number is measured with "
"the channel off. Exists as a negation rather than by "
"dropping the flag so a batch script can share one "
"argument block across the ON and OFF cells "
"(pbs/p2cot_eval.pbs) — the two cells then differ by "
"exactly this one token.")
ap.add_argument("--plan_dur_lo", type=float, default=0.8,
help="min_frames = plan_dur_lo * T-hat (D-PLAN profile). "
"0.8 per Amendment 4: [0.9,1.5] contained the true "
"length on only 82.6%% of eval rows even rho-corrected "
"(bar 95%%); [0.8,1.5] measures 98.2%%.")
ap.add_argument("--plan_dur_hi", type=float, default=1.5,
help="hard cap = plan_dur_hi * T-hat, where T-hat = ΣD̂ / ρ "
"(D-PLAN profile). NB the cap scales T-hat, NOT ΣD̂ "
"directly — see plan_decode_window(), which computes "
"max_fr from t_hat. The help text used to say ΣD̂, which "
"understates the cap by 1/ρ (~1.5x) and cost a truncation "
"diagnosis a wrong turn on 2026-09-01.")
ap.add_argument("--force_plan", default=None,
help="JSONL of plans to inject INSTEAD of the model's own — "
"the oracle-plan condition and the plan-flip probe. "
"One object per line, `row` binding it to a val row "
"index (else file order over the selection), carrying "
"`plan` (a token string, a word list, or an extractor "
"sidecar). Requires --mode oracle; get the model's own "
"plans from a --mode self run's plan_words first.")
ap.add_argument("--plan_field", default="plan",
help="row field holding the plan sidecar for a --mode "
"oracle plan arm (matches the dataset's plan_field).")
ap.add_argument("--plan_window_field", default=None,
help="row field holding an EXPLICIT [min_frames, max_frames] "
"window, which then bounds decode instead of "
"--duration_model or --plan_window. This is how the "
"G-P4 flip cells keep one window across conditions: "
"duration bins bound the decode window (§3.4), so "
"letting the window follow a FLIPPED plan would "
"manufacture the length effect the probe measures. "
"scripts/plan_flip_probe.py build writes the control "
"plan's window into every cell row as `plan_window`. A "
"row missing the field is a hard error, not a silent "
"fall back to a different window — that would put two "
"decode regimes inside one cell.")
ap.add_argument("--plan_schedule", choices=["stretch", "pack"],
default="stretch",
help="how plan words map onto frames during injection. "
"'stretch' is training's default and needs a target "
"length (ΣD̂ / --plan_speech_ratio); 'pack' needs only "
"the plan but drifts early at every pause.")
ap.add_argument("--plan_speech_ratio", type=float, default=1.0,
help="ρ for the 'stretch' target: T̂ = ΣD̂ / ρ. ΣD counts "
"SPEECH frames only (word spans exclude pauses — 0.64 "
"of total on the one clip measured so far), so the "
"default 1.0 schedules the program over its own ΣD̂ "
"frames and holds the last word after that. Set it "
"from the corpus's measured time_coverage to spread "
"the program the way training's 'stretch' did; the "
"default invents no constant.")
ap.add_argument("--plan_d_plan", type=int, default=256,
help="MUST match the checkpoint's plan-injector width.")
ap.add_argument("--plan_on_malformed", choices=["fallback", "skip", "error"],
default="fallback",
help="what to do with a row whose plan is absent/truncated/"
"malformed: keep it on the OLD window (recorded, "
"warned, counted), drop the candidate, or abort.")
args = ap.parse_args()
resolve_decode_temps(args)
args.plan_injection = resolve_plan_injection(args.plan_injection,
args.plan_injection_off)
plan_mode = bool(args.plan_window or args.plan_injection or args.force_plan
or args.plan_window_field
or args.cot_mode in ("plan", "tags+plan", "full"))
os.environ.setdefault("MVC_NUM_SPEECH_TOKENS", "2048")
os.environ.setdefault("MVC_NUM_CODEC_LEVELS", "10")
if plan_mode:
os.environ.setdefault("MVC_ENABLE_PLAN_TOKENS", "1")
if args.mode in ("gt", "nocot"):
raise SystemExit(
f"--mode {args.mode} has no <THINK> block, so there is no plan "
f"to read, force or schedule from. Plan decode applies to "
f"--mode self (the model's own program) and --mode oracle (a "
f"given one).")
if args.force_plan and args.mode != "oracle":
raise SystemExit(
"--force_plan requires --mode oracle. The flip/oracle probe is "
"two passes: --mode self to get the model's own plans "
"(plan_words in the output), then --mode oracle --force_plan "
"with the modified file.")
if args.mode == "oracle" and args.cot_mode not in ("plan", "tags+plan",
"full"):
raise SystemExit(
f"--mode oracle with plan decode needs --cot_mode plan/"
f"tags+plan/full (got {args.cot_mode!r}): the window and the "
f"injection must come from a plan the model was actually "
f"conditioned on, not one it never saw.")
if args.plan_dur_lo > args.plan_dur_hi:
raise SystemExit(f"--plan_dur_lo {args.plan_dur_lo} > --plan_dur_hi "
f"{args.plan_dur_hi}: the window would be empty")
if args.plan_window and args.plan_window_field:
raise SystemExit(
"--plan_window and --plan_window_field are mutually exclusive: "
"the first derives the window from the plan IN CONTEXT (which "
"moves when the plan is flipped), the second pins it to the "
"value carried on the row. Pick one per cell.")
if args.plan_speech_ratio <= 0.0:
raise SystemExit("--plan_speech_ratio must be > 0")
if (args.plan_injection and args.plan_schedule == "stretch"
and args.plan_speech_ratio == 1.0):
print("[plan] WARNING: --plan_schedule stretch with "
"--plan_speech_ratio 1.0 schedules the program over its own "
"SumD_hat frames and holds the last word afterwards. Training "
"stretched it over the FULL utterance (SumD/T ~ 0.64 on the "
"one clip measured so far), so this is a train/inference "
"schedule mismatch unless your corpus really has no pauses. "
"Use the `rho = SumD/T median` line from the training log.",
flush=True)
if args.mode == "self":
print(f"[plan] --max_think {args.max_think} caps the program at "
f"~{max(0, (args.max_think - 8)) // 4} words (3-4 tokens "
f"each, before any prose/tags). Rows that outgrow it are "
f"reported as `unterminated`, not silently shortened.",
flush=True)
if args.plan_window and not args.dump_stop_probs:
print("[plan] NOTE: --dump_stop_probs is off, so this run cannot "
"feed the S154 stop-calibration readout (the pre-registered "
"prediction is that plan windows remove the certain-early "
"population). The window itself is recorded either way.",
flush=True)
rows_all = [json.loads(l) for l in open(args.val, encoding="utf-8") if l.strip()]
if args.rows is not None:
idxs = args.rows
else:
cand = range(len(rows_all))
if args.max_words:
cand = [i for i in cand
if len(rows_all[i]["prompt"].split()) <= args.max_words]
idxs = list(cand)[:args.limit]
print(f"[gen] {len(idxs)} prompts selected"
+ (f" (<= {args.max_words} words)" if args.max_words else ""),
flush=True)
rows = [rows_all[i] for i in idxs]
out_path = Path(args.out)
out_path.parent.mkdir(parents=True, exist_ok=True)
if args.mode == "gt":
n = 0
with out_path.open("w", encoding="utf-8") as f:
for j, r in zip(idxs, rows):
f.write(json.dumps({
"prompt": r["prompt"], "row": j, "mode": "gt",
"emotion": r.get("emotion_label"), "speaker_id": r.get("speaker_id"),
"speech_tokens": r["speech_tokens"],
"residual_codes": r["residual_codes"],
"n_frames": len(r["speech_tokens"]),
}) + "\n")
n += 1
print(f"GEN_FISH_COT_SAMPLES_DONE mode=gt wrote {n} -> {out_path}")
return
assert args.checkpoint, "nocot/oracle/self require --checkpoint"
import config as _cfg
from model import MambaCoTModel
from train import load_checkpoint
import test_depth_checkpoint as tdc
assert tdc.NUM_LEVELS == 10, (
f"MVC_NUM_CODEC_LEVELS not seen as 10 (tdc.NUM_LEVELS={tdc.NUM_LEVELS}, "
f"env={os.environ.get('MVC_NUM_CODEC_LEVELS')!r})")
assert _cfg.NUM_SPEECH_TOKENS == 2048, (
f"MVC_NUM_SPEECH_TOKENS not seen as 2048 "
f"(config.NUM_SPEECH_TOKENS={_cfg.NUM_SPEECH_TOKENS}, "
f"env={os.environ.get('MVC_NUM_SPEECH_TOKENS')!r})")
from train import checkpoint_speaker_rows
_ckpt_spk = checkpoint_speaker_rows(args.checkpoint)
if _ckpt_spk is not None and _ckpt_spk != args.num_speakers:
print(f"[speaker] checkpoint carries a {_ckpt_spk}-row speaker table; "
f"--num_speakers {args.num_speakers} overridden to match it "
f"(a mismatch would re-initialise every voice).", flush=True)
args.num_speakers = _ckpt_spk
_dtype = torch.float32 if args.dtype == "fp32" else torch.bfloat16
model = MambaCoTModel(model_name=args.model_name, device=args.device,
dtype=_dtype, mtp_num_heads=0,
backbone_kind=args.backbone)
if args.hybrid_attention_top_k:
model.enable_hybrid_attention(args.hybrid_attention_top_k)
model.enable_speaker_conditioning(speaker_dim=args.speaker_dim,
num_speakers=args.num_speakers,
input_injection=True)
model.enable_depth_module(num_levels=10, codebook_size=2048,
d_depth=args.depth_dim, depth_layers=args.depth_layers,
depth_feedback=args.depth_feedback, cross_frame=0,
codebook_sizes=FISH_CB,
depth_arch=args.depth_arch,
depth_cond=args.depth_cond,
depth_heads=args.depth_heads)
_ckpt_has_injector = False
try:
from train import checkpoint_tensor_shapes
_ckpt_has_injector = any(k.startswith("plan_injector.")
for k in checkpoint_tensor_shapes(args.checkpoint))
except Exception as e:
print(f"[plan] could not peek at {args.checkpoint} ({type(e).__name__}); "
f"falling back to the flag", flush=True)
if args.plan_injection or _ckpt_has_injector:
model.enable_plan_injection(d_plan=args.plan_d_plan,
scaffold_dropout=0.0)
if _ckpt_has_injector and not args.plan_injection:
print("[plan] checkpoint carries a plan injector; building it so the "
"weights load EXACTLY, but this cell decodes with the plan "
"channel unused (Amendment 4's matched floor).", flush=True)
load_checkpoint(args.checkpoint, model, device=args.device)
model.eval()
reg = model.token_registry
if args.tf_check:
_teacher_forced_cache_check(model, reg, args)
return
lts = [args.temp_semantic] + list(args.acoustic_temps)
maps = None
if plan_mode:
maps = plan_id_maps(reg)
if args.plan_injection:
w = model.plan_injector.proj.weight
if float(w.detach().abs().sum().item()) == 0.0:
raise SystemExit(
f"--plan_injection: {args.checkpoint} carried no trained "
f"plan_injector (proj.weight is still exactly zero-init), so "
f"injection would be a numeric no-op. Either the checkpoint "
f"predates the plan channel, or --plan_d_plan "
f"{args.plan_d_plan} does not match its width and the weights "
f"were dropped on load.")
print(f"[plan] injection ON (d_plan={args.plan_d_plan}, bins="
f"{model.plan_bin_sizes}, schedule={args.plan_schedule})",
flush=True)
if args.force_pace and args.duration_model:
print(f"[f0] bounded + --force_pace {args.force_pace}: window is "
f"computed from the ORIGINAL row; readout_f0_probe.py verifies "
f"it is identical across cells from the recorded dur_window",
flush=True)
dur_model_for_cap = None
if args.scale_cap and args.duration_model:
raise SystemExit(
"--scale_cap and --duration_model are mutually exclusive: one "
"imposes a WINDOW on the length, the other only a ceiling, and "
"the bounded path silently wins. Pick one.")
if args.scale_cap:
if not args.duration_model_path:
raise SystemExit(
"--scale_cap needs --duration_model_path (the predictor is "
"used for the ceiling only; pass --duration_model instead if "
"you want a bounded window)")
dur_model_for_cap = json.loads(
Path(args.duration_model_path).read_text(encoding="utf-8"))
print(f"CAP SCALED to {args.scale_cap:g}x the predicted duration "
f"(floor {args.max_frames}); decode is otherwise UNBOUNDED",
flush=True)
dur_model = None
if args.duration_model:
dur_model = json.loads(Path(args.duration_model).read_text(
encoding="utf-8"))
print(f"DURATION-BOUNDED decode: {args.duration_model} "
f"feats={dur_model.get('features')} "
f"window [{args.dur_lo:g}x, {args.dur_hi:g}x] of prediction",
flush=True)
forced = load_forced_plans(args.force_plan) if args.force_plan else None
if args.plan_window:
print(f"PLAN-BOUNDED decode (§3.4): window [{args.plan_dur_lo:g}x, "
f"{args.plan_dur_hi:g}x] of the plan's own ΣD̂"
+ (f", overriding {args.duration_model}" if dur_model else "")
+ f"; malformed -> {args.plan_on_malformed}", flush=True)
plan_counts = {}
n = 0
with out_path.open("w", encoding="utf-8") as f, torch.no_grad():
for order_index, (j, r_orig) in enumerate(zip(idxs, rows)):
r, f0_tag = f0_row(r_orig, args)
spk = (args.force_speaker_id if args.force_speaker_id is not None
else r.get("speaker_id", 1))
min_fr = max(20, int(len(r_orig["prompt"].split())
* args.min_frames_per_word))
max_fr = args.max_frames
if dur_model is not None:
t_hat = predict_frames(dur_model, r_orig)
min_fr = max(20, int(args.dur_lo * t_hat))
max_fr = max(min_fr + 5, int(args.dur_hi * t_hat))
elif args.scale_cap:
t_hat = predict_frames(dur_model_for_cap, r_orig)
max_fr = max(args.max_frames, int(args.scale_cap * t_hat))
window_field_value = None
if args.plan_window_field:
window_field_value = _row_window(r_orig, args.plan_window_field,
args.val, j)
min_fr, max_fr = window_field_value
base_min_fr, base_max_fr = min_fr, max_fr
base_source = (
f"row_field:{args.plan_window_field}" if window_field_value
else "duration_model" if dur_model is not None
else "scale_cap" if args.scale_cap else "words")
given_words, plan_ids, plan_source = None, None, "self"
if plan_mode and args.mode == "oracle":
if forced is not None:
obj = forced_plan_for(forced, j, order_index, args.force_plan)
given_words = plan_words_from_obj(
obj, where=f"--force_plan entry for row {j}")
plan_source = "forced"
else:
if r_orig.get(args.plan_field) is None:
raise SystemExit(
f"{args.val} row {j} has no {args.plan_field!r} "
f"field, so there is no oracle plan to condition "
f"on. Point --plan_field at the sidecar, or pass "
f"--force_plan.")
given_words = plan_words_from_obj(
r_orig[args.plan_field],
where=f"{args.val} row {j} field {args.plan_field!r}")
plan_source = "row"
plan_ids = plan_prefix_ids(reg, given_words)
for c in range(args.candidates):
torch.manual_seed(args.seed + 1000 * j + c)
prefix, rstr = build_prefix(args.mode, model, reg, r, think_budget(args.max_think, r.get("prompt")),
args.think_temp, args.think_top_k, spk,
args.device, cot_mode=args.cot_mode,
plan_ids=plan_ids)
min_fr, max_fr = base_min_fr, base_max_fr
plan_extra, plan_bins, plan_sched = {}, None, None
if plan_mode:
parsed = parse_plan_tokens(
plan_region(prefix[0].tolist(), reg), maps)
if given_words is not None:
got = [(w["pitch_bin"], w["dur_bin"], w["energy_bin"])
for w in parsed.words]
want = [(w["pitch_bin"], w["dur_bin"], w["energy_bin"])
for w in given_words]
if not parsed.ok or got != want:
raise SystemExit(
f"row {j}: the plan written into the prefix does "
f"not read back ({parsed.status}: "
f"{parsed.reason or 'bins differ'}). The "
f"registry's bin<->id mapping and the parser "
f"disagree — every conditioned arm would be "
f"conditioned on something other than the plan "
f"it was handed.")
win = resolve_decode_window(
parsed, base_min_fr, base_max_fr, base_source,
dur_lo=args.plan_dur_lo, dur_hi=args.plan_dur_hi,
use_plan=args.plan_window,
speech_ratio=args.plan_speech_ratio)
min_fr, max_fr = win.min_fr, win.max_fr
plan_counts[parsed.status] = plan_counts.get(
parsed.status, 0) + 1
if not parsed.ok:
print(f"[row {j} cand {c}] PLAN {parsed.status}: "
f"{parsed.reason}", flush=True)
if args.plan_on_malformed == "error":
raise SystemExit(
f"row {j} cand {c}: plan {parsed.status} and "
f"--plan_on_malformed error")
if args.plan_on_malformed == "skip":
continue
target_frames = None
if args.plan_injection and parsed.ok:
durs = plan_dur_frames(parsed.words)
if args.plan_schedule == "stretch":
target_frames = max(1, int(round(
sum(durs) / args.plan_speech_ratio)))
plan_sched = tdc.build_decode_plan_schedule(
durs, prefix.shape[1], max_fr,
mode=args.plan_schedule, target_frames=target_frames,
device=args.device)
plan_bins = plan_bins_tensor(parsed.words,
device=args.device)
plan_extra = {
"plan_source": plan_source,
"plan_status": parsed.status,
"plan_ok": parsed.ok,
"plan_reason": parsed.reason,
"plan_n_words": parsed.n_words,
"plan_used": (plan_token_string(parsed.words)
if parsed.ok else None),
"plan_words": [[w["pitch_bin"], w["dur_bin"],
w["energy_bin"]] for w in parsed.words],
"plan_t_hat": (round(win.t_hat, 2)
if win.t_hat is not None else None),
"plan_sum_frames": (round(plan_sum_frames(parsed.words), 2)
if parsed.ok else None),
"plan_window_fallback": win.fallback,
"plan_injected": plan_bins is not None,
"plan_schedule_mode": (args.plan_schedule
if plan_bins is not None else None),
"plan_speech_ratio": (args.plan_speech_ratio
if plan_bins is not None else None),
"plan_target_frames": target_frames,
"dur_window_source": win.source,
}
sp_out = [] if args.dump_stop_probs else None
lb = (tdc.LoopBreaker(threshold=args.loop_break_flipk,
win=args.loop_break_win,
temp=args.loop_break_temp,
rep=args.loop_break_rep,
stop_after=args.loop_break_stop_after)
if args.loop_break_flipk > 0 else None)
if args.force_frames:
min_fr = max_fr = int(args.force_frames)
sg = None
if args.silence_guard_codes:
with open(args.silence_guard_codes, encoding="utf-8") as _sf:
_sc = json.load(_sf)
sg = tdc.SilenceGuard(
codes=(_sc["codes"] if isinstance(_sc, dict) else _sc),
budget=args.silence_guard_budget,
stop_after=args.silence_guard_stop_after,
stop_without_plan=args.silence_guard_stop_noplan,
retries=args.silence_guard_retries,
retry_temp=args.silence_guard_retry_temp)
_cuda = str(args.device).startswith("cuda")
if _cuda:
torch.cuda.synchronize(); torch.cuda.reset_peak_memory_stats()
_t0 = _time_rtf.perf_counter()
codes = tdc.depth_generate(
model, prefix, reg, max_frames=max_fr, level_temps=lts,
stop_prob_out=sp_out,
top_k=args.top_k, top_p=args.top_p, allow_stop=True, min_frames=min_fr,
stop_threshold=args.stop_threshold, speaker_id=spk,
feedback=(args.decode_feedback or args.depth_feedback),
cross_frame_decode=False,
device=args.device, antifan_bias=args.antifan_bias,
antifan_levels=args.antifan_levels,
onset_frames=args.onset_frames,
onset_temp_scale=args.onset_temp_scale,
rep_penalty=(args.rep_penalty_levels or args.rep_penalty),
rep_win=args.rep_win,
ras_tau=(args.ras_tau_levels or args.ras_tau),
ras_win=args.ras_win,
plan_bins=plan_bins, plan_schedule=plan_sched,
loop_breaker=lb, silence_guard=sg, cached=args.cached)
if _cuda:
torch.cuda.synchronize()
_gen_s = _time_rtf.perf_counter() - _t0
_peak_gb = (torch.cuda.max_memory_allocated() / 2 ** 30) if _cuda else None
if codes is None or len(codes[0]) < 3:
print(f"[row {j} cand {c}] SKIP empty gen: {r['prompt'][:40]!r}",
flush=True)
continue
stop_extra = {}
if sp_out is not None:
stop_extra = {
"stop_probs": [round(p, 6) for p, _ in sp_out],
"stop_argmax": [int(a) for _, a in sp_out],
}
f.write(json.dumps({
"prompt": r_orig["prompt"], "row": j, "cand": c,
"mode": args.mode,
"prompt_used": r["prompt"], "tag": f0_tag,
"dur_window": [min_fr, max_fr],
"emotion": r.get("emotion_label"), "speaker_id": spk,
"reasoning_used": rstr,
"speech_tokens": codes[0], "residual_codes": codes[1:],
"n_frames": len(codes[0]),
"audio_seconds": round(len(codes[0]) / CODEC_FPS, 3),
"gen_seconds": round(_gen_s, 3),
"rtf_lm": round(_gen_s / (len(codes[0]) / CODEC_FPS), 4),
"cached": bool(args.cached),
"seed": int(args.seed),
"level_temps": [round(float(t), 3) for t in lts],
"temps_explicit": bool(getattr(args, "temps_explicit", False)),
**({"peak_mem_gb": round(_peak_gb, 3)} if _peak_gb is not None else {}),
"cb0_flipk": round(tdc.flipk_rate(codes[0]), 2),
**({"loop_break": lb.stats()} if lb is not None else {}),
**({"silence_guard": sg.stats()} if sg is not None else {}),
**plan_extra,
**stop_extra,
}) + "\n")
n += 1
f.flush()
extra = f" think={rstr[:40]!r}" if rstr else ""
if plan_extra:
extra += (f" plan={plan_extra['plan_status']}/"
f"{plan_extra['plan_n_words']}w "
f"win={min_fr}-{max_fr}")
print(f"[row {j} c{c}] {len(codes[0])} fr "
f"emo={r.get('emotion_label')} {r['prompt'][:34]!r}{extra}",
flush=True)
if plan_mode:
print("PLAN_STATUS_SUMMARY " + (" ".join(
f"{k}={v}" for k, v in sorted(plan_counts.items())) or "none"),
flush=True)
print(f"GEN_FISH_COT_SAMPLES_DONE mode={args.mode} wrote {n} -> {out_path}")
if __name__ == "__main__":
main()