Sign Text Proposer (LM Studio)
What should this sign say? A local vision model answers — no API key needed
- sign_data
- image
- sign_data
- proposed_texts
- report
Once Sign Selector SAM3 has found a region full of fake lettering, somebody has to decide what the sign should say. That's this node's job, and it's the best-named node in the pack: it proposes text. It asks a vision model running in LM Studio what should be on each sign, sends the crop plus the whole scene so the answer fits the setting, and hands the proposals back inside SIGN_DATA for the Detailer to render.
The name is a lie in the most reassuring way - it doesn't call any API and needs no key. It talks plain HTTP to LM Studio's OpenAI-compatible endpoint (default http://localhost:1234/v1) using only the Python standard library. No requests, no other extension, no cloud. If LM Studio is unreachable, that's not an error: the node falls back to your fallback_texts (or existing OCR text) and says so in the report.
How it gets good answers
The model sees the crop and the whole scene by default (context_mode: crop+scene), because a floating rectangle has no idea whether it's a Berlin bakery window or a rural Japanese convenience store. Add a scene_hint ("Berlin, 1985") and you can override the model's read of the setting entirely. one_call_per_cluster keeps a shelf of identical bottles at a single request - only the cluster representative is sent, siblings inherit its text.
The system prompt is a small masterpiece of anti-transcription. Its whole point is that the garbled original is not a starting point: the model must not spell-correct CAFFEE into CAFEE, because a Viennese scene makes that near-miss feel authentic. It answers with strict JSON - text, style, font hint, a legibility score for the original, and confidence - and the Detailer pulls the style field straight into the render prompt. The language follows the scene; a German scene gets German, not a translation.
The one setting that will bite you
temperature belongs at 0.2, exactly. The author measured it: at 0.2 the model never transcribes the garbled original, at 0.25 it does so in half of all runs. It's a cliff, not a slope - choosing the right word for a sign is a low-entropy task that doesn't benefit from sampling variety. The node warns in the report if you go over.
Precedence and controls
manual_override always wins - 3: ACHTUNG per line, using the numbers from the preview - then the model proposal, then fallback_texts, then existing OCR text. avoid_repeats tells the model what wording it already used elsewhere in the picture, so twelve bottles don't all become the same brand; variety_retries re-asks with a different seed when the answer repeats anyway.
Outputs
sign_data with proposals attached - read the returned object, since the node copies the regions rather than mutating the input - plus proposed_texts and a report.
Installing it
The FVMtools clone, and LM Studio with a vision model loaded. That's it. The pack's heavy requirements are for the Face Tools; this node's HTTP client is pure stdlib.
Inputs (20)
| Name | Type | Default | Description |
|---|---|---|---|
| sign_data | SIGN_DATA | Regions from Sign Selector SAM3 | |
| image | IMAGE | The same image the selector scanned — used as scene context | |
| base_url | STRING | http://localhost:1234/v1 | LM Studio OpenAI-compatible endpoint |
| model_id | STRING | Model id as listed by LM Studio. Empty = use whatever is loaded. | |
| enabled | BOOLEAN | true | OFF: skip the model entirely and use overrides plus fallbacks only. |
| context_mode | COMBO | crop+scene | How much the model sees. crop_only is cheapest but invents text that ignores the setting. Neighbours help a row of shopfronts stay coherent. |
| scene_hint | STRING | Overrides the model's read of the setting, e.g. 'Berlin, 1985' or 'rural Japan'. | |
| language | COMBO | auto | Language for the proposed text. 'auto' lets the model follow the scene. |
| temperature | FLOAT | 0.200–2 | Keep at or below 0.2. Measured cliff, not a slope: at 0.2 the model never transcribes the garbled original, at 0.25 it does so in half of all runs — it lands in the near-miss token neighbourhood and returns e.g. 'CAFFEE' because the setting makes that spelling feel authentic. Picking the right word for a sign is a low-entropy task; it does not benefit from sampling variety. |
| max_tokens | INT | 25632–4096 | — |
| seed | INT | 00–18446744073709550000 | Passed through to LM Studio for reproducible proposals |
| one_call_per_cluster | BOOLEAN | true | ON: only the cluster representative is sent; siblings inherit its text. |
| variety_retries | INT | 20–5 | How often to ask again when the answer repeats text already used elsewhere in this picture. 0 = accept the first answer. The ban list alone does not always land — the model will return the same subject with a different price. Each retry says so explicitly and uses a different seed. |
| avoid_repeats | BOOLEAN | true | Tell the model which wording it already used elsewhere in this picture, so similar-looking motifs get different text. Each region is a separate request — without this the model has no memory of its own answers and returns the same name for every bottle on a shelf. Raising temperature would also break the tie, but brings back transcription of the original gibberish, so the variety comes from a constraint instead. Cluster siblings still share their text — this only separates regions that were NOT grouped together. |
| skip_legible | BOOLEAN | false | ON: regions the selector judged already legible are left untouched. |
| timeout | INT | 1205–600 | — |
| manual_override | STRING | One per line, 'index: text' using the numbers from the preview. Example: 3: ACHTUNG 7: Café Mozart Always wins over the model. | |
| fallback_texts | STRING | Used when the model is unreachable or returns nothing. Either 'class: text' lines (sign: OPEN) or a plain list cycled per region. | |
| system_promptopt | STRING | You are a plausible-signage-text engine inside an image-repair pipeline. An AI image generator drew a sign, label, poster, badge, book spine, screen or garment print whose lettering came out as nonsense. You are shown the cropped region first, then the full scene, then (optionally) a few neighbouring regions. Invent the text that SHOULD be there, so it can be re-rendered as real writing. THE RULE THAT MATTERS MOST: the letters in the crop are not words. Do not transcribe them, do not spell-correct them, do not change one letter and keep the rest, do not use them as a starting point. Every word you output must be a correctly spelled dictionary word in a real language, and none of them may be a near-spelling of anything in the crop. Replace every garbled token, not just the first one. Rules: 1. Answer with ONE JSON object and nothing else. No markdown fence, no ```json, no preamble, no explanation, no trailing commentary. 2. The object has exactly these five keys: {"text", "style", "font_hint", "legible_original", "confidence"} Use no other keys and omit none of them. 3. "text" is short — a real sign is a few words, not a sentence — and fits the object class and the scene: a bakery window gets a bakery word, a fire door gets an exit word, a street sign gets a street name. Spell every word out in full, vowels included; never emit an abbreviation, an acronym or a vowel-less consonant skeleton (write "PHARMACY", never "PHRMCY"). If a garbled token cannot become a real word, drop it — a shorter correct sign beats a longer one with nonsense in it. Use "\n" if the region holds more than one line. 4. Never invent or reproduce a real trademark, brand, logotype or company name. Invent neutral, generic wording instead. 5. A scene description or hint constrains only the SETTING, the LANGUAGE and the STYLE. It never tells you the letters, it is never a reason to stay faithful to what is painted there, and it never makes a misspelling acceptable. 6. Match the language of the scene. If the surroundings are German, answer in German; if Japanese, answer in Japanese; and so on. Do not translate to English unless the scene itself is English. 7. "style" describes the sign's visual character in a few words, e.g. "weathered enamel plate, white on dark blue". "font_hint" names a plausible lettering style, e.g. "bold condensed grotesque, all caps" — describe the shapes, do not name a licensed typeface. 8. "legible_original" is a number from 0.0 to 1.0 judging the text ALREADY in the crop: 1.0 means it is clean, real, readable writing that needs no repair; 0.0 means it is AI gibberish — fake letterforms, melted glyphs, nonsense. It scores what you were given; it never licenses copying it. 9. "confidence" is a number from 0.0 to 1.0 stating how sure you are that your proposed "text" fits this scene. Beware the near-miss — it is the artefact you will most often be tempted to keep. A doubled letter or a dropped accent makes a nonsense string look like a real foreign word. It is still nonsense. A foreign, historical or exotic setting never licenses a misspelling: crop "HOTELL BARR" WRONG "HOTELL BAR" RIGHT "HOTEL BAR" crop "PIZZARIA MRKT" WRONG "PIZZARIA MRKT" RIGHT "PIZZERIA" crop "BOKSTOER LIBRARE" WRONG "BOOKSTOER LIBRARE" RIGHT "LIVRARIA" And never blend two languages into a hybrid spelling. Neighbouring languages spell the same everyday shop word differently, and an image generator loves to land halfway between them — that halfway form is a real word in neither language and is exactly the kind of string you must not keep. Choose ONE language that suits the scene and use that language's exact dictionary spelling, accents and all. If the setting makes a hybrid feel authentic, that is the trap, not a licence. Valid answer (structure only — invent your own content; note the correctly spelled, fully accented word): {"text": "BÄCKEREI", "style": "gold leaf on dark green shopfront board", "font_hint": "high-contrast serif, all caps", "legible_original": 0.1, "confidence": 0.8} | System prompt. Must keep demanding a single JSON object. |
| class_instructionsopt | STRING | Per-class extra instruction, 'class: instruction' per line. Example: plate: use a German plate format like B-XY 1234 |
Outputs (3)
| Name | Type | Description |
|---|---|---|
| sign_data | SIGN_DATA | — |
| proposed_texts | STRING | — |
| report | STRING | — |