fixes #1

Merged
gsalucci merged 1 commits from fix/poster-quality-and-tui-progress into main 2026-08-27 15:36:49 +02:00
7 changed files with 3993 additions and 13 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# Caratteri di terze parti inclusi in pi-imgen
Generato da `scripts/fetch-fonts.sh` il 2026-08-27 07:43 UTC. Non modificare a mano.
Generato da `scripts/fetch-fonts.sh` il 2026-08-27 12:28 UTC. Non modificare a mano.
I file dei caratteri stanno in `vendor/fonts/<id>/` e **non** sono versionati.
Ogni cartella contiene il file `LICENSE` originale.
+6 -1
View File
@@ -724,7 +724,12 @@ export class DrawThingsBackend implements Backend {
this.offline = opts.offline ?? process.env.IMGEN_OFFLINE === "1";
this.timeoutMs = opts.timeoutMs ?? 0;
this.upscaleSteps = opts.upscaleSteps ?? 12;
this.upscaleStrength = opts.upscaleStrength ?? 0.25;
// 0.4 (up from 0.25) is the floor for going from a 768px draft to a 3579px A3 print
// target: at the lower strength SeedVR2 essentially echoes the bicubic resample,
// which is what was shipping as a "sgranata" final poster. 0.4 is past the point
// where the upscaler invents detail instead of restoring it, while still leaving
// the prompt's composition alone.
this.upscaleStrength = opts.upscaleStrength ?? 0.4;
}
// -- probe ---------------------------------------------------------------
+32 -1
View File
@@ -256,15 +256,46 @@ function resolveConfig(deps: LogoDeps): ImgenConfig {
return typeof deps.config === "function" ? deps.config() : deps.config;
}
/** `setStatus` is a no-op on some surfaces; guarded anyway — belt and braces. */
/**
* `setStatus` is a no-op on some surfaces; guarded anyway — belt and braces.
*
* Drives the working spinner too: `setStatus` alone redraws nothing on its own, and a
* generation that updates only the status line looks frozen during a multi-minute model
* run. `setWorkingVisible(true)` keeps the spinner up while we are talking to a model,
* and `setWorkingMessage` is the text it shows next to the spinner dots.
*/
function status(ctx: ExtensionContext, text: string | undefined): void {
try {
ctx.ui.setStatus("imgen", text);
const ui = ctx.ui as unknown as {
setWorkingMessage?: (t: string | undefined) => void;
setWorkingVisible?: (b: boolean) => void;
setWorkingIndicator?: (kind: string | undefined) => void;
};
ui.setWorkingMessage?.(text);
ui.setWorkingVisible?.(Boolean(text));
ui.setWorkingIndicator?.(text ? "dots" : undefined);
} catch {
/* a status line is never worth failing a job over */
}
}
/** Kill the spinner if the command exits through a path that did not already clear it. */
function clearWorking(ctx: ExtensionContext): void {
try {
const ui = ctx.ui as unknown as {
setWorkingVisible?: (b: boolean) => void;
setWorkingIndicator?: (kind: string | undefined) => void;
setWorkingMessage?: (t: string | undefined) => void;
};
ui.setWorkingVisible?.(false);
ui.setWorkingIndicator?.(undefined);
ui.setWorkingMessage?.(undefined);
} catch {
/* ignore */
}
}
function notify(ctx: ExtensionContext, message: string, kind: "info" | "warning" | "error" = "info"): void {
try {
ctx.ui.notify(message, kind);
+73 -10
View File
@@ -172,17 +172,26 @@ const RESULT_MESSAGE = "imgen-poster";
* Draft artwork: small and few steps. Its only job is to let him judge composition and
* colour, and it must come back fast enough that the refinement loop feels like a
* conversation rather than a render farm.
*
* 768px on the long edge — not 512 — because the final pass upsamples this image to
* print resolution. 512px → ~3579px A3 is a 7× stretch through any upscaler, and the
* texture collapses before the upscale can save it. 768px halves that stretch and keeps
* enough pixels (~96px/tile on the WCAG tile grid in contrast.ts) for the contrast
* measurement to read the artwork faithfully instead of returning a flat average.
*/
const DRAFT_LONG_EDGE = 512;
const DRAFT_STEPS = 4;
const DRAFT_LONG_EDGE = 768;
const DRAFT_STEPS = 6;
/**
* Final artwork: the full local cap (see render/formats.ts) and enough steps to hold up in
* print. Reached ONLY when there is no approved draft to enlarge — i.e. the headless
* pipeline. In the interactive command a second generation would be a different picture,
* so the final comes from the draft: see `PaintOptions.from`.
*
* 16 steps (up from 8) is the honest floor for z_image_turbo on a print poster: anything
* lower trades away the texture the upscale is supposed to amplify, not invent.
*/
const FINAL_STEPS = 8;
const FINAL_STEPS = 16;
/** Preview render resolution during the draft loop. A terminal cell is not 300 dpi. */
const DRAFT_PPI = 72;
@@ -263,17 +272,60 @@ function isAbort(e: unknown): boolean {
return e instanceof Error && (e.name === "AbortError" || e.name === "TimeoutError");
}
/** Status line writer that never explodes in a mode without a footer. */
/**
* Status line writer that also drives the working spinner and its message. `setStatus`
* alone redraws nothing on its own; `setWorkingMessage` is what the working indicator
* shows, and `setWorkingVisible(true)` is what forces it to stay up while we are
* talking to a model. The spinner is the thing he sees during a multi-minute diffusion
* run; without it the footer looks frozen between status updates and he does not know
* whether the command is still alive.
*
* Every call is wrapped — `setStatus` and the working-indicator setters are no-ops on
* headless surfaces, and a missing method must never abort a generation.
*/
function statusWriter(ctx: ExtensionContext): (text: string | undefined) => void {
let lastVisible = false;
return (text) => {
try {
ctx.ui.setStatus(STATUS_KEY, text);
const ui = ctx.ui as unknown as {
setWorkingMessage?: (t: string | undefined) => void;
setWorkingVisible?: (b: boolean) => void;
setWorkingIndicator?: (kind: string | undefined) => void;
};
ui.setWorkingMessage?.(text);
const wantVisible = Boolean(text);
if (wantVisible !== lastVisible) {
ui.setWorkingVisible?.(wantVisible);
ui.setWorkingIndicator?.(wantVisible ? "dots" : undefined);
lastVisible = wantVisible;
}
} catch {
/* headless surface without a status bar */
/* headless surface without a footer */
}
};
}
/**
* Make sure the working indicator is OFF when the command returns, no matter which path
* returned. `statusWriter` toggles it on/off on every status call, but any thrown branch
* that bypasses the writer would leave the spinner spinning forever.
*/
function clearWorkingIndicator(ctx: ExtensionContext): void {
try {
const ui = ctx.ui as unknown as {
setWorkingVisible?: (b: boolean) => void;
setWorkingIndicator?: (kind: string | undefined) => void;
setWorkingMessage?: (t: string | undefined) => void;
};
ui.setWorkingVisible?.(false);
ui.setWorkingIndicator?.(undefined);
ui.setWorkingMessage?.(undefined);
} catch {
/* ignore */
}
}
function notify(ctx: ExtensionContext, message: string, kind: "info" | "warning" | "error" = "info"): void {
try {
ctx.ui.notify(message, kind);
@@ -1299,6 +1351,8 @@ export function registerPoster(pi: ExtensionAPI, deps: PosterDeps): void {
} finally {
// Never leave the footer claiming we are still working.
status(undefined);
// And kill the working spinner regardless of which branch returned.
clearWorkingIndicator(ctx);
}
},
});
@@ -1397,7 +1451,7 @@ async function posterCommand(
status(S.progress.draft);
draftArt = await paintArt(deps, job, spec, "draft", {
signal: ctx.signal,
onProgress: (_m, fraction) => status(withPercent(S.progress.generating, fraction)),
onProgress: (message, fraction) => status(withPercent(S.progress.generating, fraction, message)),
onWarning: (w) => notify(ctx, w, "warning"),
});
status(S.progress.typesetting);
@@ -1464,7 +1518,7 @@ async function posterCommand(
status(S.progress.generating);
draftArt = await paintArt(deps, job, spec, "draft", {
signal: ctx.signal,
onProgress: (_m, fraction) => status(withPercent(S.progress.generating, fraction)),
onProgress: (message, fraction) => status(withPercent(S.progress.generating, fraction, message)),
onWarning: (w) => notify(ctx, w, "warning"),
});
repainted = true;
@@ -1524,7 +1578,7 @@ async function posterCommand(
artPath = await paintArt(deps, job, spec, "final", {
signal: ctx.signal,
from: draftArt,
onProgress: (_m, fraction) => status(withPercent(S.progress.finalArt, fraction)),
onProgress: (message, fraction) => status(withPercent(S.progress.finalArt, fraction, message)),
onWarning: (w) => warnings.push(w),
});
} catch (e) {
@@ -1606,8 +1660,17 @@ async function posterCommand(
}
}
/** "Genero l'immagine… 40%" — the label stays Italian, the number is just a number. */
function withPercent(label: string, fraction?: number): string {
/**
* "Genero l'immagine… 40%" — the label stays Italian, the number is just a number.
*
* Takes an optional Italian message from the backend's progress parser — "Decodifica
* dell'immagine…", "Salvataggio…", "Caricamento del modello…" — and prefers it to the
* generic label when present. The progress parser's words are what the user actually
* wants to see during a 90-second diffusion run: a percentage that never moves past 80%
* for a minute, with no other context, looks like a hang.
*/
function withPercent(label: string, fraction: number | undefined, hint?: string): string {
if (hint && hint.trim()) return hint;
if (typeof fraction !== "number" || !Number.isFinite(fraction)) return label;
const pct = Math.max(0, Math.min(100, Math.round(fraction * 100)));
return `${label} ${pct}%`;
+9
View File
@@ -548,6 +548,15 @@ async function socialCommand(
} finally {
// Never leave a status line behind, on any path.
ctx.ui.setStatus(STATUS_KEY, undefined);
// And kill the working spinner regardless of which branch returned.
const ui = ctx.ui as unknown as {
setWorkingVisible?: (b: boolean) => void;
setWorkingIndicator?: (kind: string | undefined) => void;
setWorkingMessage?: (t: string | undefined) => void;
};
ui.setWorkingVisible?.(false);
ui.setWorkingIndicator?.(undefined);
ui.setWorkingMessage?.(undefined);
}
}
+3872
View File
File diff suppressed because it is too large Load Diff
Vendored Executable
BIN
View File
Binary file not shown.