Summary
AlignmentStreamAnalyzer patches self_attn.forward every time it is constructed, capturing the previous forward as original_forward and never restoring it. Each construction therefore adds one more layer to the call chain. After enough generations in a single ComfyUI process, calling the layer recurses through every accumulated wrapper and raises RecursionError.
The failure is quiet: generation then returns a very short clip, and ComfyUI still reports the prompt as successful.
Where
engines/chatterbox/models/t3/inference/alignment_stream_analyzer.py (current main):
# Backup original forward
original_forward = target_layer.forward
def patched_forward(self, *args, **kwargs):
kwargs['output_attentions'] = True
return original_forward(*args, **kwargs)
# TODO: how to unpatch it?
target_layer.forward = MethodType(patched_forward, target_layer)
Because target_layer.forward has already been replaced by a previous run, the next original_forward is the previous patched_forward, not the model's own method.
What we saw
A long-running ComfyUI (~24h, a few hundred TTS generations) reached:
File ".../alignment_stream_analyzer.py", line 84, in patched_forward
return original_forward(*args, **kwargs)
[Previous line repeated 1479 more times]
RecursionError: maximum recursion depth exceeded
750 occurrences in the log. From the first one onward, every generation returned ~1 second of audio regardless of input length — a 174-character line and a 984-character line produced identically-sized output — while each prompt was still reported as executed successfully (Prompt executed in 0.29 seconds).
Restarting ComfyUI clears it completely: the same 218-character input went from 1.0s to 11.7s of audio immediately after a restart. It then accumulates again with use.
Suggested fix
Make the patch idempotent, and keep a handle so it can be undone:
if not getattr(target_layer, "_asa_patched", False):
original_forward = target_layer.forward
def patched_forward(self, *args, **kwargs):
kwargs['output_attentions'] = True
return original_forward(*args, **kwargs)
target_layer.forward = MethodType(patched_forward, target_layer)
target_layer._asa_original_forward = original_forward
target_layer._asa_patched = True
and restore target_layer.forward = target_layer._asa_original_forward (plus hook_handle.remove()) when the analyzer is finished with. Guarding alone is enough to stop the unbounded growth.
Note for anyone hitting this
The symptom is uniform, very short outputs with successful prompt status. It is worth checking generated duration against expected duration rather than trusting success, because the short clips are valid, decodable, non-silent audio.
Summary
AlignmentStreamAnalyzerpatchesself_attn.forwardevery time it is constructed, capturing the previousforwardasoriginal_forwardand never restoring it. Each construction therefore adds one more layer to the call chain. After enough generations in a single ComfyUI process, calling the layer recurses through every accumulated wrapper and raisesRecursionError.The failure is quiet: generation then returns a very short clip, and ComfyUI still reports the prompt as successful.
Where
engines/chatterbox/models/t3/inference/alignment_stream_analyzer.py(currentmain):Because
target_layer.forwardhas already been replaced by a previous run, the nextoriginal_forwardis the previouspatched_forward, not the model's own method.What we saw
A long-running ComfyUI (~24h, a few hundred TTS generations) reached:
750 occurrences in the log. From the first one onward, every generation returned ~1 second of audio regardless of input length — a 174-character line and a 984-character line produced identically-sized output — while each prompt was still reported as executed successfully (
Prompt executed in 0.29 seconds).Restarting ComfyUI clears it completely: the same 218-character input went from 1.0s to 11.7s of audio immediately after a restart. It then accumulates again with use.
Suggested fix
Make the patch idempotent, and keep a handle so it can be undone:
and restore
target_layer.forward = target_layer._asa_original_forward(plushook_handle.remove()) when the analyzer is finished with. Guarding alone is enough to stop the unbounded growth.Note for anyone hitting this
The symptom is uniform, very short outputs with successful prompt status. It is worth checking generated duration against expected duration rather than trusting success, because the short clips are valid, decodable, non-silent audio.