Generate a Tile, the first render job (#39) #89

Merged
crowmaki merged 2 commits from issue-39-generate-tile into main 2026-10-08 05:58:38 +00:00
Owner

Closes #39

What

A Tile can be generated with image.t2i. This is the first job that runs through the app's own render queue.

  • Schema v11:
    • reference_version is rebuilt so blob_id can be NULL. A generated version exists from the moment it is queued, with no pixels until its job finishes. It records prompt, seed, requested_width and requested_height.
    • Triggers keep versions append-only. Pixels can arrive once (NULL → blob) and never change after that.
    • New job table, with one row per ComfyUI submission. It has kind (the workflow id), exactly one target FK (reference_version_id, ON DELETE CASCADE, unique), state, position, attempt, our own UUID prompt_id, graph_blob_id, error and timestamps.
    • The graph is filled when the job is queued and stored as a blob. job.graph_blob_id is added to BLOB_REFERENCES.
  • Queuing:
    • ProjectSession::generate_tile(tile, prompt, seed?) is append-only and not undoable.
    • Shapes are about 1 MP on a ×16 grid: 4:9 (672×1504) for turnaround and mannequin Tiles, the Project ratio for location angles (16:9 → 1328×752), and 1:1 (1008×1008) for Props.
    • Voice sets are refused. Seeds are below 2^53.
  • Current version means the newest version with pixels. A queued or failed version is never locked and never compared with a crop. This holds in Rust (newest_picks, crop plan) and in TS (currentVersion). CONTEXT.md is updated to match.
  • Worker (RenderWorker, one lane, in studio-core):
    • It claims a job, then releases the Project while ComfyUI works.
    • Preflight runs before each submission.
    • It opens /ws?clientId and waits for the greeting, then submits /api/prompt with our prompt_id. A 200 with non-empty node_errors is a failure, and so is a 400.
    • It follows progress, execution_error and execution_interrupted, and stops at executing{node:null}.
    • It then walks every output key in /api/history/{id}, fetches each file from /api/view, and stores it at once.
    • A result for a Project that has since been closed is dropped.
  • ComfyUI client: upload_image (multipart; it uses the name ComfyUI returns), queue_prompt, history, view, and a WebSocket event stream (tungstenite).
  • Fake ComfyUI: /prompt, /upload/image (renames a taken name), /history/<id> and /view. It plays a scripted event sequence to the client that submitted, saves the output image for each SaveImage, and records every submitted graph. Scripts: Succeed, NodeErrors, Reject and ExecutionError.
  • Tauri:
    • Commands generate_tile, queue and follow_queue_progress. The last one sends per-job progress over a channel.
    • The worker's changes are broadcast as changed (new kind queue).
    • Opening a Project wakes the worker.
  • UI:
    • Generate… on image Tile cards, with a multi-line prompt that starts from the last prompt used.
    • Pending versions in the strip show queued, 42% or failed. Provenance reads "generated at W×H, seed N, from …" plus the error, if any.
    • The Queue panel lists failed jobs, then the running job (with a progress bar), then waiting jobs.
    • The status bar shows the queue state, the %, and the failed and waiting counts.

Decisions to check

  • Migrations now run in WAL mode with foreign keys off. schema::migrate turns foreign keys off around the transaction and runs pragma_foreign_key_check before commit. journal_mode=WAL is set before migrating. Both were needed to rebuild a table:

    • The bundled SQLite turns foreign keys on by default, so the DROP TABLE failed.
    • A table renamed in rollback-journal mode left the connection unable to checkpoint, so close() failed with "database table is locked".

    This applies to every future migration.

  • A workflow that is not ready fails its job. #24 says such jobs should wait. That waiting state, together with offline re-probing, is in #53's acceptance criteria.

  • No input uploads yet. image.t2i has no image inputs, so the worker does not upload anything yet. The client method and the fake endpoint exist and are tested. Wiring them to LoadImage slots comes with #51.

  • A job goes with its target. Deleting a Tile cascades to its jobs. If undo puts a generated version back, it shows Failed with "its job was removed".

  • Left for #52/#53:

    • Recovering a job that was running when the app or Project closed. It stays "running" until #53 reattaches it.
    • Cancel, dismissing failed jobs, retries, and a timeout for a stalled WebSocket.

Tests

  • crates/fake-comfy/tests/protocol.rs: the event sequence, history and /view, the scripted failures, and upload renaming.
  • crates/studio-core/tests/tile_generation.rs covers:
    • the pixel-less version and its queued job
    • queue order, prompt ids and seeds
    • the four shapes
    • refusals
    • a pending version not being locked
    • a version put back by undo
  • crates/studio-core/tests/render_worker.rs:
    • It queues a Tile against the fake and asserts that the submitted graph equals the template filled with the prompt, seed, size and prefix. It also asserts that the stored version has the returned pixels.
    • It checks the request order: preflight, then WebSocket, then prompt, then view.
    • It checks the progress events and the order of jobs.
    • Failure cases: node_errors on a 200, a 400, execution_error, a workflow that is not ready (never submitted), and an offline ComfyUI.
    • A Project swapped mid-job.
    • Upload naming.
  • crates/studio-core/tests/migrations.rs: v10 versions and lock picks survive the rebuild, the foreign-key check is clean, and the append-only triggers hold.
  • src-tauri/tests/tile_generation.rs: the adapter round trip against the fake, and a failed job.
  • src/lib/queue/summary.test.ts and the pending-version cases in lock.test.ts.
  • cargo test --workspace --all-features, bun test and svelte-check all pass. Clippy only shows the existing Hooks type-complexity warning.

The UI was checked only by typecheck. It was not checked by hand in the running app.

Closes #39 ## What A Tile can be generated with `image.t2i`. This is the first job that runs through the app's own render queue. - **Schema v11**: - `reference_version` is rebuilt so `blob_id` can be NULL. A generated version exists from the moment it is queued, with no pixels until its job finishes. It records `prompt`, `seed`, `requested_width` and `requested_height`. - Triggers keep versions append-only. Pixels can arrive once (NULL → blob) and never change after that. - New `job` table, with one row per ComfyUI submission. It has `kind` (the workflow id), exactly one target FK (`reference_version_id`, `ON DELETE CASCADE`, unique), `state`, `position`, `attempt`, our own UUID `prompt_id`, `graph_blob_id`, `error` and timestamps. - The graph is filled when the job is queued and stored as a blob. `job.graph_blob_id` is added to `BLOB_REFERENCES`. - **Queuing**: - `ProjectSession::generate_tile(tile, prompt, seed?)` is append-only and not undoable. - Shapes are about 1 MP on a ×16 grid: 4:9 (672×1504) for turnaround and mannequin Tiles, the Project ratio for location angles (16:9 → 1328×752), and 1:1 (1008×1008) for Props. - Voice sets are refused. Seeds are below 2^53. - **Current version** means the newest version *with pixels*. A queued or failed version is never locked and never compared with a crop. This holds in Rust (`newest_picks`, crop plan) and in TS (`currentVersion`). CONTEXT.md is updated to match. - **Worker** (`RenderWorker`, one lane, in `studio-core`): - It claims a job, then releases the Project while ComfyUI works. - **Preflight runs before each submission.** - It opens `/ws?clientId` and waits for the greeting, then submits `/api/prompt` with our `prompt_id`. A 200 with non-empty `node_errors` is a failure, and so is a 400. - It follows `progress`, `execution_error` and `execution_interrupted`, and stops at `executing{node:null}`. - It then walks every output key in `/api/history/{id}`, fetches each file from `/api/view`, and stores it at once. - A result for a Project that has since been closed is dropped. - **ComfyUI client**: `upload_image` (multipart; it uses the name ComfyUI returns), `queue_prompt`, `history`, `view`, and a WebSocket event stream (`tungstenite`). - **Fake ComfyUI**: `/prompt`, `/upload/image` (renames a taken name), `/history/<id>` and `/view`. It plays a scripted event sequence to the client that submitted, saves the output image for each `SaveImage`, and records every submitted graph. Scripts: `Succeed`, `NodeErrors`, `Reject` and `ExecutionError`. - **Tauri**: - Commands `generate_tile`, `queue` and `follow_queue_progress`. The last one sends per-job progress over a channel. - The worker's changes are broadcast as `changed` (new kind `queue`). - Opening a Project wakes the worker. - **UI**: - *Generate…* on image Tile cards, with a multi-line prompt that starts from the last prompt used. - Pending versions in the strip show *queued*, *42%* or *failed*. Provenance reads "generated at W×H, seed N, from …" plus the error, if any. - The Queue panel lists failed jobs, then the running job (with a progress bar), then waiting jobs. - The status bar shows the queue state, the %, and the failed and waiting counts. ## Decisions to check - **Migrations now run in WAL mode with foreign keys off.** `schema::migrate` turns foreign keys off around the transaction and runs `pragma_foreign_key_check` before commit. `journal_mode=WAL` is set before migrating. Both were needed to rebuild a table: - The bundled SQLite turns foreign keys on by default, so the `DROP TABLE` failed. - A table renamed in rollback-journal mode left the connection unable to checkpoint, so `close()` failed with "database table is locked". This applies to every future migration. - **A workflow that is not ready fails its job.** #24 says such jobs should wait. That waiting state, together with offline re-probing, is in #53's acceptance criteria. - **No input uploads yet.** `image.t2i` has no image inputs, so the worker does not upload anything yet. The client method and the fake endpoint exist and are tested. Wiring them to `LoadImage` slots comes with #51. - **A job goes with its target.** Deleting a Tile cascades to its jobs. If undo puts a generated version back, it shows *Failed* with "its job was removed". - **Left for #52/#53:** - Recovering a job that was running when the app or Project closed. It stays "running" until #53 reattaches it. - Cancel, dismissing failed jobs, retries, and a timeout for a stalled WebSocket. ## Tests - `crates/fake-comfy/tests/protocol.rs`: the event sequence, history and `/view`, the scripted failures, and upload renaming. - `crates/studio-core/tests/tile_generation.rs` covers: - the pixel-less version and its queued job - queue order, prompt ids and seeds - the four shapes - refusals - a pending version not being locked - a version put back by undo - `crates/studio-core/tests/render_worker.rs`: - It queues a Tile against the fake and asserts that the **submitted graph** equals the template filled with the prompt, seed, size and prefix. It also asserts that the **stored version** has the returned pixels. - It checks the request order: preflight, then WebSocket, then prompt, then view. - It checks the progress events and the order of jobs. - Failure cases: `node_errors` on a 200, a 400, `execution_error`, a workflow that is not ready (never submitted), and an offline ComfyUI. - A Project swapped mid-job. - Upload naming. - `crates/studio-core/tests/migrations.rs`: v10 versions and lock picks survive the rebuild, the foreign-key check is clean, and the append-only triggers hold. - `src-tauri/tests/tile_generation.rs`: the adapter round trip against the fake, and a failed job. - `src/lib/queue/summary.test.ts` and the pending-version cases in `lock.test.ts`. - `cargo test --workspace --all-features`, `bun test` and `svelte-check` all pass. Clippy only shows the existing `Hooks` type-complexity warning. The UI was checked only by typecheck. It was not checked by hand in the running app.
A Tile can be generated with image.t2i through the app's own render queue.

- Schema v11: a generated Reference version exists without pixels from the
  moment it is queued and records its prompt, seed and size; a job table
  holds one row per ComfyUI submission with its filled graph as a blob.
- Tiles generate at about 1 MP: 4:9 for turnaround and mannequin Tiles,
  the Project ratio for location angles, 1:1 for Props.
- A render worker with one ComfyUI lane runs preflight before each
  submission, submits under our own UUID prompt_id (a 200 with node_errors
  is a failure), follows WebSocket progress and stores the outputs from
  history and /view at once.
- The fake ComfyUI answers /prompt, /upload/image, /history and /view and
  plays scripted event sequences, recording each submitted graph.
- Queue panel, status-bar indicator and per-job progress over a channel;
  Generate on Tile cards with pending and failed placeholders.
- Migrations run in WAL mode with foreign keys off and are checked with
  foreign_key_check before commit, so tables can be rebuilt.
- Job view types move to job_model.rs, as other view types live in *_model.rs.
- One shipped_workflow(id) lookup replaces two copies.
- DTOs spell job and generation states with as_str, like the other DTOs.
- A generated version that undo puts back without its job says why it
  will not get pixels.
- CONTEXT.md records pixel-less generated versions and that a Tile's
  current version is its newest one with pixels.
crowmaki deleted branch issue-39-generate-tile 2026-10-08 05:58:39 +00:00
Sign in to join this conversation.
No description provided.