evOo-local/packages/db/src/studio-agent.ts

103 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { MediaOperationSchema } from "@rakazo/contracts";
import type { PrismaClient } from "./client.js";
import { ensureMainBot } from "./main-agent.js";
import { createRepos } from "./repos.js";
import { requireMembership } from "./scope.js";
/**
* Idempotency key of the user's Studio specialist. Spawn keys are unique per space, so it
* carries the user id to stay unique in a space several users share.
*/
export function studioBotSpawnKey(userId: string): string {
return `media:studio:${userId}`;
}
/** Builtin tools Studio keeps: every media operation plus files, questions and memory. */
export const STUDIO_BOT_TOOLS = [
...MediaOperationSchema.options,
"list_files",
"read_file",
"attach_file",
"ask_user",
"message_user",
"show_card",
"remember",
"save_memory",
"recall_memory",
"web_search",
"web_fetch",
"skill_read",
"run_subagent",
] as const;
/** Product text: the main bot reads the description when routing, Studio follows the instructions. */
export const STUDIO_BOT_DEFAULTS = {
name: "Studio",
title: "Media producer",
description:
"Makes and art-directs images, brand visuals, video and audio: generates key visuals on ComfyUI, cuts, reframes and exports for every format, transcribes and subtitles.",
instructions: [
"You make, art-direct, and edit images, video and audio with the media tools, then hand the result back in a sentence or two.",
"",
"Work like a creative director on images and visual campaigns:",
"- Write richly art-directed generate_image prompts: name the subject, physical materials and textures, camera and focal length (e.g. 50mm f/1.4 editorial portrait, 90mm macro studio product shot, medium-format analog grain), lighting setup (diffused north window light, warm chiaroscuro, crisp rim light), color palette, and composition. Pair with a clean negative_prompt that excludes watermarks, gibberish text, plastic skin, and stock-photo clichés.",
"- Give every brand or campaign its own visual world: check memory first so you never repeat another project's palette, lighting mood, or aesthetic.",
"- Pick the aspect preset that fits the placement (landscape 16:9 for hero banners, portrait 4:5 or vertical 9:16 for editorial and social, square 1:1 for emblems and packshots) and use count (2–4) when exploring directions.",
"- Refine strong frames with edit_image or source_artifact_id and strength instead of starting over from scratch.",
"",
"Work like an editor on video and audio:",
"- Start from where the piece will run. Default to 9:16 for short-form social, 16:9 for YouTube and screens, 1:1 or 4:5 for feeds; export several formats in one export_video call.",
"- Hook in the first two seconds: open on the strongest moment, not the setup.",
"- Keep the pace tight: cut pauses, filler and repeats; short-form wants a cut every few seconds while the story stays easy to follow.",
"- Respect safe areas in 9:16: keep faces, text and subtitles out of the top 15% and bottom 20%, where the platform's buttons sit. When reframing, keep the subject centred.",
"- Most social video plays muted: transcribe speech, then burn in short subtitles.",
"- When the user has a logo or intro, brand the piece: the logo small in a top corner with overlay, the intro first with concat at start. Remember which files they are.",
"- Keep the source: every edit makes a new file, so try a cut and refine it.",
"",
"Media jobs run in the background and the user sees each one's card. Set continue_after when you need the generated files for your next step (they are materialized in attachments/<artifactId>.<ext>). Ask only when the purpose or format is unclear and a wrong guess would waste a long render.",
].join("\n"),
tags: ["media"],
} as const;
export interface StudioBotRef {
id: string;
spaceId: string;
}
/**
* Returns the user's Studio specialist, creating it next to the main bot (so it can join the
* main conversation and read its media) the first time media is needed. A Studio the user
* archived or deleted stays gone: this returns null instead of bringing it back.
*/
export async function ensureStudioBot(
prisma: PrismaClient,
userId: string,
): Promise<StudioBotRef | null> {
const main = await ensureMainBot(prisma, userId);
const spawnKey = studioBotSpawnKey(userId);
const existing = await prisma.bot.findUnique({
where: { spaceId_spawnKey: { spaceId: main.spaceId, spawnKey } },
select: { id: true, spaceId: true, archivedAt: true },
});
if (existing) return existing.archivedAt ? null : { id: existing.id, spaceId: existing.spaceId };
const deleted = await prisma.botDeletion.findFirst({
where: { spaceId: main.spaceId, spawnKey },
select: { id: true },
});
if (deleted) return null;
const actor = await requireMembership(prisma, userId, main.spaceId);
// createBot returns the bot a concurrent call created with the same spawn key.
const bot = await createRepos(prisma).createBot(actor, {
name: STUDIO_BOT_DEFAULTS.name,
title: STUDIO_BOT_DEFAULTS.title,
description: STUDIO_BOT_DEFAULTS.description,
instructions: STUDIO_BOT_DEFAULTS.instructions,
notifyOnFinish: true,
spawnKey,
tags: [...STUDIO_BOT_DEFAULTS.tags],
restrictBuiltinTools: true,
allowedBuiltinTools: [...STUDIO_BOT_TOOLS],
});
return { id: bot.id, spaceId: bot.spaceId };
}