How Do You Weight a Prompt, and How Do You Write a Negative?
Forty photographs, 597 generated terms, not one bracket among them — the writer hands you flat weight and leaves every number to you.
The starting point is flat. Across the forty photographs in this site's sample set, the writer emits 597 terms: 557 arrive at exactly 1.00, and the other forty all sit at 9.00 for the same reason — the --ar 16:9 tail is read as a weight. No term in the batch carries a bracket. So nothing in a generated prompt arrives pre-weighted, and every number in yours is one you typed.
Run the range check on those forty lines as they come out and all forty fail, every hit at 9.00 from that same tail. Strip the parameter and forty of forty pass. The lines were never wrong; the checker was reading a parameter.
Negatives get a much smaller surface than the line suggests. Of the 597 terms, four name something a generator could be told to leave out — black and white photograph, on the four files whose monochrome share passed 0.85. The other 593 report a measurement: exposure, contrast, colour temperature, lighting distribution, a vertical, a mood. Negating a report does not change the photograph it reports on, which is why the negative field is where you say what you are adding.
Two runs on the same forty files, the writer at its longest Midjourney setting, then tools/weight-syntax.js. Every count on this page comes from these runs, and every command can be rerun.
Everything below is printed by one file in the library behind this tool, tools/weight-syntax.js. It parses AUTOMATIC1111 weight syntax and rewrites it with the number made visible. You can run every command shown here yourself.
What each mark actually does
Parsing a prompt with all three syntaxes in it — node tools/weight-syntax.js "((a running dog)), [blurry], (sky:1.3)":
1.21 a running dog
0.91 blurry
1.3 sky
| Syntax | Effect | Number | Note |
|---|---|---|---|
(term) | multiply by roughly 1.1 | 1.1 | nesting compounds |
((term)) | two levels of nesting | 1.21 | 1.1 × 1.1 |
[term] | divide by roughly 1.1 | 0.909 | the de-emphasis bracket |
(term:1.3) | set the weight outright | 1.3 | an outer ( ) still multiplies it |
{term} | NovelAI strength mark | — | not reproduced here; the per-level multiplier differs, so no number is invented |
Two terms with no marks at all keep weight 1, and commas are the delimiter — so dog, (cat) parses as two terms, at 1 and 1.1.
Bracket depth, both directions
One level at a time, run through the parser. This is the whole argument for writing numbers:
| Levels | (((term))) | [[[term]]] |
|---|---|---|
| 1 | 1.1 | 0.91 |
| 2 | 1.21 | 0.83 |
| 3 | 1.33 | 0.75 |
| 4 | 1.46 | 0.68 |
| 5 | 1.61 — over 1.6 | 0.62 |
| 6 | 1.77 | 0.56 |
| 7 | 1.95 | 0.51 |
| 8 | 2.14 | 0.47 |
| 9 | 2.36 | 0.42 |
| 10 | 2.59 | 0.39 — under 0.4 |
Five parentheses is a thing people type without thinking about it. It reads as "more emphasis"; it delivers 1.61, one hundredth past the top of the band. Ten brackets reads as "kill this"; it delivers 0.39, one hundredth under the bottom, where the token is ignored entirely. Rounded to two places, both look harmless.
The fix is one flag: --to explicit rewrites the whole prompt with the real number on every term.
$ node tools/weight-syntax.js "((a running dog)), [blurry], (sky:1.3)" --to explicit
(a running dog:1.21), (blurry:0.91), (sky:1.3)
And --check reports anything outside the band, exiting non-zero so you can wire it into a script:
$ node tools/weight-syntax.js "(dog:1.3), [cat]" --check
all weights inside 0.4–1.6
$ node tools/weight-syntax.js "(a:1.9)" --check
out of range: a @ 1.9
Two traps that hide a number you did not write
An outer parenthesis multiplies a weight you already set. (dog:1.3) is 1.3. Wrap it once more — ((dog:1.3)) — and the parser returns 1.43, because the explicit number is the inner level and the outer bracket still applies its 1.1. You asked for 1.3 and got 1.43. If a term already carries a number, leave it bare.
Aspect-ratio parameters are read as weights. The parser splits a term on a trailing colon-number, which is exactly what --ar 16:9 looks like. Run the Midjourney-shaped output through it and the whole tail becomes one term at weight 9:
$ node tools/weight-syntax.js "light falling on the centre with darker edges --ar 16:9"
9 light falling on the centre with darker edges --ar 16
$ node tools/weight-syntax.js "light falling on the centre with darker edges --ar 16:9" --check
out of range: light falling on the centre with darker edges --ar 16 @ 9
Nothing is wrong with the prompt. Strip the trailing parameter before you check weights, or check the terms and leave the parameters out.
One related case: the same word can legitimately carry two different weights at once. blurry, (blurry:1.2) parses to two terms, 1 and 1.2 — which is how you nudge one reading of a word without losing the other.
How to write a negative
The negative field takes the same syntax, and the number means the same thing in both fields: how strongly the term is applied. That single fact decides how a negative should be written.
$ node tools/weight-syntax.js "((blurry)), (watermark:1.4), [[[[[[[[[[text]]]]]]]]]], lowres"
1.21 blurry
1.4 watermark
0.39 text
1 lowres
$ ... --check
out of range: text @ 0.39
Three of those four are fine. The ten-bracket text is not: at 0.39 it sits under the 0.4 floor, and the note in the syntax table for that floor says plainly that below 0.4 the token is ignored. Whoever stacked those brackets was trying to push text out of the frame harder and ended up removing the instruction instead. Suppression is not emphasis with a minus sign — weakening the term weakens what it does.
So the shape of a working negative is short, plain, mostly unweighted:
- List the traits at weight 1.
blurry, watermark, lowres, jpeg artifacts, text, extra fingers. Weight 1 is a complete instruction, not a weak one. - Raise, never lower. If one trait keeps coming back, raise it:
(text:1.4). Bracketing it down moves it toward being ignored. - One change at a time. The debugging checklist behind this tool puts "do not raise several related attributes at once" on the list for a reason: pushing one element too high contaminates its neighbours.
- Check it the same way.
--checkon the negative string catches the 0.39 case above, and--to explicitshows you the number behind every term.
This tool does not write a negative for you. Searching every file in the library — 694 lines of JavaScript, plus the data files and the docs — for the string negative returns zero hits. None of the four output shapes carries one. It is the one part of the prompt you are expected to supply yourself, which is also why it is the part most often written by habit.
What a negative can actually remove: forty lines, 597 terms, four nouns
Run the forty real files used across this site through the writer at the longest Midjourney setting, then split every line at its commas. That is 597 terms: 15 per line on the median, 13 at the fewest, 17 at the most.
593 of them are properties — an exposure phrase, a contrast phrase, a colour temperature, a lighting distribution, a vertical, a mood — and every one of those is the output of a threshold sitting on a number that was measured off the pixels. The remaining four are the only terms in the whole batch that name a thing: the string black and white photograph, on the four files whose monochrome share passed 0.85.
So that is the entire surface a negative has to work with on the writer's side: four terms out of 597. Everything else it could bite lives in the generator, not in the line.
Counting terms is one way to size that surface. Counting words is the other, and it asks the half of the question this section does not: not which terms name a thing you could leave out, but how much of the line a negative can actually take away once the words the composer wrote around them are accounted for, and how many of those namings survive being read back by a weight parser. That measurement, across all four shapes and three lengths, is on how much of a generated line a negative prompt can name.
| Slot | Phrases used | Most common | Files | Terms a negative can name |
|---|---|---|---|---|
| framing | 1 | widescreen 16:9 | 40 | 0 |
| exposure | 4 | moderate, slightly dim lighting | 16 | 0 |
| contrast | 3 | medium contrast | 28 | 0 |
| saturation | 4 | moderately saturated colour | 17 | 0 |
| temperature | 5 | warm golden colour temperature | 12 | 0 |
| detail | 3 | highly detailed with intricate texture | 22 | 0 |
| distribution | 3 | evenly lit | 21 | 0 |
| vertical | 3 | brighter upper half | 22 | 0 |
| mood | 4 | moody and dramatic atmosphere | 16 | 0 |
| subject | 2 | empty on 36 of the 40 | 36 | 4 |
| all nine slots plus subject | 32 | — | 40 | 4 |
Forty real files, writer at the longest Midjourney setting. "Terms a negative can name" counts terms that denote a thing, not terms a negative deletes.
The parser does not know the word either. Hand tools/weight-syntax.js a line with a negative stuck on the end and it returns three tokens, all at weight 1.00 — the --no dark is glued onto the previous term as ordinary text, because the only characters it treats as syntax are (, [, { and an explicit :number:
1.00 widescreen 16:9 composition
1.00 dark
1.00 low-key lighting --no dark
That is not a defect to route around. In the WebUI the negative has its own field, and in Midjourney --no sits at the end of the whole prompt, so in neither case is it a weighted term. What matters is what you aim it at.
Aim it at a phrase the writer has just measured and you are arguing with your own file. On these forty, moderate, slightly dim lighting is what sixteen of them actually measured; --no dim does not make those sixteen brighter, it asks the generator to draw something other than the photograph you fed in. The measurement has already happened, and the line is a report of it.
Three moves that do change the wording, cheapest first:
- Change the number, not the sentence. Crop, lift the exposure, pull the saturation down — the phrase follows the number, and the next line says what you now have.
- Overwrite the one phrase you disagree with and leave its neighbours alone. They are still arithmetic on the same pixels, so they are still true.
- Spend the negative on the subject you are adding yourself — the one slot the writer leaves empty on 36 of these 40 files.
For where each of those phrases comes from, see what an image description generator actually writes; for the arithmetic behind a single word, see what the tool measures.
What you are weighting to begin with
The generator hands you four different shapes, because the four targets genuinely do not accept the same prompt. Midjourney gets a comma list plus a trailing --ar parameter; Stable Diffusion gets the same list with masterpiece, best quality, prepended; Flux and DALL·E get sentences, on the basis that they respond better to sentences than to tag soup.
All of it arrives unweighted. Running the parser over the Stable Diffusion and Flux output for each of the three sample images the site uses — night fireworks, strong backlight, code screenshot:
That flat starting point is only half the story, though: the Stable Diffusion line also spends its two strongest slots on the two fixed quality tags, and three of its phrases carry commas of their own, so twelve generated slots arrive as seventeen separate weighted terms. Which term your bracket actually lands on is the subject of where the weights go on a Stable Diffusion prompt built from an image.
| Sample | Target | Terms | Weight range | --to explicit changes anything |
|---|---|---|---|---|
| night-fireworks | sd | 15 | 1–1 | no |
| night-fireworks | flux | 7 | 1–1 | no |
| strong-backlight | sd | 15 | 1–1 | no |
| strong-backlight | flux | 7 | 1–1 | no |
| text-screenshot | sd | 16 | 1–1 | no |
| text-screenshot | flux | 7 | 1–1 | no |
That also tells you where weighting can land at all. A comma list is a list of terms; a sentence is one term. Putting (navy:1.3) into the middle of a Flux sentence gives the parser something to find and Flux nothing to act on — the shape it was given is prose. Weight the list, not the sentence.
The number the writer emits on its own: 120 of 480 lines, one term each
Almost everything arrives at 1.00, and every exception lives in one shape. Run the forty files in this site's sample set through all four output shapes at all three lengths: 480 lines, 5,046 terms. 4,926 of those terms sit at 1.00. The other 120 — one per Midjourney line, at every length — sit at 9.00. The other three shapes never emit a number at all, on any of their 360 lines:
| Shape | Lines | Terms | Median terms | Terms not at 1.00 | Lines failing --check | --to explicit returns the line unchanged |
|---|---|---|---|---|---|---|
| midjourney | 120 | 1,498 | 13 | 120 | 120 | 0 |
| sd | 120 | 1,738 | 15 | 0 | 0 | 120 |
| flux | 120 | 745 | 6 | 0 | 0 | 120 |
| dalle | 120 | 1,065 | 9 | 0 | 0 | 120 |
What the 9.00 lands on
Not on the parameter. There is no comma in front of --ar 16:9, so the parser welds it to the phrase in front of it and reads the pair as one term — and a term that ends in a colon followed by a number is exactly what it reads as a weight. One real line, 259 characters, read back:
$ node tools/weight-syntax.js "widescreen 16:9 composition, bright, high-key lighting, dominant palette of silver, moss green, cream, grey, muted, desaturated tones, neutral colour temperature, medium contrast, moderate detail and texture, evenly lit, balanced, natural atmosphere --ar 16:9"
1 widescreen 16:9 composition
1 bright
1 high-key lighting
1 dominant palette of silver
1 moss green
1 cream
1 grey
1 muted
1 desaturated tones
1 neutral colour temperature
1 medium contrast
1 moderate detail and texture
1 evenly lit
1 balanced
9 natural atmosphere --ar 16
Across the 120 Midjourney lines the weighted term is the last term in the line 120 times out of 120. It runs a median of 34 characters and 6 words — 18 to 53 characters, 4 to 10 words — which is a median 14.2% of the line, 6.6% at the thinnest and 22.9% at the fattest. The number is not attached to the aspect ratio. It is attached to whichever phrase the writer happened to place last, and that phrase is now reading nine times the rest of the line.
The ratio string itself appears 600 times across the 480 lines, and only 120 of those are read as a number. Every Midjourney line carries 16:9 twice — once inside the framing phrase, where it is mid-term and safe, and once as the tail parameter, where it is at the end of a term and gets read. The other three shapes carry it once each, inside the phrase, and none of their 360 lines produces a weight.
The number is the second half of your aspect ratio
Because the parser takes a trailing colon-number, the weight it invents is exactly the second number of the ratio. Sweep every pixel size from 200×200 to 2000×2000 — 3,243,601 of them — and the ratio strings that come back number 2,064. On all 2,064 the weight read off the tail equals the second number, and that second number takes 60 distinct values from 1 to 60.
Ten of the 2,064 fall inside the 0.4–1.6 band: 1:1 through 10:1, the whole-number panoramas. That is 3,866 of the 3,243,601 sizes, or 0.12%; the remaining 99.88% hand the checker a number you never wrote. Below, the same forty photographs run as Midjourney lines at sixteen real pixel sizes — 640 lines in total:
| Pixel size | --ar tail | Framing phrase | Weight read | Lines out of range | Characters deleted |
|---|---|---|---|---|---|
| 1024×1024 | 1:1 | square 1:1 | 1 | 0 / 40 | 2 |
| 1000×999 | 60:60 | square 1:1 | 60 | 40 / 40 | 3 |
| 1920×960 | 2:1 | widescreen 16:9 | 1 | 0 / 40 | 2 |
| 1200×400 | 3:1 | ultra-wide panoramic | 1 | 0 / 40 | 2 |
| 2560×1080 | 60:25 | ultra-wide panoramic | 25 | 40 / 40 | 3 |
| 3440×1440 | 43:18 | ultra-wide panoramic | 18 | 40 / 40 | 3 |
| 1500×1000 | 3:2 | landscape 3:2 | 2 | 40 / 40 | 2 |
| 2732×2048 | 60:45 | near-square 4:3 | 45 | 40 / 40 | 3 |
| 640×360 | 16:9 | widescreen 16:9 | 9 | 40 / 40 | 2 |
| 800×450 | 16:9 | widescreen 16:9 | 9 | 40 / 40 | 2 |
| 1920×1080 | 16:9 | widescreen 16:9 | 9 | 40 / 40 | 2 |
| 2560×1440 | 16:9 | widescreen 16:9 | 9 | 40 / 40 | 2 |
| 3840×2160 | 16:9 | widescreen 16:9 | 9 | 40 / 40 | 2 |
| 1080×1350 | 4:5 | portrait 3:4 | 5 | 40 / 40 | 2 |
| 1440×2560 | 9:16 | portrait 2:3 | 16 | 40 / 40 | 3 |
| 828×1792 | 28:60 | tall vertical 9:16 | 60 | 40 / 40 | 3 |
The two square rows are the ones to read twice. A 1024×1024 file reduces to 1:1 and the weight read off it is 1 — inside the band, nothing flagged, 0 of 40 lines out of range. A 1000×999 file is the same picture for every purpose that matters, but the ratio routine never returns 1:1 for a near-square; it caps both sides at 60 and returns 60:60, and the weight read off that is 60. Nothing about the photograph changed. Only the two numbers the ratio was written with did.
Being inside the band does not mean the line came back intact. On the three in-band sizes the tail is still edited: --ar 2:1 comes back as the text --ar 2, the :1 having been consumed to make the weight, and --to explicit fails to return the original string on any of those 120 lines. A clean --check reports on the numbers it read. It does not report on the characters it removed.
Stripping the tail fixes it, and costs you no term
Delete --ar 16:9 from the end of the 120 Midjourney lines and check again: 0 out-of-range terms, 120 of 120 lines clean, and --to explicit now returns all 120 unchanged. The term count does not fall on any of them — 0 of 120 — because the parameter was welded to the phrase in front of it rather than separated from it by a comma, so removing it shortens the last term instead of deleting one.
That is the only edit this needs, and it is the one edit the parser cannot make for you: it cannot tell a parameter from a weight, because to it they are the same pattern. Strip the parameters, then check the terms.
Three rules that have nothing to do with brackets
- Weight decays left to right. Order is a weighting decision. Put a landscape tag first and your subject shrinks.
- Do not join concepts with
ANDat equal weight. It tends to fuse them. Comma separation or plain adjacency almost never does. - Do not raise several related attributes at once. One element pushed too high contaminates its neighbours.
All three are in docs/prompt-debugging-checklist.md, which is ordered by how often each one is actually the cause — and weighting sits at position four of nine. Position three is use the participle: run produced no running pose, running produced it immediately. Reach for word form before reaching for a heavier number.
Two things the parser refuses to do
Braces get no invented number, and the check that follows is emptier than it looks. Wrap each term of the forty generated lines in braces and rerun the parser:
- 597 terms wrapped, 597 weights returned as
null, 0 numbers read. --checkon the braced lines: 40 of 40 pass, printing all weights inside 0.4–1.6.--checkon the same forty lines unbraced: 40 of 40 fail, every hit at 9.00 from the--ar 16:9tail.- Strip
--ar 16:9and re-check: 0 out of range, 40 of 40 clean.
A clean pass on a brace prompt means nothing was read. A failure on an unbraced one can mean the checker read a parameter. Both results come from the checker, not from the prompt.
Escaped brackets are literal. A backslash takes the next character at face value: \(dog\) parses to the text (dog) at weight 1. That is how you keep a parenthesis that is part of the subject instead of an instruction.
The routine, in the order it pays off
- Generate first, weight second. The tool's output is already flat at weight 1 — 15 or 16 terms for Stable Diffusion, 7 for Flux.
- Fix wording before numbers. Participle, then order, then weight. Three of the nine checklist items come before weighting.
- Write the number, not the brackets.
(subject:1.3), never(((subject))). - Strip parameters before you check.
--ar 16:9reads as weight 9. - Run
--to explicitand read it. If a number surprises you, it was already in your prompt. - Write the negative plainly and raise it if needed. Plain terms at 1;
(text:1.4)instead of ten brackets.
For the nine-item checklist in full and the order it is meant to be worked in, see fixing a wrong image to prompt output. For where the generated text comes from, see what an image to prompt tool measures, and for turning a picture into something you can paste into a generator, see getting a usable AI image prompt from any picture.