voice_audio_multi/tests/moss_ttsd/reference/dump_prompt_fixtures.py
Fabio c36510c21e
Some checks failed
Linux Build / Linux cpu (push) Has been cancelled
Linux Build / Linux cpu-clang (push) Has been cancelled
Linux Build / Linux vulkan (push) Has been cancelled
mac-build / macOS CPU (push) Has been cancelled
Nix Build / Nix (cpu) (push) Has been cancelled
Nix Build / Nix (vulkan) (push) Has been cancelled
windows-build / Windows CPU (push) Has been cancelled
Initial import: audio.cpp with Orange Pi documentation
2026-11-04 19:05:22 +01:00

89 lines
4.3 KiB
Python

# Dump prompt parity fixtures for MOSS-TTSD, in the schema the moss_tts_v15
# prompt_parity harness reads.
#
# Prompt-only, so the model weights are never loaded: the processor is built for
# its tokenizer and its message rendering. The codec IS loaded, because
# AutoProcessor constructs it -- a reference with audio needs real codes and
# there is no way to ask the reference for a prompt without one.
import json, os, sys
import torch
from transformers import AutoProcessor
MODEL = "/mnt/data/models/MOSS-TTSD-v1.0"
CODEC = "/mnt/data/models/MOSS-Audio-Tokenizer"
OUT = os.path.dirname(os.path.abspath(__file__))
proc = AutoProcessor.from_pretrained(MODEL, trust_remote_code=True, codec_path=CODEC)
pm = sys.modules[type(proc).__module__]
n_vq = int(proc.model_config.n_vq)
sr = int(proc.model_config.sampling_rate)
def dump(name, msg):
ids = proc([msg], mode="generation")["input_ids"] # (1, rows, 1+n_vq)
rec = {
"name": name,
"text": msg.text,
"instruction": msg.instruction,
"language": msg.language,
# Recorded as the message actually carried them. Writing a value the
# message did not use makes the fixture describe a prompt nobody rendered.
"quality": msg.quality,
"sound_event": msg.sound_event,
"ambient_sound": msg.ambient_sound,
"content": msg._content,
"input_ids": ids[0].tolist(),
}
# Positional, one entry per speaker, null where that speaker brought no
# audio. The layout IS the test: a fixture that only listed the present
# speakers could not tell a "[S2]: None" line from a missing one.
#
# Stored [n_vq][frames], the codec layout the C++ ReferenceAudio takes, while
# the message wants [frames][channels] -- hence the transpose. Truncated to
# n_vq first: the tokenizer emits 32 RVQ layers and this model reads the
# first 16 (processing_moss_tts.py, "Always follow model RVQ channels"), so a
# fixture carrying all 32 would describe codes the model never saw.
if isinstance(msg.reference, list):
rec["reference_codes"] = [
None if r is None else r[:, :n_vq].transpose(0, 1).contiguous().tolist()
for r in msg.reference
]
json.dump(rec, open(f"{OUT}/ref_prompt_{name}.json", "w"))
print(f"wrote ref_prompt_{name}.json rows={ids.shape[1]} channels={ids.shape[2]} (1+{n_vq})")
DIALOGUE = "[S1] So what did you make of it? [S2] Honestly, I was not expecting that ending."
# 1. Two speakers, neither cloned: the "[S<n>]: None" branch, twice.
dump("dialogue_none", pm.UserMessage(text=DIALOGUE, reference=[None, None]))
# 2. No speakers named at all: the `reference is None` branch.
dump("dialogue_no_reference", pm.UserMessage(text=DIALOGUE))
# 3. One cloned, one not -- the mixed case, which is where a shape error in
# either branch shows up as a token shift rather than a wrong count.
torch.manual_seed(0)
wav = torch.zeros(1, sr) # one second; content is irrelevant to prompt shape
wav[0, ::137] = 0.2 # some structure so the codec does not emit a constant
codes = proc.encode_audios_from_wav([wav], sampling_rate=sr)
dump("dialogue_mixed", pm.UserMessage(text=DIALOGUE, reference=[codes[0], None]))
# 4. Continuation: the mode TTSD actually clones in. The user turn names the
# speakers, and an assistant turn carries the concatenated reference audio
# that the model continues from. The assistant span uses a different pair of
# slot tokens from a user span -- gen for the frames, delay for the trailing
# n_vq - 1 -- and the turn is left unterminated so generation continues it.
cont_msgs = [
pm.UserMessage(text=DIALOGUE, reference=[codes[0], None]),
pm.AssistantMessage(audio_codes_list=[codes[0]]),
]
ids = proc([cont_msgs], mode="continuation")["input_ids"]
json.dump({
"name": "dialogue_continuation",
"text": DIALOGUE,
"instruction": None, "language": None,
"quality": None, "sound_event": None, "ambient_sound": None,
"content": cont_msgs[0]._content,
"reference_codes": [codes[0][:, :n_vq].transpose(0, 1).contiguous().tolist(), None],
"assistant_codes": codes[0][:, :n_vq].transpose(0, 1).contiguous().tolist(),
"input_ids": ids[0].tolist(),
}, open(f"{OUT}/ref_prompt_dialogue_continuation.json", "w"))
print(f"wrote ref_prompt_dialogue_continuation.json rows={ids.shape[1]}")