Subtitle Translator (API / Local HF)
There is no API key field, and that is on purpose
- translated_top
- translated_bottom
The display name gives the whole game away: Subtitle Translator (API: edit config.json). You will hunt for an api_key widget, not find one, and assume the node shipped broken. It didn't. The endpoint lives in a file, and the reason is better than the usual "the author got lazy" explanation.
Feed it two subtitle lines, get two translated lines back, wire them into Cinematic Frame & Subtitle so a Chinese line rides under the English original. That's the job: translation is a language model's job, not a diffusion model's, which makes this an "LLM bolted into the graph" node - the model runs alongside generation as a tool, rather than as part of the checkpoint's text encoder.
Where the settings live
Open config.json in the pack folder:
{
"api_base": "http://127.0.0.1:8080",
"api_key": "",
"model": "",
"per_node": {
"FeiFeiSubtitleTranslator": { "api_base": "", "api_key": "", "model": "" }
}
}
Top-level values are the shared endpoint for every LLM node in the pack. A per_node entry overrides them for that node alone, and an empty string inherits - which is the feature worth knowing about here: subtitles are a tiny, fast, boring job, so you can point this node at a small quick model while image captioning uses a heavy vision model on a different host. api_base accepts a bare host (http://127.0.0.1:8080) or a full /v1 URL. The file is re-read on every run, so edits apply on the next queue with no ComfyUI restart.
Why not a node input? Because ComfyUI writes widget values verbatim into every saved workflow and into the metadata of every image you export, stored as a positional array with no field names. A key typed into a panel lands on disk, travels with any workflow you share or PNG you post, and can't be scrubbed afterwards. Keeping it in config.json keeps it out of everything the workflow produces. Confirmed in the shipped code: no LLM node in this pack reads an environment variable - not OPENAI_API_KEY, not OPENAI_BASE_URL, not a proxy var.
That cuts both ways, and the pack says so in the config comments: api_key sits there in plaintext, the file is not gitignored, and if you commit it with a key filled in you've published your key. Use HTTPS for anything that isn't loopback - over plain HTTP the key, your prompts and your images are readable by anything on the path.
The inputs and outputs
text_top and text_bottom are the two lines (multiline boxes, so paste away). target_lang is English or Chinese and defaults to Chinese, which is the sane default if you're writing the top line in English and want the translation underneath. temperature defaults to 0.3 - deliberately low, because translation wants fidelity and a subtitle has no room for creative flourishes. Nudging it past ~0.6 starts producing paraphrases that no longer match the performance on screen; there's very little reason to move it.
Optional: extra_notes for forcing terminology or tone (character names, a period dialect, how long a line may be), and timeout in seconds (default 120, 10–600).
Out come translated_top and translated_bottom, both STRING. Wire them into Cinematic Frame & Subtitle's subtitle_top / subtitle_bottom inputs and you have a bilingual movie still. Under the hood it's a single POST to /v1/chat/completions through the official openai SDK, with a standard-library urllib fallback if the SDK is missing or the client can't be constructed - so a broken proxy setup degrades instead of killing the pack. The system prompt asks for exactly one JSON object, {"top": ..., "bottom": ...}, and the reply is parsed with a brace-balanced extractor that tolerates code fences and stray prose before the JSON.
Failure modes, and they're designed
This node never raises. Failures come back as a string in translated_top:
API Error: both subtitle lines are empty- deliberate; it won't burn a call on a blank frame.API Error: api_base is empty in config.json- the file isn't pointing anywhere.API Error: translator did not return valid JSON: <first 200 chars>- a small local model chatting instead of answering, the classic LLM-in-a-graph failure. Pin it withextra_notes("reply with JSON only"), or use a bigger model.- Connection or timeout errors - check something is actually listening on that host and port, then raise
timeout; the first call after a local model cold-loads is the one that trips 120 seconds.
Every distinct pair of lines is an HTTP round trip, and on a metered cloud endpoint, money. It's a translation, not a live interpreter - keep the lines stable across a batch rather than re-queueing new text for every frame.
Install
ComfyUI Manager → search ComfyUI-FeiFei, or:
cd ComfyUI/custom_nodes
git clone https://github.com/aifeifei798/ComfyUI-FeiFei.git
# restart ComfyUI, nodes appear under the FeiFei category
The pack's only real dependency is openai>=1.40; torch, numpy and Pillow come with ComfyUI. There are no model files to download - you're pointing at someone else's server. A local llama.cpp on :8080 needs no key; for OpenAI, DeepSeek, Moonshot or any other OpenAI-compatible host, set api_base, a real api_key, and model in config.json. And read the paragraph above about not committing that file.
Inputs (7)
| Name | Type | Default | Description |
|---|---|---|---|
| source | COMBO | API (config.json) | API (config.json) sends the request to the endpoint in config.json; Local HF model runs the `model_dir` folder listed in config.json with transformers. |
| text_top | STRING | — | |
| text_bottom | STRING | — | |
| target_lang | COMBO | Chinese | 2 options: English, Chinese |
| temperature | FLOAT | 0.300.1–1.5 | — |
| extra_notesopt | STRING | — | |
| timeoutopt | INT | 12010–600 | API mode only: request timeout in seconds. |
Outputs (2)
| Name | Type | Description |
|---|---|---|
| translated_top | STRING | — |
| translated_bottom | STRING | — |