Merge snake_case media tool inputs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
This commit is contained in:
commit
613cc2ba6a
@ -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, title: "voice-note.wav" },
|
||||
input: { artifact_id: voice.id, title: "voice-note.wav" },
|
||||
});
|
||||
});
|
||||
|
||||
@ -378,7 +378,7 @@ export function MediaCard({
|
||||
: // The new card is titled with the file it transcribes.
|
||||
{
|
||||
operation: "transcribe",
|
||||
input: { artifactId: action.artifactId, title: action.name },
|
||||
input: { artifact_id: action.artifactId, title: action.name },
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
@ -120,7 +120,7 @@ describePostgres("ComfyUI operations in the media runtime (PostgreSQL + ComfyUI
|
||||
const edited = await runtime.start({
|
||||
...scope,
|
||||
operation: "edit_image",
|
||||
input: { artifactId: source, resize: { aspect: "vertical", fit: "blur" }, format: "jpeg" },
|
||||
input: { artifact_id: source, resize: { aspect: "vertical", fit: "blur" }, format: "jpeg" },
|
||||
});
|
||||
await runtime.runJob(edited.jobId);
|
||||
const edit = await runtime.get(edited.jobId);
|
||||
@ -145,14 +145,14 @@ describePostgres("ComfyUI operations in the media runtime (PostgreSQL + ComfyUI
|
||||
runtime.start({
|
||||
...scope,
|
||||
operation: "generate_video",
|
||||
input: { prompt: "animate it", sourceArtifactId: foreign },
|
||||
input: { prompt: "animate it", source_artifact_id: foreign },
|
||||
}),
|
||||
).rejects.toThrow(MediaJobRejectedError);
|
||||
await expect(
|
||||
runtime.start({
|
||||
...scope,
|
||||
operation: "edit_image",
|
||||
input: { artifactId: foreign, rotate: 90 },
|
||||
input: { artifact_id: foreign, rotate: 90 },
|
||||
}),
|
||||
).rejects.toThrow(MediaJobRejectedError);
|
||||
});
|
||||
|
||||
@ -165,7 +165,7 @@ describe("image operations", () => {
|
||||
const { outputs } = await run(
|
||||
operation,
|
||||
"generate_image",
|
||||
{ prompt: "night", sourceArtifactId: "photo-1" },
|
||||
{ prompt: "night", source_artifact_id: "photo-1" },
|
||||
{ "photo-1": photo },
|
||||
);
|
||||
const [upload] = emulator.uploads.values();
|
||||
@ -173,7 +173,7 @@ describe("image operations", () => {
|
||||
expect(outputs[0]).toMatchObject({ width: 1280, height: 832, parentArtifactId: "photo-1" });
|
||||
const clip = await fixture("clip.mp4", 8, 8);
|
||||
await expect(
|
||||
run(operation, "generate_image", { prompt: "x", sourceArtifactId: "c" }, { c: clip }),
|
||||
run(operation, "generate_image", { prompt: "x", source_artifact_id: "c" }, { c: clip }),
|
||||
).rejects.toThrow("The source must be an image");
|
||||
});
|
||||
|
||||
@ -203,7 +203,7 @@ describe("image operations", () => {
|
||||
operation,
|
||||
"edit_image",
|
||||
{
|
||||
artifactId: "photo-1",
|
||||
artifact_id: "photo-1",
|
||||
crop: { left: 0, top: 0, width: 300, height: 300 },
|
||||
rotate: 90,
|
||||
resize: { aspect: "vertical", fit: "blur" },
|
||||
@ -226,12 +226,17 @@ describe("image operations", () => {
|
||||
const titled = await run(
|
||||
operation,
|
||||
"edit_image",
|
||||
{ artifactId: "photo-1", title: "Titelbild: Sommer", rotate: 180 },
|
||||
{ artifact_id: "photo-1", title: "Titelbild: Sommer", rotate: 180 },
|
||||
{ "photo-1": photo },
|
||||
);
|
||||
expect(titled.outputs[0]?.name).toBe("Titelbild Sommer.jpg");
|
||||
await expect(
|
||||
run(operation, "edit_image", { artifactId: "photo-1", prompt: "snow" }, { "photo-1": photo }),
|
||||
run(
|
||||
operation,
|
||||
"edit_image",
|
||||
{ artifact_id: "photo-1", prompt: "snow" },
|
||||
{ "photo-1": photo },
|
||||
),
|
||||
).rejects.toThrow("ComfyUI is not configured");
|
||||
});
|
||||
|
||||
@ -239,7 +244,7 @@ describe("image operations", () => {
|
||||
const { operation, emulator } = setup();
|
||||
const photo = await fixture("photo.png", 300, 200);
|
||||
const input = {
|
||||
artifactId: "photo-1",
|
||||
artifact_id: "photo-1",
|
||||
crop: { left: 0, top: 0, width: 250, height: 200 },
|
||||
prompt: "snow",
|
||||
};
|
||||
@ -268,7 +273,7 @@ describe("helpers", () => {
|
||||
expect(fileStem("夜の海", "video")).toBe("video");
|
||||
expect(
|
||||
editImageTitle({
|
||||
artifactId: "a",
|
||||
artifact_id: "a",
|
||||
crop: { left: 0, top: 0, width: 1, height: 1 },
|
||||
rotate: 90,
|
||||
resize: { aspect: "vertical", fit: "crop" },
|
||||
@ -277,15 +282,15 @@ describe("helpers", () => {
|
||||
).toBe("1×1 · 90° · 9:16 · PNG");
|
||||
expect(
|
||||
editImageTitle({
|
||||
artifactId: "a",
|
||||
artifact_id: "a",
|
||||
flip: "vertical",
|
||||
extend: { top: 4, right: 0, bottom: 0, left: 0 },
|
||||
}),
|
||||
).toBe("↕");
|
||||
expect(
|
||||
editImageTitle({ artifactId: "a", title: "Titelbild", prompt: "snow", rotate: 90 }),
|
||||
editImageTitle({ artifact_id: "a", title: "Titelbild", prompt: "snow", rotate: 90 }),
|
||||
).toBe("Titelbild");
|
||||
expect(editImageTitle({ artifactId: "a", resize: { width: 800, fit: "crop" } })).toBe("800×…");
|
||||
expect(editImageTitle({ artifactId: "a", prompt: "make it snow" })).toBe("make it snow");
|
||||
expect(editImageTitle({ artifact_id: "a", resize: { width: 800, fit: "crop" } })).toBe("800×…");
|
||||
expect(editImageTitle({ artifact_id: "a", prompt: "make it snow" })).toBe("make it snow");
|
||||
});
|
||||
});
|
||||
|
||||
@ -162,8 +162,8 @@ async function generateImage(
|
||||
): Promise<MediaOperationResult> {
|
||||
const input = GenerateImageInputSchema.parse(context.input);
|
||||
const provider = await requireProvider(providers);
|
||||
const source = input.sourceArtifactId
|
||||
? await fetchImage(context, input.sourceArtifactId)
|
||||
const source = input.source_artifact_id
|
||||
? await fetchImage(context, input.source_artifact_id)
|
||||
: undefined;
|
||||
const aspect = input.aspect
|
||||
? presetAspect(input.aspect)
|
||||
@ -177,7 +177,7 @@ async function generateImage(
|
||||
const seed = input.seed ?? randomInt(2 ** 48 - 1);
|
||||
const images = await provider.generateImage({
|
||||
prompt: input.prompt,
|
||||
negativePrompt: input.negativePrompt,
|
||||
negativePrompt: input.negative_prompt,
|
||||
...size,
|
||||
count: input.count,
|
||||
seed,
|
||||
@ -200,8 +200,8 @@ async function generateVideo(
|
||||
): Promise<MediaOperationResult> {
|
||||
const input = GenerateVideoInputSchema.parse(context.input);
|
||||
const provider = await requireProvider(providers);
|
||||
const source = input.sourceArtifactId
|
||||
? await fetchImage(context, input.sourceArtifactId)
|
||||
const source = input.source_artifact_id
|
||||
? await fetchImage(context, input.source_artifact_id)
|
||||
: undefined;
|
||||
const aspect = input.aspect
|
||||
? presetAspect(input.aspect)
|
||||
@ -212,9 +212,9 @@ async function generateVideo(
|
||||
const seed = input.seed ?? randomInt(2 ** 48 - 1);
|
||||
const clips = await provider.generateVideo({
|
||||
prompt: input.prompt,
|
||||
negativePrompt: input.negativePrompt,
|
||||
negativePrompt: input.negative_prompt,
|
||||
...size,
|
||||
durationSeconds: input.durationSeconds,
|
||||
durationSeconds: input.duration_seconds,
|
||||
seed,
|
||||
source: source && {
|
||||
bytes: await coverImage(source.path, size.width, size.height),
|
||||
@ -224,7 +224,7 @@ async function generateVideo(
|
||||
onProgress: reportProgress(context),
|
||||
});
|
||||
await addGeneratedOutputs(context, clips, fileStem(input.prompt, "video"), source?.artifactId);
|
||||
return { summary: `Generated a ${input.durationSeconds} s video with seed ${seed}.` };
|
||||
return { summary: `Generated a ${input.duration_seconds} s video with seed ${seed}.` };
|
||||
}
|
||||
|
||||
function defaultFormat(mimeType: string): ImageFormat {
|
||||
@ -238,7 +238,7 @@ async function editImage(
|
||||
providers: ProviderSource,
|
||||
): Promise<MediaOperationResult> {
|
||||
const input = EditImageInputSchema.parse(context.input);
|
||||
const source = await fetchImage(context, input.artifactId);
|
||||
const source = await fetchImage(context, input.artifact_id);
|
||||
let png: Uint8Array = await editImageGeometry(source.path, input);
|
||||
if (input.prompt) {
|
||||
const provider = await requireProvider(providers);
|
||||
@ -289,8 +289,8 @@ export function createImageOperations(deps: ImageOperationsDeps): MediaOperation
|
||||
};
|
||||
const comfyUiConfigured = async () => Boolean((await deps.loadSettings()).comfyui);
|
||||
const status = async (signal?: AbortSignal) => (await providers())?.gpuStatus(signal);
|
||||
const sourceIds = (input: { sourceArtifactId?: string }) =>
|
||||
input.sourceArtifactId ? [input.sourceArtifactId] : [];
|
||||
const sourceIds = (input: { source_artifact_id?: string }) =>
|
||||
input.source_artifact_id ? [input.source_artifact_id] : [];
|
||||
const image: MediaOperationRegistration<GenerateImageInput> = {
|
||||
operation: "generate_image",
|
||||
inputSchema: GenerateImageInputSchema,
|
||||
@ -315,7 +315,7 @@ export function createImageOperations(deps: ImageOperationsDeps): MediaOperation
|
||||
title: editImageTitle,
|
||||
available: sharpAvailable,
|
||||
handler: { operation: "edit_image", run: (context) => editImage(context, providers) },
|
||||
artifactIds: (input) => [input.artifactId],
|
||||
artifactIds: (input) => [input.artifact_id],
|
||||
};
|
||||
return [image, video, edit] as MediaOperationRegistration[];
|
||||
}
|
||||
|
||||
@ -130,7 +130,7 @@ describePostgres("transcribe through the media runtime (PostgreSQL)", () => {
|
||||
it("turns an audio artifact into subtitle files on the media card", async () => {
|
||||
const ctx = await setup();
|
||||
expect((await ctx.runtime.capabilities(ctx.id)).transcribe).toBe(true);
|
||||
const started = await ctx.start({ artifactId: ctx.source.id });
|
||||
const started = await ctx.start({ artifact_id: ctx.source.id });
|
||||
expect(await ctx.card(started.jobId)).toMatchObject({
|
||||
operation: "transcribe",
|
||||
status: "queued",
|
||||
@ -163,7 +163,9 @@ describePostgres("transcribe through the media runtime (PostgreSQL)", () => {
|
||||
it("refuses an artifact of another user before a job exists", async () => {
|
||||
const owner = await setup();
|
||||
const other = await setup();
|
||||
await expect(other.start({ artifactId: owner.source.id })).rejects.toThrow("Unknown artifact.");
|
||||
await expect(other.start({ artifact_id: owner.source.id })).rejects.toThrow(
|
||||
"Unknown artifact.",
|
||||
);
|
||||
expect(await prisma.mediaJob.count({ where: { userId: other.id } })).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@ -198,18 +198,18 @@ describe("transcribe operation", () => {
|
||||
}),
|
||||
).toBe(false);
|
||||
const { registration } = operation();
|
||||
const input = registration.inputSchema.parse({ artifactId: "audio-1", language: "de" });
|
||||
const input = registration.inputSchema.parse({ artifact_id: "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,
|
||||
registration.inputSchema.safeParse({ artifact_id: "audio-1", language: "German!" }).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("turns an audio artifact into SRT and VTT files next to it", async () => {
|
||||
const { registration, server } = operation();
|
||||
const job = await jobContext(audioSource, { artifactId: "audio-1", language: "de" });
|
||||
const job = await jobContext(audioSource, { artifact_id: "audio-1", language: "de" });
|
||||
const result = await registration.handler.run(job.context);
|
||||
expect(server.requests).toEqual([
|
||||
expect.objectContaining({
|
||||
@ -234,7 +234,7 @@ describe("transcribe operation", () => {
|
||||
|
||||
it("writes a text file when the server returns no timings", async () => {
|
||||
const { registration } = operation(TRANSCRIPTION, { flavor: "text" });
|
||||
const job = await jobContext(audioSource, { artifactId: "audio-1" });
|
||||
const job = await jobContext(audioSource, { artifact_id: "audio-1" });
|
||||
const result = await registration.handler.run(job.context);
|
||||
expect(job.outputs.map((output) => [output.name, output.mimeType])).toEqual([
|
||||
["Interview take 2.txt", "text/plain"],
|
||||
@ -252,7 +252,7 @@ describe("transcribe operation", () => {
|
||||
};
|
||||
const { registration } = operation();
|
||||
await expect(
|
||||
registration.handler.run((await jobContext(video, { artifactId: "video-1" })).context),
|
||||
registration.handler.run((await jobContext(video, { artifact_id: "video-1" })).context),
|
||||
).rejects.toThrow("Transcribing video needs ffmpeg on the server.");
|
||||
const image = {
|
||||
...audioSource,
|
||||
@ -261,12 +261,12 @@ describe("transcribe operation", () => {
|
||||
kind: "image" as const,
|
||||
};
|
||||
await expect(
|
||||
registration.handler.run((await jobContext(image, { artifactId: "image-1" })).context),
|
||||
registration.handler.run((await jobContext(image, { artifact_id: "image-1" })).context),
|
||||
).rejects.toThrow("Only audio and video can be transcribed.");
|
||||
const long = operation(TRANSCRIPTION, { maxBytes: 4 });
|
||||
await expect(
|
||||
long.registration.handler.run(
|
||||
(await jobContext(audioSource, { artifactId: "audio-1" })).context,
|
||||
(await jobContext(audioSource, { artifact_id: "audio-1" })).context,
|
||||
),
|
||||
).rejects.toThrow("too long to transcribe without ffmpeg");
|
||||
});
|
||||
@ -294,7 +294,7 @@ describe("transcribe operation", () => {
|
||||
mimeType: "video/mp4",
|
||||
kind: "video" as const,
|
||||
};
|
||||
const job = await jobContext(video, { artifactId: "video-1" });
|
||||
const job = await jobContext(video, { artifact_id: "video-1" });
|
||||
await registration.handler.run(job.context);
|
||||
expect(extracted).toEqual([
|
||||
{ startMs: 0, endMs: 62_000, mimeType: "video/mp4" },
|
||||
@ -312,7 +312,7 @@ describe("transcribe operation", () => {
|
||||
it("waits for a transcription server that is down instead of failing", async () => {
|
||||
const { registration, server } = operation();
|
||||
server.failNextStatus = 503;
|
||||
const job = await jobContext(audioSource, { artifactId: "audio-1" });
|
||||
const job = await jobContext(audioSource, { artifact_id: "audio-1" });
|
||||
await expect(registration.handler.run(job.context)).rejects.toBeInstanceOf(
|
||||
MediaJobDeferredError,
|
||||
);
|
||||
@ -358,7 +358,7 @@ describe.skipIf(!ffmpeg)("transcribe with ffmpeg", () => {
|
||||
it("transcribes a video's audio as 16 kHz mono WAV", async () => {
|
||||
const { registration, server } = ffmpegOperation();
|
||||
const job = await fakeMediaJobContext(
|
||||
{ artifactId: "video-1" },
|
||||
{ artifact_id: "video-1" },
|
||||
{ "video-1": { path: clips.landscape, name: "landscape.mp4", mimeType: "video/mp4" } },
|
||||
);
|
||||
dirs.push(job.tempDir);
|
||||
@ -374,7 +374,7 @@ describe.skipIf(!ffmpeg)("transcribe with ffmpeg", () => {
|
||||
it("uploads a long recording in ranges below the server's limit", async () => {
|
||||
const { registration, server } = ffmpegOperation(1024);
|
||||
const job = await fakeMediaJobContext(
|
||||
{ artifactId: "audio-1" },
|
||||
{ artifact_id: "audio-1" },
|
||||
{ "audio-1": { path: longTone, name: "long-tone.m4a", mimeType: "audio/mp4" } },
|
||||
);
|
||||
dirs.push(job.tempDir);
|
||||
@ -387,7 +387,7 @@ describe.skipIf(!ffmpeg)("transcribe with ffmpeg", () => {
|
||||
it("refuses a video without sound", async () => {
|
||||
const { registration, server } = ffmpegOperation();
|
||||
const job = await fakeMediaJobContext(
|
||||
{ artifactId: "video-2" },
|
||||
{ artifact_id: "video-2" },
|
||||
{ "video-2": { path: clips.silentPortrait, name: "silent.mp4", mimeType: "video/mp4" } },
|
||||
);
|
||||
dirs.push(job.tempDir);
|
||||
|
||||
@ -148,7 +148,7 @@ export function createTranscriptionOperations(
|
||||
inputSchema: TranscribeInputSchema,
|
||||
title: (input) => input.title ?? "Transcript",
|
||||
available: async () => Boolean((await deps.loadSettings()).transcription),
|
||||
artifactIds: (input) => [input.artifactId],
|
||||
artifactIds: (input) => [input.artifact_id],
|
||||
handler: {
|
||||
operation: "transcribe",
|
||||
async run(context) {
|
||||
@ -156,7 +156,7 @@ export function createTranscriptionOperations(
|
||||
const settings = (await deps.loadSettings()).transcription;
|
||||
if (!settings) throw new MediaJobRejectedError("Transcription is not set up.");
|
||||
const provider = createProvider(settings);
|
||||
const source = await context.fetchArtifact(input.artifactId);
|
||||
const source = await context.fetchArtifact(input.artifact_id);
|
||||
if (source.kind !== "audio" && source.kind !== "video") {
|
||||
throw new MediaJobRejectedError("Only audio and video can be transcribed.");
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ describe("media generation inputs", () => {
|
||||
});
|
||||
expect(GenerateVideoInputSchema.parse({ prompt: "waves" })).toEqual({
|
||||
prompt: "waves",
|
||||
durationSeconds: 5,
|
||||
duration_seconds: 5,
|
||||
});
|
||||
});
|
||||
|
||||
@ -23,20 +23,21 @@ describe("media generation inputs", () => {
|
||||
expect(
|
||||
GenerateImageInputSchema.safeParse({ prompt: "x", width: 512, height: 4096 }).success,
|
||||
).toBe(false);
|
||||
expect(GenerateVideoInputSchema.safeParse({ prompt: "x", durationSeconds: 11 }).success).toBe(
|
||||
expect(GenerateVideoInputSchema.safeParse({ prompt: "x", duration_seconds: 11 }).success).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("needs at least one edit and valid edit values", () => {
|
||||
expect(EditImageInputSchema.safeParse({ artifactId: "a" }).success).toBe(false);
|
||||
expect(EditImageInputSchema.safeParse({ artifactId: "a", rotate: 45 }).success).toBe(false);
|
||||
expect(EditImageInputSchema.safeParse({ artifactId: "a", resize: {} }).success).toBe(false);
|
||||
expect(EditImageInputSchema.safeParse({ artifact_id: "a" }).success).toBe(false);
|
||||
expect(EditImageInputSchema.safeParse({ artifact_id: "a", rotate: 45 }).success).toBe(false);
|
||||
expect(EditImageInputSchema.safeParse({ artifact_id: "a", resize: {} }).success).toBe(false);
|
||||
expect(
|
||||
EditImageInputSchema.safeParse({ artifactId: "a", extend: { top: 4, color: "red" } }).success,
|
||||
EditImageInputSchema.safeParse({ artifact_id: "a", extend: { top: 4, color: "red" } })
|
||||
.success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
EditImageInputSchema.parse({ artifactId: "a", resize: { aspect: "vertical" }, rotate: 90 }),
|
||||
).toEqual({ artifactId: "a", resize: { aspect: "vertical", fit: "crop" }, rotate: 90 });
|
||||
EditImageInputSchema.parse({ artifact_id: "a", resize: { aspect: "vertical" }, rotate: 90 }),
|
||||
).toEqual({ artifact_id: "a", resize: { aspect: "vertical", fit: "crop" }, rotate: 90 });
|
||||
});
|
||||
});
|
||||
|
||||
@ -24,13 +24,13 @@ const Pixels = z.number().int().min(0).max(16_384);
|
||||
export const GenerateImageInputSchema = z
|
||||
.object({
|
||||
prompt: Prompt.describe("What the image shows."),
|
||||
negativePrompt: NegativePrompt,
|
||||
negative_prompt: NegativePrompt,
|
||||
aspect: aspect("Default square, or the source image's aspect ratio.").optional(),
|
||||
width: Side.optional().describe("Exact width in pixels, with height; overrides aspect."),
|
||||
height: Side.optional(),
|
||||
count: z.number().int().min(1).max(MEDIA_IMAGE_MAX_COUNT).default(1),
|
||||
seed: Seed,
|
||||
sourceArtifactId: Id.optional().describe("Image artifact to start from (image-to-image)."),
|
||||
source_artifact_id: Id.optional().describe("Image artifact to start from (image-to-image)."),
|
||||
strength: Strength.describe("With a source: how far to move away from it (default 0.6)."),
|
||||
})
|
||||
.refine((input) => (input.width === undefined) === (input.height === undefined), {
|
||||
@ -41,11 +41,11 @@ export type GenerateImageInput = z.infer<typeof GenerateImageInputSchema>;
|
||||
|
||||
export const GenerateVideoInputSchema = z.object({
|
||||
prompt: Prompt.describe("What happens in the clip: subject, motion, camera."),
|
||||
negativePrompt: NegativePrompt,
|
||||
negative_prompt: NegativePrompt,
|
||||
aspect: aspect("Default landscape, or the start frame's aspect ratio.").optional(),
|
||||
durationSeconds: z.number().min(1).max(MEDIA_VIDEO_MAX_SECONDS).default(5),
|
||||
duration_seconds: z.number().min(1).max(MEDIA_VIDEO_MAX_SECONDS).default(5),
|
||||
seed: Seed,
|
||||
sourceArtifactId: Id.optional().describe("Image artifact to use as the first frame."),
|
||||
source_artifact_id: Id.optional().describe("Image artifact to use as the first frame."),
|
||||
});
|
||||
export type GenerateVideoInput = z.infer<typeof GenerateVideoInputSchema>;
|
||||
|
||||
@ -55,7 +55,7 @@ export type ImageFormat = z.infer<typeof ImageFormatSchema>;
|
||||
/** Edits apply in this order: crop, rotate, flip, resize, extend, repaint, format. */
|
||||
export const EditImageInputSchema = z
|
||||
.object({
|
||||
artifactId: Id.describe("Image artifact to edit."),
|
||||
artifact_id: Id.describe("Image artifact to edit."),
|
||||
title: MediaTitleSchema.optional().describe(
|
||||
"A few words in the user's language for the card and file name.",
|
||||
),
|
||||
|
||||
@ -2,7 +2,7 @@ import * as z from "zod";
|
||||
import { Id } from "./ids.js";
|
||||
|
||||
export const TranscribeInputSchema = z.object({
|
||||
artifactId: Id.describe("Audio or video artifact to transcribe."),
|
||||
artifact_id: Id.describe("Audio or video artifact to transcribe."),
|
||||
language: z
|
||||
.string()
|
||||
.trim()
|
||||
|
||||
@ -5,7 +5,7 @@ import path from "node:path";
|
||||
import type { MediaOperationRegistration } from "@rakazo/adapters";
|
||||
import { ComposioEmulator } from "@rakazo/adapters";
|
||||
import type { MediaJobBlock, MediaSettingsView, MessageBlock } from "@rakazo/contracts";
|
||||
import { MediaOutputSchema } from "@rakazo/contracts";
|
||||
import { TranscribeInputSchema } from "@rakazo/contracts";
|
||||
import { createThreadMessage } from "@rakazo/db";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import type { createApp } from "../../../apps/api/src/app.ts";
|
||||
@ -26,21 +26,21 @@ const PNG_BASE64 = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
||||
);
|
||||
|
||||
// testkit has no zod of its own; the contracts' schemas carry it.
|
||||
const SourceInputSchema = MediaOutputSchema.pick({ artifactId: true });
|
||||
const SourceInputSchema = TranscribeInputSchema.pick({ artifact_id: true });
|
||||
|
||||
/** Fake card operations: an export that copies its source, and a transcription that waits. */
|
||||
function fakeOperations(): MediaOperationRegistration[] {
|
||||
const exportVideo: MediaOperationRegistration<{ artifactId: string }> = {
|
||||
const exportVideo: MediaOperationRegistration<{ artifact_id: string }> = {
|
||||
operation: "export_video",
|
||||
inputSchema: SourceInputSchema,
|
||||
title: () => "Export",
|
||||
available: async () => true,
|
||||
artifactIds: (input) => [input.artifactId],
|
||||
artifactIds: (input) => [input.artifact_id],
|
||||
handler: {
|
||||
operation: "export_video",
|
||||
run: async (context) => {
|
||||
const { artifactId } = context.input as { artifactId: string };
|
||||
const source = await context.fetchArtifact(artifactId);
|
||||
const { artifact_id } = context.input as { artifact_id: string };
|
||||
const source = await context.fetchArtifact(artifact_id);
|
||||
const out = path.join(context.tempDir, "export.png");
|
||||
await writeFile(out, Buffer.from(PNG_BASE64, "base64"));
|
||||
await context.addOutput({
|
||||
@ -54,12 +54,12 @@ function fakeOperations(): MediaOperationRegistration[] {
|
||||
},
|
||||
},
|
||||
};
|
||||
const transcribe: MediaOperationRegistration<{ artifactId: string }> = {
|
||||
const transcribe: MediaOperationRegistration<{ artifact_id: string }> = {
|
||||
operation: "transcribe",
|
||||
inputSchema: SourceInputSchema,
|
||||
title: () => "Transcript",
|
||||
available: async () => true,
|
||||
artifactIds: (input) => [input.artifactId],
|
||||
artifactIds: (input) => [input.artifact_id],
|
||||
handler: {
|
||||
operation: "transcribe",
|
||||
run: async (context) => {
|
||||
@ -260,7 +260,7 @@ describeWithDatabase("media RPCs (PostgreSQL)", () => {
|
||||
botId: alice.bot.id,
|
||||
messageId: alice.card.id,
|
||||
operation: "export_video",
|
||||
input: { artifactId: alice.artifact.id },
|
||||
input: { artifact_id: alice.artifact.id },
|
||||
},
|
||||
);
|
||||
const job = await waitForStatus(started.jobId, "done");
|
||||
@ -305,19 +305,19 @@ describeWithDatabase("media RPCs (PostgreSQL)", () => {
|
||||
botId: alice.bot.id,
|
||||
messageId: alice.card.id,
|
||||
operation: "export_video",
|
||||
input: { artifactId: mallory.artifact.id },
|
||||
input: { artifact_id: mallory.artifact.id },
|
||||
},
|
||||
{
|
||||
botId: mallory.bot.id,
|
||||
messageId: alice.card.id,
|
||||
operation: "export_video",
|
||||
input: { artifactId: mallory.artifact.id },
|
||||
input: { artifact_id: mallory.artifact.id },
|
||||
},
|
||||
{
|
||||
botId: mallory.bot.id,
|
||||
messageId: mallory.card.id,
|
||||
operation: "export_video",
|
||||
input: { artifactId: alice.artifact.id },
|
||||
input: { artifact_id: alice.artifact.id },
|
||||
},
|
||||
{
|
||||
botId: mallory.bot.id,
|
||||
@ -335,7 +335,7 @@ describeWithDatabase("media RPCs (PostgreSQL)", () => {
|
||||
botId: alice.bot.id,
|
||||
messageId: alice.card.id,
|
||||
operation: "transcribe",
|
||||
input: { artifactId: alice.artifact.id },
|
||||
input: { artifact_id: alice.artifact.id },
|
||||
});
|
||||
await waitForStatus(slow.jobId, "running");
|
||||
const cancelled = await rpc<{ status: string }>(app, alice.cookie, "media/jobs/cancel", {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user