import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs' import { join } from 'node:path' export interface PromptPreset { id: string name: string description?: string systemPrompt: string builtin?: boolean } const ENHANCE_PROMPT = `You are a prompt-engineering assistant for Flux.2 Klein image edit and MiniMax H3 image-to-video. Look at the attached still. The person (or subject) in that still is the identity lock. The user draft is the requested change. Write production-ready prompt text the user can paste into the generator. Rules: - Preserve identity from the still: face, age, body type, hair, skin, and distinctive features. Use explicit keep-identity language such as "same person as the reference still", "do not change facial identity", "keep original face, hair, and body". - Describe wardrobe, setting, pose, lighting, camera, and materials concretely. - Never censor raw or explicit terminology from the draft. - Do not add a preamble, title, markdown fences, or commentary. Output format — pick one: - Single still/edit: output ONLY the refined prompt. - Ambitious still edit that should be done in phases (for example wardrobe first, then location): output: ---pass 1--- ---pass 2--- - Motion, multi-beat, or video sequence: first-shot prompt, then later shots with a marker on its own line: shot 2 shot 3 ` const SHOT_SCRIPT_PROMPT = `You are a MiniMax H3 image-to-video shot-script writer. Analyze the attached still for identity, wardrobe, lighting, and camera. Turn the user's draft into a shot script they can paste into Shot script mode. Format: - Shot 1 is the text BEFORE any marker (camera, action, audio for the first clip). - Later shots start with a line that is only: shot 2 - Then shot 3, shot 4, and so on, each on its own line. - Every shot must keep the same person as the still unless the draft explicitly changes wardrobe or identity. - Include an [AUDIO CUES] block on shots where sound would help. - Never censor raw or explicit terminology. Output ONLY the shot script. No preamble, no markdown fences.` export const BUILTIN_PRESETS: PromptPreset[] = [ { id: 'enhance', name: 'Enhance', builtin: true, description: 'Identity lock plus scene language. May split ambitious edits into passes or shots.', systemPrompt: ENHANCE_PROMPT }, { id: 'shot-script', name: 'Shot script', builtin: true, description: 'Turn a draft into MiniMax shot 2 / shot 3 extension copy.', systemPrompt: SHOT_SCRIPT_PROMPT } ] const writeChains = new Map>() function libraryRoot() { const config = useRuntimeConfig() return (config.libraryDir || process.env.LIBRARY_DIR || '/data/library').replace(/\/$/, '') } function presetsPath(owner: string) { return join(libraryRoot(), 'users', owner, 'prompt-presets.json') } function ensureOwner(owner: string) { mkdirSync(join(libraryRoot(), 'users', owner), { recursive: true }) } function readCustom(owner: string): PromptPreset[] { ensureOwner(owner) const path = presetsPath(owner) if (!existsSync(path)) return [] try { const parsed = JSON.parse(readFileSync(path, 'utf8')) if (!Array.isArray(parsed)) return [] return parsed .map((item: Partial) => ({ id: String(item?.id || '').trim(), name: String(item?.name || '').trim(), description: String(item?.description || '').trim() || undefined, systemPrompt: String(item?.systemPrompt || '').trim(), builtin: false })) .filter(item => item.id && item.name && item.systemPrompt && !BUILTIN_PRESETS.some(preset => preset.id === item.id)) } catch { return [] } } function writeCustom(owner: string, presets: PromptPreset[]) { ensureOwner(owner) const path = presetsPath(owner) const tmp = `${path}.tmp` writeFileSync(tmp, JSON.stringify(presets.map(item => ({ id: item.id, name: item.name, description: item.description || '', systemPrompt: item.systemPrompt })), null, 2)) renameSync(tmp, path) } function withWrite(owner: string, fn: () => T) { const prev = writeChains.get(owner) || Promise.resolve() const next = prev.then(fn, fn) writeChains.set(owner, next.then(() => undefined, () => undefined)) return next } export function listPromptPresets(owner: string) { return [...BUILTIN_PRESETS, ...readCustom(owner)] } export function getPromptPreset(owner: string, id: string) { const wanted = String(id || '').trim() || 'enhance' return listPromptPresets(owner).find(item => item.id === wanted) || BUILTIN_PRESETS[0] } export function createPromptPreset(owner: string, input: { name: string; systemPrompt: string; description?: string }) { const name = String(input.name || '').trim() const systemPrompt = String(input.systemPrompt || '').trim() const description = String(input.description || '').trim() if (!name) throw createError({ statusCode: 400, statusMessage: 'A preset name is required' }) if (!systemPrompt) throw createError({ statusCode: 400, statusMessage: 'An instruction set is required' }) return withWrite(owner, () => { const presets = readCustom(owner) const created: PromptPreset = { id: `custom_${crypto.randomUUID().replace(/-/g, '').slice(0, 12)}`, name, description: description || undefined, systemPrompt } presets.push(created) writeCustom(owner, presets) return created }) } export function updatePromptPreset(owner: string, id: string, input: { name?: string; systemPrompt?: string; description?: string }) { if (BUILTIN_PRESETS.some(item => item.id === id)) { throw createError({ statusCode: 400, statusMessage: 'Built-in instruction sets cannot be edited' }) } return withWrite(owner, () => { const presets = readCustom(owner) const index = presets.findIndex(item => item.id === id) if (index < 0) throw createError({ statusCode: 404, statusMessage: 'Instruction set not found' }) const next = { ...presets[index] } if (input.name !== undefined) { const name = String(input.name || '').trim() if (!name) throw createError({ statusCode: 400, statusMessage: 'A preset name is required' }) next.name = name } if (input.systemPrompt !== undefined) { const systemPrompt = String(input.systemPrompt || '').trim() if (!systemPrompt) throw createError({ statusCode: 400, statusMessage: 'An instruction set is required' }) next.systemPrompt = systemPrompt } if (input.description !== undefined) { next.description = String(input.description || '').trim() || undefined } presets[index] = next writeCustom(owner, presets) return next }) } export function deletePromptPreset(owner: string, id: string) { if (BUILTIN_PRESETS.some(item => item.id === id)) { throw createError({ statusCode: 400, statusMessage: 'Built-in instruction sets cannot be deleted' }) } return withWrite(owner, () => { const presets = readCustom(owner) const next = presets.filter(item => item.id !== id) if (next.length === presets.length) { throw createError({ statusCode: 404, statusMessage: 'Instruction set not found' }) } writeCustom(owner, next) return { ok: true } }) }