diff --git a/apps/web/e2e/media-card.spec.ts b/apps/web/e2e/media-card.spec.ts index 1db958f..8938856 100644 --- a/apps/web/e2e/media-card.spec.ts +++ b/apps/web/e2e/media-card.spec.ts @@ -11,6 +11,7 @@ import { type MediaBlock = Record & { kind: "media" }; type Snapshot = { + botId?: string; threadId: string; messages: Array<{ seq: number } & Record>; }; @@ -59,31 +60,36 @@ async function stillPngBase64(page: Page): Promise { /** * Media jobs run on a GPU server or ffmpeg that CI does not have, so the card is seeded: real - * artifacts are uploaded, and the thread snapshot carries a bot message with the job's card. + * artifacts are uploaded, and the bot thread's snapshot (from bootstrap on load, threads/get on + * refresh) carries a bot message with the job's card. */ async function seedMediaMessage(page: Page, botId: string, block: () => MediaBlock) { - await page.route("**/rpc/threads/get", async (route) => { - const fetched = await fetchRpcEnvelope(route); - const snapshot = fetched.body.json as (Snapshot & { botId?: string }) | undefined; - if (snapshot?.botId === botId) { - const seq = Math.max(0, ...snapshot.messages.map((message) => message.seq)) + 1; - snapshot.messages.push({ - id: `media-message-${seq}`, - threadId: snapshot.threadId, - seq, - role: "bot", - botId, - blocks: [block()], - createdAt: new Date().toISOString(), - }); - } - // Fresh headers: the original content length no longer matches the patched body. - await route.fulfill({ - status: fetched.status, - contentType: "application/json", - body: JSON.stringify(fetched.body), + const seed = (snapshot: Snapshot | undefined) => { + if (snapshot?.botId !== botId) return; + const seq = Math.max(0, ...snapshot.messages.map((message) => message.seq)) + 1; + snapshot.messages.push({ + id: `media-message-${seq}`, + threadId: snapshot.threadId, + seq, + role: "bot", + botId, + blocks: [block()], + createdAt: new Date().toISOString(), }); - }); + }; + for (const procedure of ["bootstrap", "threads/get"]) { + await page.route(`**/rpc/${procedure}`, async (route) => { + const fetched = await fetchRpcEnvelope(route); + const json = fetched.body.json as (Snapshot & { thread?: Snapshot }) | undefined; + seed(procedure === "bootstrap" ? json?.thread : json); + // Fresh headers: the original content length no longer matches the patched body. + await route.fulfill({ + status: fetched.status, + contentType: "application/json", + body: JSON.stringify(fetched.body), + }); + }); + } } test("renders a media job card while it runs and when it is done", async ({ page }, testInfo) => { @@ -168,13 +174,7 @@ test("renders a media job card while it runs and when it is done", async ({ page body: JSON.stringify({ json: { jobId: "media-job-2", messageId: "media-message-next" } }), }); }); - const seeded = page.waitForResponse( - async (response) => - response.url().includes("/rpc/threads/get") && - (await response.text()).includes("media-job-1"), - ); await page.reload(); - await seeded; const card = page.getByTestId("media-card"); await expect(card).toBeVisible({ timeout: 30_000 }); @@ -206,6 +206,6 @@ test("renders a media job card while it runs and when it is done", async ({ page botId, messageId: expect.stringMatching(/^media-message-\d+$/), operation: "transcribe", - input: { artifactId: voice.id }, + input: { artifactId: voice.id, title: "voice-note.wav" }, }); }); diff --git a/apps/web/src/components/media/MediaCard.tsx b/apps/web/src/components/media/MediaCard.tsx index 8c319ea..9f0e382 100644 --- a/apps/web/src/components/media/MediaCard.tsx +++ b/apps/web/src/components/media/MediaCard.tsx @@ -32,7 +32,7 @@ const CARD_CLASS = export type MediaCardAction = | { kind: "export"; artifactId: string; preset: MediaPresetId } - | { kind: "transcribe"; artifactId: string } + | { kind: "transcribe"; artifactId: string; name: string } | { kind: "cancel" }; /** Which follow-up jobs the space can run; actions it cannot run are not offered. */ @@ -156,7 +156,9 @@ function OutputActions({ variant="outline" size="sm" disabled={busy} - onClick={() => onAction({ kind: "transcribe", artifactId: output.artifactId })} + onClick={() => + onAction({ kind: "transcribe", artifactId: output.artifactId, name: output.name }) + } > {t`Transcribe`} @@ -378,7 +380,11 @@ export function MediaCard({ operation: "export_video", input: { artifact_id: action.artifactId, presets: [action.preset] }, } - : { operation: "transcribe", input: { artifactId: action.artifactId } }), + : // The new card is titled with the file it transcribes. + { + operation: "transcribe", + input: { artifactId: action.artifactId, title: action.name }, + }), }); }; return ; diff --git a/packages/adapters/src/media/transcription.test.ts b/packages/adapters/src/media/transcription.test.ts index d39c55e..1de999b 100644 --- a/packages/adapters/src/media/transcription.test.ts +++ b/packages/adapters/src/media/transcription.test.ts @@ -201,6 +201,7 @@ describe("transcribe operation", () => { const input = registration.inputSchema.parse({ artifactId: "audio-1", language: "de" }); expect(registration.artifactIds?.(input)).toEqual(["audio-1"]); expect(registration.title(input)).toBe("Transcript"); + expect(registration.title({ ...input, title: "voice-note.wav" })).toBe("voice-note.wav"); expect( registration.inputSchema.safeParse({ artifactId: "audio-1", language: "German!" }).success, ).toBe(false); diff --git a/packages/adapters/src/media/transcription.ts b/packages/adapters/src/media/transcription.ts index a43548c..af28f57 100644 --- a/packages/adapters/src/media/transcription.ts +++ b/packages/adapters/src/media/transcription.ts @@ -146,7 +146,7 @@ export function createTranscriptionOperations( { operation: "transcribe", inputSchema: TranscribeInputSchema, - title: () => "Transcript", + title: (input) => input.title ?? "Transcript", available: async () => Boolean((await deps.loadSettings()).transcription), artifactIds: (input) => [input.artifactId], handler: { diff --git a/packages/contracts/src/media-transcription.ts b/packages/contracts/src/media-transcription.ts index 3d0373a..970c05a 100644 --- a/packages/contracts/src/media-transcription.ts +++ b/packages/contracts/src/media-transcription.ts @@ -9,5 +9,12 @@ export const TranscribeInputSchema = z.object({ .regex(/^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/, "Use a language code such as en or de") .optional() .describe("Spoken language as a code such as en or de; detected when omitted."), + title: z + .string() + .trim() + .min(1) + .max(120) + .optional() + .describe("A few words in the user's language for the media card."), }); export type TranscribeInput = z.infer;