LTX 2.5 text-only prompt enhancer (TextGenerateLTX2Prompt) silently returns an empty string
- Dominant language
- Python
- Stars
- 133k
- Forks
- 15.7k
- Avg merge
- 1d 6h
- Merged PRs (30d)
- 155
Description
### Expected Behavior
`TextGenerateLTX2Prompt` should return the enhanced caption it generated, for both the text-only (t2v) and image (i2v) paths.
### Actual Behavior
On the **text-only** path the node returns an **empty string**, silently. No error, no warning — the downstream conditioning is simply built from an empty prompt. The image-fed sibling node in the same workflow works fine.
The model is not at fault: it generates a correct, complete caption. The node's own post-processing discards it.
### Steps to Reproduce
1. Load `LTX-2.5_T2V_I2V_Single_Stage_Distilled.json` from the ComfyUI-LTXVideo example workflows (unmodified).
2. Set the "use image input" boolean to `false` so the t2v enhancer (node `5549`, "Positive prompt (t2v) enhancer") drives the graph.
3. Run with any reasonably short prompt.
4. `5549`'s output is `""`. The i2v enhancer (`5546`), which differs **only** by having `image` connected, returns a full caption.
I confirmed from `/history` that both nodes receive byte-identical inputs at the API level — same prompt source node, same CLIP, same seed, same sampling params — the only difference is `image`.
### Root cause 1 — trailing `` deletes the answer
`comfy_extras/nodes_textgen.py`:
```python
text = re.sub(r".*?", "", text, flags=re.DOTALL)
if "" in text: # unclosed/truncated reasoning: keep what follows the last close
text = text.rsplit("", 1)[-1]
```
This assumes reasoning *precedes* the answer. But `TextGenerateLTX2Prompt` primes the model with an **unclosed** channel:
```python
model_open = "" if thinking else "<|channel>final\n"
```
unlike the tokenizer's own template (`comfy/text_encoders/gemma4.py`), which primes a *closed* empty block:
```python
model_open = "" if thinking else "<|channel>thought\n"
```
So the model may emit `` to close the block **after** the caption. `Gemma4SDTokenizer.decode` maps that to `` (`comfy/text_encoders/gemma4.py`), leaving it trailing — and `rsplit("", 1)[-1]` then keeps the empty remainder.
I verified this by replaying the node's exact post-processing chain over a real captured generation: **827 chars in → `""` out**.
### Root cause 2 — `min_length=1` is dropped for Gemma4, so t2v gets ~200 leading pad tokens
`TextGenerate.execute` deliberately passes `min_length=1` (added in 5f211752, "Force min length 1 when tokenizing for text generation"). `SD1Tokenizer.tokenize_with_weights` forwards `**kwargs` correctly, but `Gemma4_Tokenizer.tokenize_with_weights` drops them:
```python
text_tokens = super().tokenize_with_weights(llama_text, return_word_ids)
```
So `min_length` never reaches `sd1_clip.py`, and `self.min_length` is whatever `ltxav_gemma4_tokenizer` forced:
```python
if gemma_tokenizer.min_length == 1:
gemma_tokenizer.min_length = 1024
```
With `pad_left=True`, the pads are inserted **ahead of** `<|turn>system`. Measured with the tokenizer embedded in `gemma4_e2b_it_bf16.safetensors`:
| path | prompt tokens | leading `` inserted |
| --- | --- | --- |
| i2v (`LTX24_I2V_SYSTEM_PROMPT`) | 998 | 26 |
| t2v (`LTX24_T2V_SYSTEM_PROMPT`) | 809 | 215 |
The i2v system prompt is longer (it carries an extra first-frame-grounding section), so it nearly reaches 1024 and is barely padded. The t2v scaffold is 802 tokens with an empty user prompt, so anything short of a ~220-token user prompt gets heavily padded — and that padding is what tips the model into emitting the closing ``.
### Evidence isolating the two causes
Three probes through the generic `TextGenerate` node (which does no regex stripping, so raw markers are visible), same CLIP / seed / sampling as the workflow:
| probe | prompt fed | leading pads | trailing ``? | node would output |
| --- | --- | --- | --- | --- |
| A | exactly what `TextGenerateLTX2Prompt` builds for t2v | 193 | **yes** | `""` |
| B | i2v system prompt, **no image** | 9 | no | full caption |
| C | t2v system prompt + filler past 1024 tokens | 0 | no | full caption |
B rules out the image as the differentiator. A vs C isolates the padding: identical system prompt, and only the padded one emits the closing marker.
### Suggested fix
Two one-liners; either resolves the user-visible bug, both are independently correct.
```diff
--- a/comfy_extras/nodes_textgen.py
+++ b/comfy_extras/nodes_textgen.py
if "" in text: # unclosed/truncated reasoning: keep what follows the last close
- text = text.rsplit("", 1)[-1]
+ head, _, tail = text.rpartition("")
+ # A trailing close (the model shutting the primed channel after the answer)
+ # leaves nothing after it -- keep the answer instead of discarding everything.
+ text = tail if tail.strip() else head
```
```diff
--- a/comfy/text_encoders/gemma4.py
+++ b/comfy/text_encoders/gemma4.py
- text_tokens = super().tokenize_with_weights(llama_text, return_word_ids)
+ text_tokens = super().tokenize_with_weights(llama_text, return_word_ids, **kwargs)
```
Normal conditioning encodes pass no `min_length`, so they keep the 1024 padding — only the text-generation path changes.
With both applied, the t2v enhancer returns a 940-char caption and it propagates to the sampler; the workflow renders normally. Happy to open a PR if you'd like.
Alternatively, priming a *closed* channel (`<|channel>final\n`, matching the tokenizer's own template) would likely remove the trailing marker at the source.
### Version
- ComfyUI **v0.32.0** (`c2bcbecd`); both lines are unchanged on `master` as of today
- Python 3.12.11, torch 2.11.0+cu130, RTX 5090, Linux (WSL2)
- Text encoder: `gemma4_e2b_it_bf16.safetensors` via `CLIPLoader` with `type: ltxv`
Contributor guide
Research direction
Start with the post-processing in comfy_extras/nodes_textgen.py and tokenization in comfy/text_encoders/gemma4.py, then reproduce the issue with the LTX-2.5 example workflow and the t2v enhancer. Compare the text-only and image paths, including the generated markers and leading padding. Done means the t2v node returns its generated caption and the text reaches downstream conditioning without breaking the i2v path.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python, pytorch
- Domain
- ai, backend, machine-learning
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100