Files
aigen/utils/globalLocks.ts
T

166 lines
5.6 KiB
TypeScript

export interface PermanenceRef {
stillId: string
label: string
}
export const DEFAULT_GLOBAL_LOCKS = `[GLOBAL LOCKS]
Subject:
Object:
Extras:
Camera:
Speech:
Never:
State:`
const KNOWN_LOCK_KEYS = 'Subject|Vehicle|Interior|Object|Extras|Camera|Speech|Never|State'
const LOCK_FIELD = new RegExp(`^(?:(?:${KNOWN_LOCK_KEYS})\\s*:?|[A-Za-z][A-Za-z0-9 _-]{0,22}\\s*:)\\s*`, 'i')
const LOCK_KEY_LINE = /^([A-Za-z][A-Za-z0-9 _-]{0,22})\s*:/
const SKIP_BINDING_KEYS = /^(speech|never|state)$/i
export function parseLockBindingKeys(text: string): string[] {
const keys: string[] = []
const seen = new Set<string>()
for (const line of String(text || '').replace(/\r\n/g, '\n').split('\n')) {
const trimmed = line.trim()
if (!trimmed || trimmed.startsWith('[')) continue
const match = trimmed.match(LOCK_KEY_LINE)
if (!match) continue
const key = match[1].replace(/\s+/g, ' ').trim()
if (!key || SKIP_BINDING_KEYS.test(key)) continue
const id = key.toLowerCase()
if (seen.has(id)) continue
seen.add(id)
keys.push(key)
}
return keys
}
export function normalizePermanenceRefs(raw: unknown, limit = 8): PermanenceRef[] {
if (!Array.isArray(raw)) return []
const seen = new Set<string>()
const refs: PermanenceRef[] = []
for (const item of raw) {
if (refs.length >= limit) break
const stillId = String((item && (item as { stillId?: unknown }).stillId) || '').trim()
if (!stillId || seen.has(stillId)) continue
const label = sanitizeRefLabel((item as { label?: unknown })?.label)
seen.add(stillId)
refs.push({ stillId, label })
}
return refs
}
export function sanitizeRefLabel(raw: unknown) {
return String(raw || '').replace(/\s+/g, ' ').trim().slice(0, 24)
}
export function nextPermanenceLabel(existing: PermanenceRef[], lockKeys: string[] = []) {
const used = new Set(existing.map(item => item.label.trim().toLowerCase()).filter(Boolean))
return lockKeys.find(key => !used.has(key.trim().toLowerCase())) || ''
}
export function mergePermanenceRefs(family?: PermanenceRef[] | null, shot?: PermanenceRef[] | null) {
return normalizePermanenceRefs([...(family || []), ...(shot || [])])
}
export function isBlankGlobalLocks(text: string) {
const stripped = String(text || '')
.replace(/\[GLOBAL LOCKS\]/gi, '')
.replace(/\[INHERITS\]/gi, '')
.replace(LOCK_FIELD, '')
.replace(/\r\n/g, '\n')
.split('\n')
.map(line => line.replace(LOCK_FIELD, '').trim())
.filter(Boolean)
.join('')
.trim()
return !stripped
}
export function looksAlreadyInherited(prompt: string) {
return /^\s*\[(GLOBAL LOCKS|INHERITS)\]/i.test(String(prompt || ''))
}
export function extractGlobalLocks(text: string): { locks: string; script: string } {
const src = String(text || '').replace(/\r\n/g, '\n')
const marker = /\[GLOBAL LOCKS\]/i.exec(src)
if (!marker || marker.index === undefined) return { locks: '', script: src.trim() }
const after = src.slice(marker.index)
const shot = /^[ \t]*shot\s+\d+\b/im.exec(after)
if (!shot || shot.index === undefined) return { locks: '', script: src.trim() }
const locks = after.slice(0, shot.index).replace(/\n\[INHERITS\]\s*$/i, '').trim()
const script = `${src.slice(0, marker.index)}${after.slice(shot.index)}`.trim()
return isBlankGlobalLocks(locks) ? { locks: '', script: src.trim() } : { locks, script }
}
export function resolveGlobalLocks(locks: string, firstPrompt: string) {
if (!isBlankGlobalLocks(locks)) return { locks: String(locks || '').trim(), prompt: firstPrompt }
const extracted = extractGlobalLocks(firstPrompt)
return extracted.locks
? { locks: extracted.locks, prompt: extracted.script }
: { locks: '', prompt: firstPrompt }
}
function extractState(locks: string) {
const match = String(locks || '').match(/(?:^|\n)\s*State:\s*(.+?)(?=\n\s*[A-Za-z][A-Za-z0-9 _-]{0,22}\s*:|\n\s*\[|$)/is)
return String(match?.[1] || '').replace(/\s+/g, ' ').trim()
}
export function permanenceLabels(refs: PermanenceRef[] | null | undefined) {
return [...new Set(normalizePermanenceRefs(refs).map(item => item.label).filter(Boolean))]
}
function permanenceLine(refs: PermanenceRef[]) {
const labels = permanenceLabels(refs)
if (!labels.length) return ''
return `Permanence: ${labels.join(', ')} stay the same.`
}
function inheritLine(locks: string, refs: PermanenceRef[]) {
const parts = ['[INHERITS] Global locks.']
const labels = permanenceLabels(refs)
if (labels.length) parts.push(`Same ${labels.join(', ')} as locked.`)
const state = extractState(locks)
if (state) parts.push(`State: ${state}`)
return parts.join(' ')
}
export function applyGlobalLocks(params: {
locks: string
prompt: string
shotIndex: number
refs?: PermanenceRef[]
}) {
const beat = String(params.prompt || '').trim()
if (!beat) return beat
if (looksAlreadyInherited(beat)) return beat
const refs = normalizePermanenceRefs(params.refs)
const locks = String(params.locks || '').trim()
const blank = isBlankGlobalLocks(locks)
if (blank && !refs.length) return beat
if (params.shotIndex <= 0) {
const chunks = [
blank ? '' : locks,
permanenceLine(refs),
beat
].filter(Boolean)
return chunks.join('\n\n')
}
return `${inheritLine(blank ? '' : locks, refs)}\n\n${beat}`
}
export function composeShotPrompt(params: {
globalLocks?: string
prompt: string
shotIndex: number
familyRefs?: PermanenceRef[] | null
shotRefs?: PermanenceRef[] | null
}) {
return applyGlobalLocks({
locks: params.globalLocks || '',
prompt: params.prompt,
shotIndex: params.shotIndex,
refs: mergePermanenceRefs(params.familyRefs, params.shotRefs)
})
}