No description
Find a file
schattenan aa79d0bd82 Prompt library, per-mode prompt memory and server prompt enhancement
Prompts are no longer transient:
- Generate and Edit each remember their own prompt (and enhancement), so
  moving from a generation to editing its result keeps both texts.
- Every submitted prompt lands in an automatic Recent list; job notes map
  job ids to the user's own prompt + enhancement for restores.
- A prompt library (data_dir()/prompts.json, atomic writes, corrupt files
  set aside, file lock against a second app instance): user categories ->
  prompts with title, description, mode, negative and enhanced text; a
  three-pane library window (categories incl. All/Recent, searchable card
  list with drag-to-category, details editor with dirty tracking; save
  mode with Update vs Save as new; import/export JSON). Opened from two
  icon buttons on the Prompt card (Ctrl+L browse, Ctrl+Shift+S save); Use
  is undoable in the composer.

Prompt enhancement (server feature prompt_rewrite, e8dea8a):
- A separate Enhanced prompt field under the user's prompt, which is never
  overwritten. Enhance (Ctrl+E) queues a rewrite job of exactly the request
  that will be sent (t2i: prompt; edit: base + refs; generate with refs:
  blank canvas + refs + canvas sentence, verified live); the result is tied
  to its source and shows Current / Stale / Enhancing / Failed /
  Unavailable (region edits keep their prompt, per the server).
- Send: [My prompt | Enhanced], per mode. Generate/Regenerate with a
  current enhancement sends it verbatim (regenerate stays cached and fast);
  a stale or missing one is chained: rewrite first, then the image job from
  the click-time snapshot. Hand edits are kept and undoable.
- t2i wh_ratio shows as a clickable aspect suggestion.
- A rewrite unloads the server's image models: reuse baselines on that
  server are dropped ("cache cleared by a prompt rewrite") and the next
  image job is estimated cold (+ reload) and kept out of the timing model.
- Rewrite jobs get their own Queue/History cards (text result, expand,
  copy, use as enhanced prompt) and never go through the image download.

Verified live against the deployed server; 1310 tests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 13:54:53 +02:00
docs Prompt library, per-mode prompt memory and server prompt enhancement 2026-09-23 13:54:53 +02:00
scripts Prompt library, per-mode prompt memory and server prompt enhancement 2026-09-23 13:54:53 +02:00
src/qwen_studio Prompt library, per-mode prompt memory and server prompt enhancement 2026-09-23 13:54:53 +02:00
tests Prompt library, per-mode prompt memory and server prompt enhancement 2026-09-23 13:54:53 +02:00
.gitignore Scaffold: design contract, theme, widgets, data model, conditioning 2026-09-22 21:15:21 +02:00
pyproject.toml Scaffold: design contract, theme, widgets, data model, conditioning 2026-09-22 21:15:21 +02:00
README.md Prompt library, per-mode prompt memory and server prompt enhancement 2026-09-23 13:54:53 +02:00

Qwen Image Studio

A desktop client for qwen-image-server, the REST server around Qwen-Image-2.1. You can:

  • generate images from a prompt,
  • generate with reference images, for example "draw the girl from image 2 in a new scene",
  • edit an image inside rectangular regions, with a visual region editor,
  • keep prompts in a prompt library (your own categories, titles and descriptions) — every prompt you send is remembered in Recent too,
  • enhance a short prompt into a detailed one with the server's prompt rewriter, next to your own text, which is never overwritten,

and follow every job with live progress, an ETA, a queue and the server's history. The client is written in Python with PySide6 (Qt 6) and needs no changes on the server. The deployed server advertises the features that let edits reuse their encoding and rewrite prompts; the client also works with older servers without them (see Server features).

scripts/shoot.py renders screenshots of every main state into shots/ (see Development).

Install

You need Python ≥ 3.10.

cd qwen-image-studio
python -m venv .venv
.venv/bin/pip install -e .            # add [dev] for the tests: pip install -e '.[dev]'

Run

.venv/bin/qwen-image-studio           # or: .venv/bin/python -m qwen_studio

Connecting to the server

The chip at the top right shows the connection. It reads ● comfy · 11.9 GB free · queue 0 in green when the server is fine, amber when it reports degraded (for example the ComfyUI backend is down), and red offline when it cannot be reached. Hover over it for the details, including VRAM and the optional features the server advertises.

Click the chip, or the gear, to open Settings:

  • Server URL. The default is http://192.168.68.73:8500; host:port works too. Test connection checks /healthz in the background (a host that is down takes seconds to fail; the window stays responsive). Saving switches servers at once. Queued or running jobs keep being followed on the server they were sent to, and the History keeps its rows' server until the new list arrives. Until the new server has answered, its features are unknown: a job with Reuse encoding switched off waits for that answer ("Checking server features…").
  • Job polling / health check. How often running jobs and the server status are polled. The server has no push channel, so everything comes from polling.
  • Show results as soon as a job finishes. Switches the stage to the result, unless you are dragging something.
  • Regenerate seed. Random, or Sequential: the first seed the last job's images did not use, to step through neighbouring variations without repeating one.
  • Reuse the input encoding by default. The starting state of the composer's Reuse encoding toggle.
  • History size, result cache. The cache size is shown, with a Clear cache button.
  • File dialogs. Used to open images, add references and save results. System (the default) is your desktop's own file dialog; on KDE Plasma that is Dolphin's dialog, with thumbnails, a preview panel, places and search. Built-in is the app's own dialog: a grid of thumbnails with the folders on top and then the newest images first (handy in folders full of timestamped screenshots), with a detail list one click away. Names too long for three lines are shortened in the middle, so their endings (timestamps, page numbers) stay readable. It remembers the last folder, the view, the sort order and its size. If the desktop's dialog is not available (see Troubleshooting), Settings says so while System is picked, and the built-in one is used.

Files the app keeps:

what where
settings, window layout, last composer controls (including the ones the current mode hides, such as the generate size while editing), the built-in file dialog's folder, view and size ~/.config/qwen-image-studio/settings.ini
downloaded results and job inputs ~/.cache/qwen-image-studio/jobs/
timing history for the ETA ~/.local/share/qwen-image-studio/timing.json
prompt library, Recent prompts, and which prompt was behind each enhanced job ~/.local/share/qwen-image-studio/prompts.json (+ prompts.json.lock, shared by running copies)

Images are never persisted: after a restart the composer has its prompts (both modes, with their enhanced prompts) and settings, but no base image or references.

The window

  • Top bar: Generate / Edit switch, what is being worked on, and the server chip. A thin line under the bar shows the progress of the job on the GPU.
  • Left rail: Input (Open, Paste, Add references). In Edit mode with an image it also has Tools (Select V, Draw R, Undo / Redo) and Overlays (crop windows, model view, labels). View holds fit and zoom. Shortcut tips are at the bottom.
  • Stage: the Canvas (Edit mode: the base image with its regions) and the Result viewer.
  • Side panel: Compose (everything about the next job), Queue (this session's jobs) and History (every job the server knows).

Workflows

Generate

  1. Type a prompt. Generate and Edit each keep their own prompt, so switching modes (or using a result as the new base) never costs you the other one. The composer also has an Enhanced prompt (see Prompt enhancement), a Negative prompt (it uses true CFG and doubles the sampling time; shared by both modes) and a Final prompt preview of exactly what is sent.
  2. In Output, pick an aspect ratio and a pixel budget, or type width and height. The engine uses multiples of 32; the readout shows the real size.
  3. In Sampling, set the steps and the seed. The seed is random by default; the lock keeps a fixed seed, and last: 1234 ⧉ reuses the last job's seed. You can ask for 1–4 images per job; the result viewer names the seed that renders the shown image on its own ("seed 1236 · image 3 of 3").
  4. Press Generate (Ctrl+Enter). The button shows an ETA learned from your earlier jobs.

Generate with reference images

Add up to 9 references in the References card: drop files, paste with Ctrl+Shift+V, or use +. Drag to reorder and hover to remove. Refer to them in the prompt by the number on their badge: "Draw the girl from image 2 …". References larger than 2048 × 2048 px are scaled down when they are added (the server works at a lower resolution anyway); a base image keeps its full size.

The server takes no references in generate mode (it rejects them; older versions dropped them silently). The client therefore sends the job as an edit on a blank white canvas of the chosen size: the canvas is image 1 and the references are images 2…n. The sentence "Image 1 is only a blank canvas that sets the frame; fill it completely." is added to the prompt. Tested on the live server, this puts the referenced subject into a new scene at the canvas size — as an edit, 1024 × 1024 comes back as 992 × 992 and canvases above 2048² px are scaled down; the Output readout and the top bar show the size you will get. The References card says so, and the final-prompt preview shows the added sentence.

Edit with regions

  1. Switch to Edit and open an image: Ctrl+O, drop a file on the canvas, or paste one with Ctrl+V. Transparency is flattened onto white and EXIF rotation is applied, so the canvas shows exactly the pixels the server receives.

  2. Drag on the image to draw regions. You can move them, resize them with the 8 handles (Shift keeps the aspect), nudge them with the arrow keys (Shift ×10), duplicate with Ctrl+D, reorder with [ ] and delete with Del. Ctrl+Z / Ctrl+Shift+Z undo and redo. Ctrl+drag draws over existing regions.

  3. Each region can have its own instruction (double-click the region, or use the Regions card). The prompt is optional when every region has a label.

  4. Choose how the regions reach the model in Mask → Style:

    style what the server sends
    Annotate Image 1 with coloured outlines; the prompt names each box by its colour ("In the red box region: …"). At most 8 regions, one per colour. Every region is drawn in exactly the colour the server names it — with Crop regions that is its colour inside its crop window, since each window restarts at red (the label reads "2 · red · crop 2").
    Mask The original, plus a black-and-white mask as an extra image.
    Paint The regions painted flat white. Good for removing or replacing content.

    The Model view overlay shows image 1 as the server will send it.

  5. Area. Full image edits the whole picture at the Output size (see below). Crop regions instead cuts a context window around each group of nearby regions (context × the region size, at least min size px), edits each window at its own resolution and pastes it back into the full-size page. This is best for small regions on large pages, such as sound effects on a manga page. The Crop windows overlay shows the planned windows with their render sizes, and the Final prompt preview shows one prompt per window. Crop mode renders one image.

  6. Composite original outside regions pastes the original pixels back outside the regions. Feather is the blur of that seam: it blends about 2.5 × feather px outside each region (everything beyond stays pixel-exact) and the region's own edge is only half generated. Expand grows the regions first; the canvas shows both.

Output size. In Edit mode the Output card has the same Pixels choices as Generate (0.5–1.6 MP). The base image keeps its aspect and is scaled down to that budget, never up, so each choice shows the size it will render for your base ("0.75 MP · 704 × 1056"), and the header shows the result. Budgets at or above the base's own size all give the base's own size and are tagged full size. Full size always keeps the base image's own pixels (each side rounded down to a multiple of 32); the server renders at most 4 MP, so for a larger base it reads Max · 4.0 MP and scales it down. Restoring a job brings back exactly the size it was sent with: a job sent with Full size comes back as Full size, and a size that is none of these adds a custom entry with exactly that size, so the restored job still reuses its encoding; it disappears when you pick another size. In crop mode the choice is disabled: every crop window renders at its own size. Every MP figure in the composer, the base image's included, counts 1 MP as 1024 × 1024 px.

The composer checks every server rule before sending: prompt or labels, region limits, ranges, regions inside the image, and images over the server's 64 MB upload limit. Generate stays disabled and the first problem is shown above it.

Working with results

The Result viewer supports:

  • zoom with the wheel, and pan with Space+drag or middle-drag;
  • double-click to toggle fit / 100 %;
  • ← / → between the images of a job;
  • Compare (C) for edits: a before/after split you can drag; hold B to flip to the before image.

The floating toolbar has save (Ctrl+S), copy (Ctrl+C), use as base (continues editing the result; the regions follow it to the new resolution), add as reference, restore settings and rerun.

Restore replaces the composer, so it can be undone from the message it shows. A job from the History downloads its inputs first (from the job card, the message stays until they arrive and has a Cancel). The last restore you clicked wins, and inputs that arrive after you started changing the composer are only applied if you choose Restore anyway.

Prompt library

Prompts are never transient. Three things keep them:

  • Per-mode prompts. Generate and Edit each have their own prompt (and enhanced prompt, and Send choice), with their own undo history. They are saved a moment after every change, so a crash or a logout loses nothing.
  • Recent. Every image job you send is recorded with your own prompt, the negative prompt and — if one existed — its enhanced prompt. A repeat moves to the top with a count. The last 200 are kept.
  • The library. Your own categories of saved prompts, each with a title, an optional description, the prompt, an optional negative and enhanced prompt, and whether it is for Generate, Edit or either.

The two small buttons in the Prompt card's header open the library window:

  • Save prompt (bookmark, Ctrl+Shift+S) opens a draft of the current prompt with a suggested title, in the category you saved into last; pick another category on the left to file it there, and press Enter — a message on the stage says where it went. If the prompt came from the library and is still recognisably that prompt, the button shows a filled bookmark with a check ("Update “…” in the library") and the window offers Update the saved one or Save as new instead of a near-duplicate. Without a prompt the button is disabled.
  • Prompt library (book, Ctrl+L) opens it for browsing: All prompts, Recent and your categories on the left, cards in the middle (title, description, prompt, a Generate / Edit badge, a sparkle when an enhanced prompt is saved, how often and when you used it), and the selected prompt's editable details on the right. Search (Ctrl+F) matches every word in titles, descriptions and all three texts, in the category shown or in all of them; sort by recently used, title or newest. Use (Enter or double-click) loads the prompt into the composer — a Generate or Edit prompt switches to that mode. It replaces what you had there, but the message on the stage has Undo (and Ctrl+Z in the prompt field brings your text back too). Insert (Shift+Enter, or the card's menu) puts it into your prompt at the cursor instead — or after a comma at the end — for style and modifier snippets. A recent prompt can be saved into the library from its details.

Organising: add a category with the folder button, rename it in place (double-click or F2), drag categories to reorder them and drag cards onto a category to move them. Deleting a category asks whether its prompts move to Unsorted (which always exists) or go with it. Ctrl+N starts a new prompt, Ctrl+S saves the details, Del deletes; edits you have not saved are never dropped without asking. Export… writes all categories (or the one shown) to a JSON file, and Import… merges one: categories are matched by name and prompts you already have are skipped, so importing twice adds nothing. The library is one JSON file, written atomically after every change; a file that cannot be read is renamed aside (prompts.json.corrupt-<time>), never overwritten. Two windows (or two running copies of the app) can share it: each change first reads what the other one saved, under a file lock, so neither overwrites the other's prompts. If a write fails (a full disk), a message says so until a later write works; your prompts stay in the window meanwhile.

A saved enhanced prompt remembers which prompt it was made for. Edit the prompt later (in the library, or tweak it in the composer and Update) and the enhancement stays — marked for an earlier prompt — but counts as stale: with Send: Enhanced the composer re-enhances before it sends, so the old text never goes out for the new prompt.

Prompt enhancement

The server (from e8dea8a, feature prompt_rewrite) can run the official Qwen-Image-2.1 prompt rewriter: it turns a short prompt into the long, well-formed kind of prompt the model was trained on. It runs as an ordinary queued job:

  • Generate without references → t2i rewriter (≈ 18–26 s). It reads only the text, and also suggests an aspect ratio: a small "2:3 suggested" chip appears in the Output card; click it to use that aspect at your current pixel budget.
  • Edit, or Generate with references → i2i rewriter (≈ 30–36 s, a few seconds more per extra image). It looks at the images too; with several images the rewritten prompt names them <image1>, <image2>, … in the order they are sent (with one it just says "the input"). The client rewrites exactly the request it will send — the same images in the same order, and for Generate with references the blank canvas and its sentence — and then sends the rewritten text as is.

Your prompt is never overwritten. The rewrite goes into the Enhanced prompt section under your prompt (per mode), where you can edit it (Revert goes back to the rewriter's text). Its chip says whether it is Current (made from this prompt and these images), Stale (you changed the prompt or the images since), the rewrite's progress (its phase and time left, or its place in the queue), or Failed with the server's message. Enhance (Ctrl+E) asks for one; Re-enhance asks for another variant (a new seed gives a different text). A new text replaces edits you made to the old one — the hint warns first, and Ctrl+Z in the field (or Undo on the message) brings them back. Collapse the section and it stays collapsed; the chip keeps reporting.

Send: My prompt | Enhanced picks what Generate sends — for each mode on its own, so enhancing a generation never makes an edit go out rewritten. It switches to Enhanced the first time an enhancement arrives for that mode, and the final-prompt preview shows the text that will go out. When Enhanced is chosen but there is nothing current to send, the collapsed section says Will enhance first and Generate's tooltip says it rewrites first.

  • Enhanced and current: the enhanced text is sent directly. Regenerate stays fast and can reuse the encoding.
  • Enhanced but stale or missing: Generate chains — it queues the rewrite and, once it is done, sends the image job exactly as it was when you clicked (seed included) with the fresh text. The stage shows "Enhancing the prompt…", and the rewrite's queue card says what follows ("↳ Then generate · seed … · 25 steps"); canceling it cancels both. If the rewrite fails, the image job is not sent (the stage says Not sent), and the message offers Send my prompt instead. Clicking Generate twice while the rewrite runs gives two images from one rewrite. If you switch servers while it runs, the image job is not sent to the new server (it was built for the old one); Generate sends it there. Quitting while an image job waits for its rewrite asks first.
  • A rewrite you started earlier that finishes after you restored an enhanced job or loaded a saved enhancement never replaces that one; use its text from its queue card if you want it.

What it costs: the rewrite itself, and more. The server frees the GPU for the rewriter, so the next image job reloads the model (≈ 30 s on the deployed server) and every cached encoding on that server is gone. The reuse chip then says ✎ Changed: cache cleared by a prompt rewrite, and the ETA on the Generate button includes the reload (and, for a chained Generate, the rewrite). Other clients of the same server unload the models with their rewrites too: the app notices when the server's GPU memory is suddenly almost all free (✎ Changed: the server unloaded its models). It cannot tell when another client's image job has already reloaded them, so the ETA may then be ≈ 30 s high.

When it is not available: the server has no rewriter (the section is hidden — unless you have an enhancement from before, which then reads Unavailable and is not sent), the server runs the diffusers backend, the prompt is empty, an edit has no base image yet, or it is a region edit: region prompts name the boxes by colour and the mask by number, which a rewrite would destroy, so they are always sent as written (the server refuses rewrites with regions too).

Rewrite jobs appear in the Queue and History as prompt-rewrite cards (a wand instead of a thumbnail, "prompt rewrite · t2i / i2i · 3 images"), with the rewritten text once done (three lines; click for all of it). Use makes it the enhanced prompt of the mode it was made for (in the History it also puts back the prompt and images it was made from); Copy copies the text. A rewrite that failed offers Retry (a new seed) and Restore (its prompt and images). Image jobs sent with an enhanced prompt keep your prompt as their title in the Queue and History (marked "✦ enhanced"; the History filter finds them by your words too). A job sent with an enhanced prompt is remembered, so Restore puts your own prompt back in the prompt field and the enhancement next to it, with Send: Enhanced; restoring a job sent with your own prompt sets Send: My prompt, so Regenerate repeats it.

Regenerate and encoding reuse

A Qwen-Image job has two halves. Conditioning runs the prompt and the condition images through the Qwen3-VL encoder and the VAE. Sampling runs the denoising loop. On the 12 GB card the encode is the expensive part; the server can cache it, but only if the inputs are exactly the same.

The composer compares what the next job would encode with the newest job whose encoding the server holds — a job that was rejected, canceled before it ran or failed leaves nothing behind, and a new server has nothing yet. This covers the prompt, negative prompt, base image and references (by pixel content), regions and their labels, mask/region mode, crop settings and output resolution. Seed, steps, CFG, image count and composite settings are sampling only. The chip above the Generate button shows the result:

  • ♻ Same inputs — encoding reusable (green): nothing that feeds the encoder changed, and this server can reuse the encoding. Regenerate (Ctrl+R) then sends the composer state with a new seed and re-uploads the same bytes as the last job, so the server's caches hit.
  • = Same inputs — … (grey): nothing changed, but this server encodes such a job again anyway (~10 s) — edits and reference images on a server without reuse_encoding, every edit on the diffusers backend. The chip says which.
  • ✎ Changed: prompt, regions (amber): these inputs may be encoded again (in Generate mode named as you see them: prompt, references, output size; a long list ends in "+3 more", the tooltip has all of it). The server keeps its 64 most recent encodes, so going back to older inputs can still hit — the chip only promises reuse against the newest job. Regenerate is then an ordinary job with a new seed, and its tooltip says what is re-encoded.
  • ✎ Changed: cache cleared by a prompt rewrite (amber): a prompt rewrite ran (or is about to run) on this server, which unloads the model and its cached encodings; the next job reloads first.
  • Fresh encode forced (grey): the Reuse encoding toggle is off. The toggle only shows when the server supports it, and is disabled where it has no effect: a text-only prompt on ComfyUI is served from its cache either way, and the diffusers backend encodes edits anyway.

The ETA on the Generate button follows the same verdict. Job cards show ♻ same inputs when the server can reuse the previous job's encoding, or ♻ encoding reused when the server reports what actually happened. A crop-mode edit runs one backend call per crop window, so its card counts the windows: ♻ reused 2/2, or ♻ reused 1/2 after changing one region's label (only that window is re-encoded). Rerun, on a job card or in the result viewer, re-submits that exact job with a new seed; a job opened from the History downloads its inputs first. After you restore a job, Regenerate continues from it.

Measured on the deployed ComfyUI backend (RTX 5070):

job time
text-to-image 512², 4 steps, cold 34 s
same prompt again 1.0 s
new prompt 2.0 s
832×1216, 25 steps ≈ 32–47 s
1328², 25 steps ≈ 65 s
edit 1 MP + 2 refs, 4 / 8 steps 13.1 s / 16.2 s (≈ 0.75 s per step, ≈ 10 s encode)

Text-to-image always hits ComfyUI's node cache. Edits need the reuse_encoding server feature, which the deployed server has: without it (servers before 9712dd3) condition images are uploaded under random names, so every edit pays the ≈ 10 s encode again, even with identical inputs.

Server features

The server lists features in /healthz. The deployed server (e8dea8a) advertises reuse_encoding, encode_cache_report and prompt_rewrite; older servers advertise fewer or none. The client works fully without them and never sends a field the server did not advertise (the server rejects unknown fields).

feature effect
reuse_encoding The payload accepts reuse_encoding: bool. Condition images are uploaded to ComfyUI under content-addressed names, so identical inputs hit its cache (an LRU of 64 encodes) for edits too. The composer shows the Reuse encoding toggle.
encode_cache_report Jobs report timings.encode_cached, and job cards show the truth ("♻ encoding reused").
prompt_rewrite Jobs of mode rewrite run the prompt rewriter and answer with text. The composer shows the Enhanced prompt section (see Prompt enhancement); without the feature nothing is ever sent as a rewrite.
generate_refs Not provided by any server (qwen-image-server rejects references in generate mode). The client would send a native generate job with references if a server advertised it.

ETA and progress

The deployed ComfyUI backend reports only coarse phases ("Sampling"; a crop job reads as one long "Sampling" too, because the engine overwrites its "crop 2/3" phase). The ETA comes from a small timing model fitted to your own finished jobs of the same kind (mode, number of condition images, cached or not, backend); a job that exactly repeats a finished one (same seed too) is served from the cache in about a second and is not counted. With no data yet it starts from the measurements above. Prompt rewrites are timed on their own (t2i and i2i apart) and never mix with image jobs; the image job after a rewrite reloads the model, so ≈ 30 s is added to its estimate and its time is not learned from. Bars are determinate when the server reports steps (diffusers backend, except multi-crop edits). Otherwise they show elapsed / estimate, capped at 95 %, and "finishing…" past the estimate. Queued jobs show their place in line: the server keeps a job canceled while queued in its queue until the worker skips it, so such slots are left out — in the cards and in the chip's queue count.

Queue and history

  • Queue. One card per job of this session, newest first, with a thumbnail, the prompt, a meta line, a status chip, progress with time left, and the server's error message when a job fails (long ones are cut; the tooltip has all of it). Actions: cancel, open (or click the card), rerun, restore, remove. A running job only stops when the engine next checks: until then the card says canceling…; a cancel the server refuses is reported. Clear finished tidies the list; the server keeps the jobs. Prompt rewrites have cards of their own (see Prompt enhancement).
  • History. GET /v1/jobs from the server, newest first. It refreshes when shown, every 30 s while visible, and on F5. Records and thumbnails load lazily as rows scroll into view. Filter by text (prompt, labels, id, a rewrite's text, and your own prompt behind an enhanced one) or by status, or show only image jobs or only prompt rewrites. Actions: open in the viewer, restore (downloads the job's inputs through the cache), copy the job id; a rewrite's Use and Copy.

Shortcuts

keys action
Ctrl+Enter Generate
Ctrl+R Regenerate: current inputs, new seed
Ctrl+O Open a base image (switches to Edit)
Ctrl+Shift+O Add reference images
Ctrl+V Paste: base image in Edit, reference in Generate (text fields paste text)
Ctrl+Shift+V Paste as a reference
Ctrl+1 / Ctrl+2 Canvas / Result stage (on the focused canvas, Ctrl+1 is 100 %)
Ctrl+Shift+E Switch Generate ↔ Edit
Ctrl+E Enhance the prompt (Re-enhance: a new variant)
Ctrl+Shift+S Save the prompt to the library (or update the saved prompt it came from)
Ctrl+L Prompt library
library: Ctrl+F, Ctrl+N, Ctrl+S, F2, Del, Enter, Shift+Enter Search, new prompt, save, rename category, delete, use, insert at the cursor
Ctrl+, Settings
F11 Full screen
Esc Deselect the region, leave compare, leave full screen
V / R Select / Draw tool
Ctrl+Z, Ctrl+Shift+Z / Ctrl+Y Undo / redo region changes
Ctrl+0, Ctrl++, Ctrl+- Fit, zoom in, zoom out
canvas: Del, arrows, Ctrl+D, [ ] Delete, nudge (Shift ×10), duplicate, reorder the selected region
viewer: ← →, C, hold B, Ctrl+S, Ctrl+C Switch image, compare, flip to before, save, copy

Troubleshooting

  • Chip says offline. Check the URL in Settings (Test connection shows the exact error) and that the server listens on the LAN interface (uvicorn app.server:app --host 0.0.0.0 --port 8500).
  • Chip is amber (degraded). The API server runs, but its backend does not (ComfyUI down). Jobs will queue or fail until it is back.
  • A job fails with "CUDA out of memory". Lower the output size (Pixels) or image count, or use crop mode for small regions on a large page.
  • The first job takes ~30 s longer. The server loads the model lazily on the first job (unless it runs with QIS_PRELOAD=1). Later ones are fast. The same happens to the first image job after a prompt rewrite.
  • Enhancement is greyed out. Its tooltip says why: a region edit, no prompt yet, no base image, or a server on the diffusers backend. Without the rewriter (a server before e8dea8a) the section is not shown at all.
  • "job no longer exists on the server". The job's server no longer knows it: its job directory was wiped, or another server now answers at that address. (Switching servers in Settings does not cause it: jobs are followed on the server they were sent to.)
  • An image will not open. The message appears at the top of the stage. PNG, JPEG, WebP, BMP, GIF and TIFF are supported.
  • Quitting while the server hangs. Requests still waiting on a server that accepts connections but never answers are given 1.5 s; then the app exits anyway instead of lingering in the background.
  • "The prompt library could not be saved". The data folder is full or not writable. Your prompts stay in the window, and every later change tries again; the message goes once a write works.
  • The ETA after another client's rewrite is too long. The app sees that the models were unloaded, but not that another client's job has already reloaded them; the next job simply finishes early.
  • The file dialog is a plain list of names without thumbnails. The pip PySide6 has its own Qt, which cannot load your desktop's Qt integration. The app therefore starts Qt with its desktop-portal theme (QT_QPA_PLATFORMTHEME=xdgdesktopportal), which hands file dialogs to xdg-desktop-portal, so on KDE Plasma you get KDE's own dialog. This needs xdg-desktop-portal plus your desktop's backend (xdg-desktop-portal-kde) running. If QT_QPA_PLATFORMTHEME is already set in your environment, the app keeps your value. Either way, Settings → File dialogs → Built-in gives you a thumbnail grid on any desktop.

Development

.venv/bin/pip install -e '.[dev]'
.venv/bin/python -m pytest              # whole suite, offscreen, never touches the live server
.venv/bin/python scripts/shoot.py       # renders shots/main_*.png for visual review
  • docs/DESIGN.md is the contract every module is built against.
  • The tests run against tests/fake_server.py, an in-process stand-in for the server with the same routes, validation messages and job lifecycle.
  • scripts/shoot.py drives the real main window through its main states against that fake server. It swaps in artwork from ../qwen-image-server/assets and ../bust-calculator/test-image so the screenshots look like real use.

Layout of src/qwen_studio/:

module role
models.py, conditioning.py, planning.py, timing.py, api.py Pure Python: data model, dirty tracking, server rules and sizes, ETA model, HTTP client.
enhance.py, prompt_store.py Pure Python: prompt-enhancement rules (what a rewrite is made from, when it is stale, what is sent); the prompt library, Recent and job notes with their JSON file.
images.py, services.py, settings.py Qt, no widgets: image normalization, the threaded API service, health monitor, job tracker, cache, settings.
mask_canvas.py, region_list.py, compose_panel.py, result_view.py, queue_panel.py, history_panel.py, prompt_library.py The widgets (the last one is the prompt library window).
file_dialogs.py Open/save dialogs: the desktop's own (through the portal), or the built-in thumbnail grid.
theme.py, widgets.py The design system.
main_window.py, app.py Wiring and bootstrap.