Add Irodori-TTS v4 support - #881
Merged
Merged
Conversation
Collaborator
lucasnewman
approved these changes
Aug 10, 2026
Irodori-TTS v4-Small replaces the two scratch-trained text/caption encoders with a single pretrained ModernBERT-ja-310m backbone feeding separate projectors, and raises speaker_patch_size to 4. - Port ModernBERT to MLX (modernbert.py): fused Wqkv, GeGLU MLP, alternating global/sliding-window attention with per-layer-type RoPE theta, and layer 0's identity attn_norm. Verified against transformers on random weights to 5e-7. - Add PretrainedTextBackbone + PretrainedConditionProjector and route text/caption encoding through them when text_encoder_type is "pretrained"; scratch encoders remain the default for v1-v3. - Load the tokenizer bundled with the converted weights when present, so inference no longer depends on the upstream tokenizer repo. - Support multi-clip reference audio: clips are encoded separately and concatenated, then trimmed to the checkpoint's ref_max_seconds (120s for v4, 30s before). - Size the no-reference placeholder latent to speaker_patch_size, which otherwise patches down to an empty sequence at v4's patch size 4. The sliding-window mask keeps its diagonal open so padding queries far from any real token cannot produce an all-masked softmax row. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nn.quantize turns the projection into a QuantizedLinear whose weight is packed uint32, so aligning the backbone activations to self.projector.weight.dtype truncated them to integers. This was a no-op in fp16, so only quantized v4 builds were affected: the text/caption projector output landed 79% off and generation was audibly broken. Dropping the cast brings the 8-bit projector back to 0.7% of fp16, and text_state to 0.7% of the fp32 PyTorch reference. Adds a regression test that quantizes a small v4 model and checks the projector output scale. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two divergences from the reference implementation, both affecting caption-conditioned checkpoints (v2 VoiceDesign, v3 VoiceDesign, v4): - An absent caption still tokenizes to a BOS token, and its mask was left set, so the DiT and the duration predictor saw a one-token caption instead of no caption at all. Upstream zeroes the caption mask when the caption text is empty. For v4 voice cloning without a caption this shortened the predicted duration by ~4% (79 vs 82 frames) and cut the tail of the utterance. - The trailing-silence trim was applied unconditionally, so a silence point of 0 would have produced zero-length audio. Upstream only applies the trim when the flattening point is positive. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
normalize_text had drifted from Irodori-TTS/irodori_tts/text_normalization.py in ways that changed what the model is actually asked to read. All of these predate v4 and affect every version: - Sentence-final 。 and 、 were stripped. Upstream keeps them, and they carry prosody: dropping the 。 shortened predicted durations and clipped endings. - ASCII spaces (U+0020) were removed, so "hello world" became "helloworld". Only the ideographic space U+3000 should go. - ".." / "..." were not folded to "…". - NFKC normalization was missing; only fullwidth alnum and halfwidth kana were approximated by hand, leaving ㈱ or Ⅲ untouched. - Outer-bracket stripping ran once and ignored nesting depth, so 「前半」と「後半」 lost its outer quotes and ((x)) only lost one level. The caption is now stripped (not normalized), matching upstream, and the reported token count uses the mask instead of the padded width. Adds a differential test that runs both implementations over a fuzz corpus when an upstream checkout is present, plus explicit cases for each behaviour. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- use_cfg ignored has_caption_cfg, so a caption-only checkpoint run with cfg_scale_text=0 would have skipped guidance entirely. - has_caption_cfg did not check that the caption mask is non-empty. With an empty caption the caption-uncond bundle is identical to the conditional one, so its guidance term is zero: computing it just cost an extra batch slot per step. Also documents that v4 over-predicts duration for short caption-only prompts and repeats the utterance to fill the window. That is upstream model behaviour: the reference PyTorch implementation predicts the same frame counts (verified to 0.0000%) and repeats identically. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The normalization differential test looked for the reference repository at a single hard-coded location. Honour IRODORI_TTS_UPSTREAM first so it can run wherever the checkout lives, and keep skipping when absent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
yoshphys
force-pushed
the
feature/irodori-tts-v4-pr
branch
from
August 10, 2026 23:46
e9cc914 to
e5dc2a7
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds support for Irodori-TTS v4-Small, and fixes several divergences from the reference implementation that were found while validating it.
v4 support
v4-Small unifies the previous base and VoiceDesign models into one checkpoint. Compared to v3 the architecture change is narrow: the two scratch-trained text/caption encoders are replaced by a single pretrained ModernBERT-ja-310m backbone feeding separate projectors, and
speaker_patch_sizegoes from 1 to 4. The DiT, adaLN, speaker encoder and duration predictor are unchanged.modernbert.py: MLX port of ModernBERT — fusedWqkv, GeGLU MLP, alternating global / sliding-window attention with per-layer-type RoPE theta, and layer 0's identityattn_norm.PretrainedTextBackbone+PretrainedConditionProjector, used whentext_encoder_typeis"pretrained". v1–v3 keep the scratch encoders.ref_max_seconds(120s for v4, 30s before). This matches how v4 was trained.Converted models:
mlx-community/Irodori-TTS-v4-Small-fp16and-8bit.Fixes that also change v1–v3 behaviour
Validating v4 against the reference implementation surfaced pre-existing bugs.
normalize_texthad drifted in ways that change what the model is asked to read, so existing checkpoints will sound different (closer to the reference):。and、were stripped. Upstream keeps them, and they carry prosody — dropping the。shortened predicted durations and clipped endings."hello world"became"helloworld". Only the ideographic space U+3000 should go.../...were not folded to….㈱orⅢuntouched.「前半」と「後半」lost its outer quotes.Also fixed, affecting every caption-conditioned checkpoint (v2 VD, v3 VD, v4):
use_cfgignoredhas_caption_cfg, andhas_caption_cfgdid not check that the caption mask is non-empty.And one v4-only bug:
PretrainedConditionProjectorcoerced its input toself.projector.weight.dtype. Afternn.quantizethat weight is packed uint32, so activations were truncated to integers — a no-op in fp16, but it put the 8-bit projector output 79% off.Verification
Each stage was compared against the reference PyTorch implementation on the real v4 checkpoint:
transformers)normalize_text(3035 strings incl. 3000 fuzz)text_state(backbone + projector + norm)v_pred)The two non-exact rows are fp16-vs-fp32 weight error.
Known model behaviour (not a port issue)
With a caption but no reference audio, v4 roughly doubles the predicted duration for short texts (under ~7 tokens) and reads the sentence twice to fill the window. Running the reference PyTorch sampler produces the same frame counts and the same repetition, so this is upstream model behaviour. It is documented in the model README along with the workarounds (pass reference audio, or set
duration_scale/seconds).Tests
79 Irodori tests pass (13 new for v4, 4 for caption/tail-trim handling, 8 for normalization including a differential check against a local upstream checkout when present). Full
test_models.py: 441 passed, 1 pre-existing unrelated failure (TestSparkTTSModel::test_init).🤖 Generated with Claude Code