Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/core/edit-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ export class Edit {
const { options } = asset;
const config: GenerationConfig = {
clipId,
asset,
type,
...(typeof asset["model"] === "string" ? { model: asset["model"] } : {}),
options: typeof options === "object" && options !== null && !Array.isArray(options) ? (options as Record<string, unknown>) : {},
Expand Down
6 changes: 4 additions & 2 deletions src/core/generation/asset-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,10 @@ export class AssetGenerator {
};
try {
const result = this.statusProvider({ ...structuredClone(config), signal: controller.signal });
if (result instanceof Promise) result.then(settle, fail);
else settle(result);
if (result instanceof Promise) {
if (this.status?.value.tone !== "error") this.setStatus({ clipId: config.clipId, value: { text: "Checking generation…", pending: true } });
result.then(settle, fail);
} else settle(result);
} catch (error) {
fail(error);
}
Expand Down
2 changes: 2 additions & 0 deletions src/core/generation/generation-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Edit } from "@core/edit-session";

export type GenerationConfig = {
clipId: string;
asset: Record<string, unknown>;
type: "image" | "video" | "audio";
model?: string;
options: Record<string, unknown>;
Expand All @@ -11,6 +12,7 @@ export type GenerationConfig = {

export type GenerationStatus = {
text: string;
pending?: boolean;
tone?: "neutral" | "warning" | "error";
onActivate?: () => void;
};
Expand Down
5 changes: 3 additions & 2 deletions src/core/ui/generate-toolbar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,8 @@ export class GenerateToolbar extends BaseToolbar {
if (!clipId) return;
if ((this.promptInput?.value ?? "").trim() === "") return;
if (this.edit.getClipGenerationState(clipId)?.status === "generating") return;
if (this.edit.getGenerationStatus(clipId)?.tone === "error") return;
const status = this.edit.getGenerationStatus(clipId);
if (status?.pending || status?.tone === "error") return;
// A generation failure surfaces as clip state; a rejection means the clip could not be
// generated at all — no handler registered, or nothing on the asset to generate from.
this.edit.generateClip(clipId).catch((error: unknown) => {
Expand Down Expand Up @@ -354,7 +355,7 @@ export class GenerateToolbar extends BaseToolbar {
const generating = state?.status === "generating";
const hasPrompt = (this.promptInput?.value ?? "").trim() !== "";
const label = this.generateBtn.querySelector("[data-generate-label]");
const blocked = status?.tone === "error";
const blocked = status?.pending || status?.tone === "error";
this.generateBtn.disabled = generating || !hasPrompt || missing.length > 0 || blocked;
this.generateBtn.classList.toggle("is-generating", generating);
if (label) {
Expand Down
25 changes: 23 additions & 2 deletions tests/edit-clip-operations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1845,8 +1845,29 @@ describe("Edit Clip Operations", () => {
});
e.selectClip(0, 0);
expect(configs(status)).toEqual([
{ clipId: e.getClipId(0, 0), type: "audio", model: "polly-neural", options: { voice: "Matthew" }, length: 4, prompt: "hello" }
{
asset: expect.objectContaining({ type: "audio", prompt: "hello", model: "polly-neural", options: { voice: "Matthew" } }),
clipId: e.getClipId(0, 0),
type: "audio",
model: "polly-neural",
options: { voice: "Matthew" },
length: 4,
prompt: "hello"
}
]);
});

it("passes the complete resolved asset for quoting and refreshes legacy voice changes", async () => {
const { e, status } = await promptEdit({ asset: { type: "text-to-speech", text: "{{ WORDS }}", voice: "Joanna" } }, [
{ find: "WORDS", replace: "hello" }
]);
e.selectClip(0, 0);
const clipId = e.getClipId(0, 0)!;
expect(status.mock.calls.at(-1)?.[0]).toMatchObject({ asset: { type: "text-to-speech", text: "hello", voice: "Joanna" } });
const previous = status.mock.calls.at(-1)![0];
await e.updateClipById(clipId, { asset: { type: "text-to-speech", text: "{{ WORDS }}", voice: "Matthew" } } as never);
expect(previous.signal.aborted).toBe(true);
expect(status.mock.calls.at(-1)?.[0]).toMatchObject({ asset: { voice: "Matthew", text: "hello" } });
});

it("resolves merge fields in the prompt", async () => {
Expand Down Expand Up @@ -1955,7 +1976,7 @@ describe("Edit Clip Operations", () => {
const { e, status } = await promptEdit({ asset: { type: "image", prompt: "a cat" } });
status.mockResolvedValue({ text: "later" });
e.selectClip(0, 0);
expect(e.getGenerationStatus(e.getClipId(0, 0) ?? "")).toBeUndefined();
expect(e.getGenerationStatus(e.getClipId(0, 0) ?? "")).toMatchObject({ pending: true });
await flush();
expect(e.getGenerationStatus(e.getClipId(0, 0) ?? "")).toEqual({ text: "later" });
});
Expand Down
11 changes: 4 additions & 7 deletions tests/generate-toolbar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,10 +195,7 @@ describe("GenerateToolbar", () => {
expect(container.querySelector("[data-model-label]")?.textContent).toBe("Select model");
expect(container.querySelector<HTMLButtonElement>("[data-options-picker]")?.disabled).toBe(true);
container.querySelector<HTMLButtonElement>("[data-model-picker]")?.click();
expect([...container.querySelectorAll("[data-model-value]")].map(node => node.textContent)).toEqual([
"flux-schnell",
"nano-banana-2"
]);
expect([...container.querySelectorAll("[data-model-value]")].map(node => node.textContent)).toEqual(["flux-schnell", "nano-banana-2"]);
expect(container.textContent).not.toContain("Automatic");
toolbar.dispose();
});
Expand Down Expand Up @@ -603,7 +600,7 @@ describe("GenerateToolbar", () => {
toolbar.dispose();
});

it("keeps Generate and Enter blocked until an error status refresh settles", async () => {
it.each([true, false])("keeps Generate and Enter blocked until an async status settles (previous error: %s)", async previousError => {
const edit = createMockEdit();
let generations = 0;
const generator = new AssetGenerator({
Expand All @@ -627,8 +624,8 @@ describe("GenerateToolbar", () => {
const pending = new Promise<GenerationStatus | undefined>(resolve => {
resolveStatus = resolve;
});
generator.registerStatus(({ prompt }) => (prompt === "a cat" ? { text: "Insufficient credits", tone: "error" } : pending));
const config = { clipId: "clip-1", type: "image" as const, options: {}, length: 4, prompt: "a cat" };
generator.registerStatus(({ prompt }) => (previousError && prompt === "a cat" ? { text: "Insufficient credits", tone: "error" } : pending));
const config = { asset: { type: "image", prompt: "a cat" }, clipId: "clip-1", type: "image" as const, options: {}, length: 4, prompt: "a cat" };
generator.describe(config);
const { toolbar, container } = mountToolbar(edit);
const button = container.querySelector<HTMLButtonElement>("[data-action='generate']")!;
Expand Down
Loading