Merge image edit titles in the user's language
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
This commit is contained in:
commit
6d7d46024d
@ -223,6 +223,13 @@ describe("image operations", () => {
|
||||
expect((await sharp(outputs[0]!.path).metadata()).format).toBe("webp");
|
||||
expect(result.summary).toBe("Edited photo.jpg into photo-edit.webp (1080×1920).");
|
||||
expect(emulator.requests).toHaveLength(0);
|
||||
const titled = await run(
|
||||
operation,
|
||||
"edit_image",
|
||||
{ artifactId: "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 }),
|
||||
).rejects.toThrow("ComfyUI is not configured");
|
||||
@ -267,7 +274,17 @@ describe("helpers", () => {
|
||||
resize: { aspect: "vertical", fit: "crop" },
|
||||
format: "png",
|
||||
}),
|
||||
).toBe("crop · 90° · 9:16 · PNG");
|
||||
).toBe("1×1 · 90° · 9:16 · PNG");
|
||||
expect(
|
||||
editImageTitle({
|
||||
artifactId: "a",
|
||||
flip: "vertical",
|
||||
extend: { top: 4, right: 0, bottom: 0, left: 0 },
|
||||
}),
|
||||
).toBe("↕");
|
||||
expect(
|
||||
editImageTitle({ artifactId: "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");
|
||||
});
|
||||
|
||||
@ -36,6 +36,7 @@ import {
|
||||
imageSize,
|
||||
sharpAvailable,
|
||||
} from "./image-edit.js";
|
||||
import { cleanFileName } from "./video-operations.js";
|
||||
|
||||
/** Free GPU memory a generation needs before it starts; fixed, so a custom workflow cannot lower it. */
|
||||
export const IMAGE_GENERATION_MIN_VRAM_MB = 6_000;
|
||||
@ -75,19 +76,22 @@ export function fileStem(text: string, fallback: string): string {
|
||||
return words?.slice(0, 6).join("-").slice(0, 48).replace(/-+$/, "") || fallback;
|
||||
}
|
||||
|
||||
/** Short card title for an edit without a prompt, such as "crop · 9:16 · PNG". */
|
||||
/**
|
||||
* Card title: the bot's title or repaint prompt, else language-neutral tokens for what the edit
|
||||
* does, such as "300×300 · 90° · ↔ · 9:16 · PNG".
|
||||
*/
|
||||
export function editImageTitle(input: EditImageInput): string {
|
||||
if (input.title) return input.title;
|
||||
if (input.prompt) return input.prompt;
|
||||
const { resize } = input;
|
||||
const { crop, resize } = input;
|
||||
const parts = [
|
||||
input.crop && "crop",
|
||||
crop && `${crop.width}×${crop.height}`,
|
||||
input.rotate && `${input.rotate}°`,
|
||||
input.flip && "flip",
|
||||
input.flip && (input.flip === "horizontal" ? "↔" : "↕"),
|
||||
resize &&
|
||||
(resize.aspect
|
||||
? MEDIA_PRESETS[resize.aspect].aspect
|
||||
: `${resize.width ?? "…"}×${resize.height ?? "…"}`),
|
||||
input.extend && "extend",
|
||||
input.format?.toUpperCase(),
|
||||
];
|
||||
return parts.filter(Boolean).join(" · ");
|
||||
@ -256,8 +260,10 @@ async function editImage(
|
||||
const format = input.format ?? defaultFormat(source.mimeType);
|
||||
const encoded = await encodeImage(png, format);
|
||||
const mimeType = IMAGE_FORMAT_MIME_TYPES[format];
|
||||
const stem = source.name.replace(/\.[^.]+$/, "") || "image";
|
||||
const name = `${stem}-edit${attachmentExtensionForMimeType(mimeType)}`;
|
||||
const stem = input.title
|
||||
? cleanFileName(input.title, "image")
|
||||
: `${cleanFileName(source.name.replace(/\.[^.]+$/, ""), "image")}-edit`;
|
||||
const name = `${stem}${attachmentExtensionForMimeType(mimeType)}`;
|
||||
const path = join(context.tempDir, name);
|
||||
await writeFile(path, encoded.bytes);
|
||||
await context.addOutput({
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import * as z from "zod";
|
||||
import { Id } from "./ids.js";
|
||||
import { MediaFitSchema, MediaPresetIdSchema } from "./media.js";
|
||||
import { MediaTitleSchema } from "./media-video.js";
|
||||
|
||||
export const MEDIA_IMAGE_MAX_COUNT = 4;
|
||||
export const MEDIA_VIDEO_MAX_SECONDS = 10;
|
||||
@ -55,6 +56,9 @@ export type ImageFormat = z.infer<typeof ImageFormatSchema>;
|
||||
export const EditImageInputSchema = z
|
||||
.object({
|
||||
artifactId: Id.describe("Image artifact to edit."),
|
||||
title: MediaTitleSchema.optional().describe(
|
||||
"A few words in the user's language for the card and file name.",
|
||||
),
|
||||
crop: z
|
||||
.object({ left: Pixels, top: Pixels, width: Pixels.min(1), height: Pixels.min(1) })
|
||||
.optional()
|
||||
|
||||
@ -14,7 +14,7 @@ const TimeRangeSchema = z
|
||||
.refine((range) => range.end > range.start, "end must be after start");
|
||||
|
||||
/** Written by the bot in the user's language; shown on the media card and used as file name. */
|
||||
const MediaTitleSchema = z.string().trim().min(1).max(120);
|
||||
export const MediaTitleSchema = z.string().trim().min(1).max(120);
|
||||
|
||||
export const EditVideoStepSchema = z.discriminatedUnion("op", [
|
||||
z
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user