Files
aigen/server/utils/promptPresets.ts
T

195 lines
7.2 KiB
TypeScript

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---
<prompt for pass 1>
---pass 2---
<prompt for pass 2>
- Motion, multi-beat, or video sequence: first-shot prompt, then later shots with a marker on its own line:
shot 2
<extension prompt>
shot 3
<extension prompt>`
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<string, Promise<unknown>>()
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<PromptPreset>) => ({
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<T>(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 }
})
}