Add sidecar Qwen VL prompt recommend, fix queued shot bursts, and leave unused MiniMax identity slots empty.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
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 }
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user