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/.). 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 { 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 }; }