26 Commits
Author SHA1 Message Date
Federico Volovnik-JoseandClaude Opus 4.7 0435efa858 feat(loader): mixed-precision audio_tokenizer (norms stay fp32)
A full fp16 cast on the audio_tokenizer saved ~3 GB VRAM but produced
silent audio: fp16 lacks the dynamic range LayerNorm/RMSNorm need to
keep their normalization statistics meaningful, and the decoder
collapsed to near-zero output.

Now: cast all weights to the resolved dtype, then explicitly cast
LayerNorm/GroupNorm/RMSNorm back to fp32. A forward pre-hook on every
leaf-ish module aligns incoming float tensors with each module's own
param dtype, so:
- fp32 wav input -> fp16 Linear: hook casts to fp16
- fp16 hidden state -> fp32 LayerNorm: hook casts to fp32
- fp32 norm output -> fp16 next layer: that layer's hook casts to fp16

Tested with a tiny mixed Linear/Conv1d/LayerNorm stand-in: fp32 inputs
flow through fp16 Linear, fp16 hidden states reach fp32 norm cleanly.
End-to-end audio quality has to be verified on the real model.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 13:03:37 +02:00
Federico Volovnik-Jose 81aeb98fc7 Revert "feat(loader): add int8 dtype option for VRAM-tight GPUs"
This reverts commit ca798a7832.
2026-04-28 13:02:43 +02:00
Federico Volovnik-JoseandClaude Opus 4.7 ca798a7832 feat(loader): add int8 dtype option for VRAM-tight GPUs
Adds bitsandbytes 8-bit quantization on the main LM as an alternative
memory-saving path. The audio_tokenizer (where fp16 broke output
quality) stays fp32 and is unaffected.

Wiring:
- New "int8" entry in MOSSTTSLoader's dtype dropdown
- get_or_load branches on dtype_str == "int8" — passes BitsAndBytesConfig
  + device_map={"": resolved_device} to from_pretrained, skipping the
  torch_dtype + .to(device) path (incompatible with bnb-loaded models)
- bitsandbytes is lazy-imported with a clear install message; only int8
  users pay the dependency
- Validates that int8 is requested only on cuda (no cpu kernels)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 12:39:46 +02:00
Federico Volovnik-Jose d737a39147 Revert "fix(loader): cast audio_tokenizer to model dtype on cuda"
This reverts commit b9312fa778.
2026-04-28 12:36:36 +02:00
Federico Volovnik-JoseandClaude Opus 4.7 86f24e44db fix(loader): cast audio_tokenizer inputs to param dtype via forward hook
After casting the audio_tokenizer's weights to fp16/bf16 (saves ~3 GB
VRAM), incoming fp32 wav tensors hit Linear/Conv layers with fp16
weights and crash with "expected scalar type Float but found Half" deep
inside encode_audios_from_wav.

Register a forward-pre-hook on every leaf-ish module of the
audio_tokenizer that casts floating-point input tensors to the module's
parameter dtype on the fly. Covers both the encode (voice reference)
and decode paths without needing to enumerate the model's submodule
names.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 12:12:28 +02:00
Federico Volovnik-JoseandClaude Opus 4.7 b9312fa778 fix(loader): cast audio_tokenizer to model dtype on cuda
AutoProcessor.from_pretrained loads the audio_tokenizer model in fp32 by
default, ignoring the torch_dtype passed to the main model. .to(device)
without an explicit dtype only relocates, doesn't cast — leaving the
audio_tokenizer at fp32 on the GPU and roughly doubling its VRAM
footprint.

On a 20 GB card this is the difference between a comfortable headroom
and OOM during generation: observed peak reserved was 19.7 GB / 20.5 GB
total, despite expandable_segments already being on. Casting to the
model's resolved dtype on cuda recovers ~3 GB.

CPU path keeps default precision; fp16 ops are slow on most CPUs and
the VRAM concern doesn't apply off-GPU.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 09:06:58 +02:00
Federico Volovnik-Jose f6e5d420a1 feat(loader): expose audio_tokenizer_device option for memory-tight GPUs
Add a new audio_tokenizer_device input on MOSSTTSLoader (auto/cuda/cpu).
auto mirrors the main model's device (previous behaviour). cpu keeps the
audio tokenizer on host RAM, saving ~1-3 GB of VRAM at the cost of some
CPU latency at the start (encoding the reference clip) and end (decoding
the generated codes) of each generation.

Useful on 16-20 GB GPUs where MOSS-TTS' main model + activations push
right against the ceiling. Including the audio_tokenizer_device in the
cache key prevents stale entries when a user toggles the setting.
2026-04-27 15:04:44 +02:00
Federico Volovnik-Jose 0fc61d561d fix(loader): cheap repr for MOSS_TTS_MODEL to avoid cascading OOM in error path
When generation fails (e.g. OOM), ComfyUI's execution.format_value calls
repr() on the workflow input dict. Default dict.__repr__ recursively reprs
the contained Processor, whose __repr__ calls to_dict() which deepcopies
every nested submodule — including the audio_tokenizer's full GPU weights.
That deepcopy triggers a second OOM during error formatting, swamps the
trace with copy.deepcopy frames, and masks the original error.

Wrap the model handle in a dict subclass with a tiny __repr__ that just
shows device / dtype / attn / model_id and never touches the heavy
contents. All existing entry['key'] / entry.get(...) access patterns are
preserved.
2026-04-27 13:11:58 +02:00
Federico Volovnik-Jose 2de74ef795 fix(loader): backport _get_initial_cache_position for transformers 5.x
transformers 5.x removed GenerationMixin._get_initial_cache_position in
favour of new cache helpers. MOSS-TTS' custom _sample method still calls
self._get_initial_cache_position(cur_len, device, model_kwargs) so we have
to restore it. The reimplementation matches the old behaviour: populate
model_kwargs['cache_position'] with arange(past_length, cur_len).
2026-04-27 13:05:22 +02:00
Federico Volovnik-Jose 6c3bb212e9 fix(loader): make attributes auto-derive shim no-op on transformers 5.x
The user upgraded transformers and the API got reworked — ProcessorMixin
no longer exposes 'attributes' / 'optional_attributes' as data classvars
(replaced by methods like get_attributes()). Our shim was unconditionally
patching ProcessorMixin.__init__ and crashing on AttributeError when its
inner code referenced cls.optional_attributes.

Skip the entire patch on transformers 5.x where the old classvars are
absent — the new release auto-derives attributes natively, no shim needed.
Same gate-by-feature-detection logic as the other shims in this function.
2026-04-27 13:02:24 +02:00
Federico Volovnik-Jose bc06db37f5 fix(loader): delegate missing model_config attrs to language_config
MossTTSDelayConfig only propagates hidden_size and vocab_size from its inner
language_config to the top-level config. Many decoder attrs that transformers'
generation/cache machinery requires (num_hidden_layers, num_attention_heads,
head_dim, rope_*, etc.) only exist on language_config and aren't surfaced.

Install a __getattr__ fallback on the config class so any otherwise-missing
attribute is fetched from language_config. Combined with the existing
token_id backfill (which has to overwrite explicit Nones, not missing
attrs), this lets transformers' internals find what they need without us
hand-listing every attribute they might touch.
2026-04-27 09:34:17 +02:00
Federico Volovnik-Jose 8fd7691813 fix(loader): backfill pad/eos/bos token_id on MOSS-TTS model_config
MossTTSDelayConfig.__init__ sets self.pad_token_id = 151643 (default), then
calls super().__init__(**kwargs). PretrainedConfig's initializer overwrites
self.pad_token_id back to None when kwargs has no pad_token_id — which is
exactly the case here, since the repo's saved config.json only stores those
IDs under language_config, not top-level. The end result is
processor.model_config.pad_token_id == None, and MOSS-TTS' _pad crashes
with 'can't assign a NoneType to a torch.LongTensor' on the first
processor() call.

After loading the processor, copy pad/eos/bos token_id up from
language_config to the top-level model_config when missing.
2026-04-27 09:15:37 +02:00
Federico Volovnik-Jose 538f4f17b6 fix(loader): only treat string *_class attributes as processor attributes
Previous auto-derive logic counted any cls attribute ending in '_class' that
was non-None. ProcessorMixin has methods like check_argument_for_proper_class
and register_for_auto_class which match that suffix, so the derived attributes
list ended up as ['check_argument_for_proper', 'register_for_auto', 'tokenizer'].

Filter to data attributes whose value is a string — that's how transformers
declares processor classes (tokenizer_class = 'AutoTokenizer'). Methods'
function objects are not strings and are now excluded.
2026-04-27 08:48:40 +02:00
Federico Volovnik-Jose 62e201edd2 fix(loader): auto-derive ProcessorMixin.attributes from *_class declarations
Newer transformers releases compute ProcessorMixin.attributes automatically
from each subclass' *_class members. 4.57.6 still hardcodes
['feature_extractor', 'tokenizer'] as the default, and MOSS-TTS' processor
inherits that default while only declaring tokenizer_class and
audio_tokenizer_class. ProcessorMixin.__init__ then asserts both attributes
were passed and crashes with 'This processor requires 2 arguments:
feature_extractor, tokenizer'.

Monkey-patch ProcessorMixin.__init__ to derive cls.attributes from *_class
class members for any subclass that didn't override attributes itself.
optional_attributes are excluded from the derived list to mirror the
new transformers behavior.
2026-04-27 08:43:04 +02:00
Federico Volovnik-Jose e21b059767 fix(loader): register AutoModel -> PreTrainedModel in AUTO_TO_BASE_CLASS_MAPPING
MOSS-TTS' processor declares audio_tokenizer_class = 'AutoModel'. transformers'
ProcessorMixin then runs check_argument_for_proper_class which translates
that string via AUTO_TO_BASE_CLASS_MAPPING. The mapping in 4.57.6 only knows
AutoTokenizer / AutoFeatureExtractor / AutoImageProcessor / AutoVideoProcessor;
AutoModel isn't there, so the lookup returns 'AutoModel' literally and the
isinstance check tries MossAudioTokenizerModel against the AutoModel factory
class (not its instances). That always fails.

Registering AutoModel -> PreTrainedModel makes the check resolve to the base
class that MossAudioTokenizerModel actually inherits from, so the processor
init succeeds.
2026-04-27 08:22:58 +02:00
Federico Volovnik-Jose 017076e48d fix(loader): also alias PreTrainedConfig -> PretrainedConfig
MOSS-TTS' audio tokenizer config does:
  from transformers.configuration_utils import PreTrainedConfig

In newer transformers (>= ~4.58) the class was renamed from PretrainedConfig
(lowercase t) to PreTrainedConfig (capital T). On 4.57.6 only the old name
exists, so the import fails. Aliasing the new name to the old one in the
compat shim lets MOSS-TTS' remote code import it regardless of version.
2026-04-27 07:55:17 +02:00
Federico Volovnik-Jose d7d090eb75 fix(loader): shim transformers.processing_utils.MODALITY_TO_BASE_CLASS_MAPPING
MOSS-TTS' remote processor code does:
  processing_utils.MODALITY_TO_BASE_CLASS_MAPPING['audio_tokenizer'] = 'PreTrainedModel'

That attribute only exists in transformers releases newer than what most
ComfyUI envs ship (e.g. transformers 4.57.6 doesn't have it yet). Without it,
AutoProcessor.from_pretrained crashes with AttributeError before the model
can load. Add a compat helper that creates the dict if missing, so MOSS-TTS'
own line works regardless of the installed transformers version.
2026-04-27 07:48:24 +02:00
Federico Volovnik-Jose 96bf1a5f1b fix: pytest collection + lazy transformers import
Two issues from final review verification:

1. Pytest's default importer walked into the root __init__.py (which uses
   ComfyUI-relative imports) when collecting tests. Adding a root conftest.py
   with collect_ignore for non-test top-level dirs and switching to
   --import-mode=importlib stops it. The conftest also seeds sys.path so
   tests can still 'from lib.x import' / 'from nodes.x import'.

2. lib/model_state.py used to do 'from transformers import AutoModel,
   AutoProcessor' at module-level. transformers' lazy chain transitively
   imports torchvision, which breaks at attribute access in environments
   where torch and torchvision versions don't match (common in ComfyUI
   installs). Lifting the import into a _get_auto_classes() factory keeps
   the package importable; tests patch the factory instead of patching
   transformers attributes.
2026-04-26 14:55:10 +02:00
Federico Volovnik-Jose 284d50b44c fix: convert to relative imports + cache key/cpu/cleanup_one fixes
- Use relative imports across nodes/ and lib/ so ComfyUI's loader resolves the
  local subpackages instead of colliding with ComfyUI's own top-level nodes module.
- tts_output_to_audio_dict now returns CPU tensors so downstream nodes don't see CUDA tensors.
- model_state cache key uses resolved dtype string, so dtype=auto and the matching
  explicit choice share one cache slot (no spurious 13 GB reload).
- Add cleanup_one(key) helper as called for in spec section 4.
- Fix pyproject Homepage URL to point at this repo.
2026-04-25 16:51:32 +02:00
Federico Volovnik-Jose 45f57ee640 feat(lib): add generation parameter assembly helpers 2026-04-25 16:32:08 +02:00
Federico Volovnik-Jose 6d36d3878f feat(lib): add model state cache with auto device/dtype/attn resolution 2026-04-25 16:30:56 +02:00
Federico Volovnik-Jose 1964079031 feat(lib): add voice reference resolution 2026-04-25 16:29:30 +02:00
Federico Volovnik-Jose 0370200d19 feat(lib): add audio format conversions 2026-04-25 16:28:37 +02:00
Federico Volovnik-Jose 983b064b81 feat(lib): add DelayGenerationConfig (from notebook) 2026-04-25 16:27:31 +02:00
Federico Volovnik-Jose 1328b2a40c feat(lib): add quality presets and resolve_preset 2026-04-25 16:26:24 +02:00
Federico Volovnik-Jose 1e0563db14 chore: initial project skeleton 2026-04-25 11:13:38 +02:00