export type PromptSequenceCharacter = { id: string name: string description: string } export type PromptSequenceScene = { id: string title: string style: string settings: string characters: PromptSequenceCharacter[] prompt: string } export type PromptSequence = { id: string name: string scenes: PromptSequenceScene[] createdAt: number updatedAt: number } export const PROMPT_SEQUENCE_NAME_MAX = 80 export const PROMPT_SEQUENCE_FIELD_MAX = 50_000 export const PROMPT_SEQUENCE_TITLE_MAX = 60 export function newId() { if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { return crypto.randomUUID() } return `ps-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}` } export function emptyCharacter(): PromptSequenceCharacter { return { id: newId(), name: '', description: '' } } export function emptyScene(index = 1): PromptSequenceScene { return { id: newId(), title: `Scene ${Math.max(1, index)}`, style: '', settings: '', characters: [emptyCharacter()], prompt: '' } } export function cloneSceneForNext(previous: PromptSequenceScene, nextIndex: number): PromptSequenceScene { return { id: newId(), title: `Scene ${nextIndex}`, style: previous.style, settings: previous.settings, characters: (previous.characters || []).map(character => ({ id: newId(), name: character.name, description: character.description })), prompt: previous.prompt } } /** Character line: "Name: description", or name-only, or description-only. */ export function formatCharacterLine(character: { name?: string; description?: string }) { const name = String(character?.name || '').trim() const description = String(character?.description || '').trim() if (!name && !description) return '' if (name && description) return `${name}: ${description}` return name || description } export function assembleSceneText(scene: { style?: string settings?: string characters?: Array<{ name?: string; description?: string }> prompt?: string }) { const characterBlock = (scene.characters || []) .map(formatCharacterLine) .filter(Boolean) .join('\n') const parts = [ String(scene.style || '').trim(), String(scene.settings || '').trim(), characterBlock, String(scene.prompt || '').trim() ].filter(Boolean) return parts.join('\n\n') } /** * Whole sequence as one copyable block. * Scenes join with a double blank line (\n\n\n) — same separator desktop Batch uses. * Sequence name is never included. */ export function assembleSequenceText(sequence: { scenes?: Array<{ style?: string settings?: string characters?: Array<{ name?: string; description?: string }> prompt?: string }> }) { return (sequence.scenes || []) .map(assembleSceneText) .filter(Boolean) .join('\n\n\n') } export function normalizePromptSequenceName(raw: unknown) { return String(raw || '').trim().replace(/\s+/g, ' ').slice(0, PROMPT_SEQUENCE_NAME_MAX) } export function normalizePromptSequenceField(raw: unknown) { return String(raw || '').replace(/\r\n/g, '\n').slice(0, PROMPT_SEQUENCE_FIELD_MAX) } export function normalizePromptSequenceTitle(raw: unknown, fallback: string) { const title = String(raw || '').trim().replace(/\s+/g, ' ').slice(0, PROMPT_SEQUENCE_TITLE_MAX) return title || fallback } export function normalizeCharacter(raw: unknown): PromptSequenceCharacter { const rec = (raw && typeof raw === 'object' ? raw : {}) as Record return { id: String(rec.id || '').trim() || newId(), name: String(rec.name || '').trim().slice(0, 120), description: normalizePromptSequenceField(rec.description).trim() } } export function normalizeScene(raw: unknown, index: number): PromptSequenceScene { const rec = (raw && typeof raw === 'object' ? raw : {}) as Record const characters = Array.isArray(rec.characters) ? rec.characters.map(normalizeCharacter) : [emptyCharacter()] return { id: String(rec.id || '').trim() || newId(), title: normalizePromptSequenceTitle(rec.title, `Scene ${index + 1}`), style: normalizePromptSequenceField(rec.style), settings: normalizePromptSequenceField(rec.settings), characters: characters.length ? characters : [emptyCharacter()], prompt: normalizePromptSequenceField(rec.prompt) } } export function normalizePromptSequence(raw: unknown): PromptSequence | null { if (!raw || typeof raw !== 'object') return null const rec = raw as Record const name = normalizePromptSequenceName(rec.name) if (!name) return null const scenesRaw = Array.isArray(rec.scenes) ? rec.scenes : [] const scenes = scenesRaw.map((scene, index) => normalizeScene(scene, index)) const createdAt = Number(rec.createdAt) || Date.now() return { id: String(rec.id || '').trim() || newId(), name, scenes: scenes.length ? scenes : [emptyScene(1)], createdAt, updatedAt: Number(rec.updatedAt) || createdAt } }