Restore character names on Prompt Builder compile lines.

Character lines again use Name: description with outfit and facing features; set names and framing labels stay uncopied.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-10-04 23:17:15 -05:00
co-authored by Cursor
parent dd8897a3cf
commit f6c5ef8af5
3 changed files with 62 additions and 27 deletions
+1 -1
View File
@@ -123,7 +123,7 @@
maxlength="120"
placeholder="Name"
>
<p class="mt-1 text-[11px] text-zinc-500">Label only — not copied into the prompt.</p>
<p class="mt-1 text-[11px] text-zinc-500">Copied as “Name: …” when this character is on.</p>
</label>
<label class="block text-sm">
<span class="mb-1 block text-[11px] text-zinc-500">Base description</span>
+43 -16
View File
@@ -37,12 +37,15 @@ const {
} = await import(pathToFileURL(tmp).href)
unlinkSync(tmp)
test('formatCharacterLine copies base and outfit, never name', () => {
assert.equal(formatCharacterLine({ name: 'Ada', description: 'tall' }), 'tall')
test('formatCharacterLine copies name, description, and outfit', () => {
assert.equal(formatCharacterLine({ name: 'Ada', description: 'tall' }), 'Ada: tall')
assert.equal(
formatCharacterLine({ name: 'Ada', description: 'tall' }, { name: 'Work', description: 'blue jacket' }),
'tall. Outfit: blue jacket'
'Ada: tall. Outfit: blue jacket'
)
assert.equal(formatCharacterLine({ name: '', description: 'tall' }), 'tall')
assert.ok(!formatCharacterLine({ name: '', description: 'tall' }).includes(':'))
assert.equal(formatCharacterLine({ name: 'Ada', description: '' }), 'Ada')
})
test('stripBangs removes inline ignore spans and cleans commas', () => {
@@ -178,8 +181,7 @@ test('legacy sequence without sets still assembles', () => {
updatedAt: 1
})
assert.deepEqual(legacy.sets, [])
assert.equal(assembleSequenceText(legacy), 'oil\n\ntall\n\nwalk')
assert.ok(!assembleSequenceText(legacy).includes('Ada'))
assert.equal(assembleSequenceText(legacy), 'oil\n\nAda: tall\n\nwalk')
})
test('cloneSceneForNext copies setStates including framingId', () => {
@@ -208,8 +210,7 @@ test('character outfit and off still work with sets', () => {
characterStates: [{ characterId: 'c1', on: true, outfitId: 'o1' }],
prompt: 'hi'
}, [ada], [])
assert.equal(text, 'tall. Outfit: red dress\n\nhi')
assert.ok(!text.includes('Ada'))
assert.equal(text, 'Ada: tall. Outfit: red dress\n\nhi')
})
test('ensureSceneStatesForCharacters turns new character on only for current scene', () => {
@@ -244,7 +245,7 @@ test('sectionOrder settings then prompt then characters; empty style and set omi
}
const order = ['settings', 'prompt', 'characters']
const text = assembleSceneText(scene, [ada], [set], order)
assert.equal(text, 'soft light\n\nlooks up\n\ntall')
assert.equal(text, 'soft light\n\nlooks up\n\nAda: tall')
assert.ok(!text.includes('open yard'))
assert.ok(!text.includes('Courtyard'))
assert.ok(!text.includes('wide'))
@@ -295,11 +296,10 @@ test('legacy sequence without sectionOrder keeps default order and \\n\\n\\n joi
const full = assembleSequenceText(legacy)
assert.equal(
full,
'oil\n\nday\n\nstone hall\n\ntall\n\nwalk\n\n\nink\n\ntall\n\nstop'
'oil\n\nday\n\nstone hall\n\nAda: tall\n\nwalk\n\n\nink\n\nAda: tall\n\nstop'
)
assert.ok(!full.includes('Hall'))
assert.ok(!full.includes('wide'))
assert.ok(!full.includes('Ada'))
})
test('bang strip applies to set framing and character text before join', () => {
@@ -335,12 +335,11 @@ test('bang strip applies to set framing and character text before join', () => {
characterStates: [{ characterId: 'c1', on: true, outfitId: null }],
prompt: ''
}, [ada], [set])
assert.equal(text, 'courtyard, dusk\n\nyoung woman, linen robe')
assert.equal(text, 'courtyard, dusk\n\nAda: young woman, linen robe')
assert.ok(!text.includes('brown eyes'))
assert.ok(!text.includes('scar'))
assert.ok(!text.includes('prop note'))
assert.ok(!text.includes('!'))
assert.ok(!text.includes('Ada'))
assert.ok(!text.includes('Yard'))
})
@@ -375,17 +374,17 @@ test('facing away drops eyes; feature stays omitted with camera reason; out emit
}
const away = compileCharacterForFacing(ada, 'away')
assert.equal(away.text, 'young woman, long braid, linen robe')
assert.equal(away.text, 'Ada: young woman, long braid, linen robe')
assert.equal(away.omittedFeatures.length, 1)
assert.equal(away.omittedFeatures[0].text, 'brown eyes')
assert.equal(away.omittedFeatures[0].reason, 'camera')
const toward = compileCharacterForFacing(ada, 'toward')
assert.equal(toward.text, 'young woman, brown eyes, linen robe')
assert.equal(toward.text, 'Ada: young woman, brown eyes, linen robe')
assert.ok(toward.omittedFeatures.some(item => item.text === 'long braid'))
const hands = compileCharacterForFacing(ada, 'hands')
assert.equal(hands.text, 'young woman, linen robe')
assert.equal(hands.text, 'Ada: young woman, linen robe')
assert.ok(hands.omittedFeatures.some(item => item.text === 'brown eyes'))
const out = compileCharacterForFacing(ada, 'out')
@@ -405,7 +404,35 @@ test('facing away drops eyes; feature stays omitted with camera reason; out emit
characterStates: [{ characterId: 'c1', on: true, outfitId: null }],
prompt: 'walks'
}, [ada], [set])
assert.equal(sceneText, 'stone hall\n\nyoung woman, long braid, linen robe\n\nwalks')
assert.equal(sceneText, 'stone hall\n\nAda: young woman, long braid, linen robe\n\nwalks')
assert.ok(!sceneText.includes('brown eyes'))
assert.ok(!sceneText.includes('Hall'))
})
test('Britt line includes name, colon, description, and outfit; nameless has no stray colon', () => {
const britt = {
...emptyCharacter(),
id: 'c-britt',
name: 'Britt',
description: 'a beautiful 40 year old woman with strawberry blond hair',
features: []
}
const gown = { ...emptyOutfit(), id: 'o1', name: 'gown', description: 'a purple evening gown' }
const line = compileCharacterForFacing(britt, 'toward', gown).text
assert.equal(
line,
'Britt: a beautiful 40 year old woman with strawberry blond hair. Outfit: a purple evening gown'
)
assert.ok(line.includes('Britt'))
assert.ok(line.includes(':'))
assert.ok(line.includes('strawberry blond hair'))
assert.ok(line.includes('Outfit: a purple evening gown'))
const nameless = compileCharacterForFacing({
...emptyCharacter(),
name: '',
description: 'a figure in shadow'
}, 'toward').text
assert.equal(nameless, 'a figure in shadow')
assert.ok(!nameless.includes(':'))
})
+18 -10
View File
@@ -53,9 +53,9 @@ export type PromptSequenceFeature = {
export type PromptSequenceCharacter = {
id: string
/** Label only — never copied into the prompt. */
/** Copied into the prompt as "Name: …" when the character is on and facing is not out. */
name: string
/** Base description; always included when in frame (facing !== out). */
/** Base description; included with the name when in frame (facing !== out). */
description: string
features: PromptSequenceFeature[]
outfits: PromptSequenceOutfit[]
@@ -283,7 +283,8 @@ export type CompiledCharacterFacing = {
/**
* Compile one character for a camera facing.
* Name is never copied. Facing "out" emits nothing.
* Name is copied ("Name: description"). Facing "out" emits nothing.
* Feature text appends after the description, before the outfit.
*/
export function compileCharacterForFacing(
character: {
@@ -318,15 +319,18 @@ export function compileCharacterForFacing(
return { text: '', omittedFeatures, ignoredBangs }
}
const parts: string[] = []
const base = stripBangs(String(character?.description || ''))
if (base) parts.push(base)
const name = stripBangs(String(character?.name || ''))
const description = stripBangs(String(character?.description || ''))
let head = ''
if (name && description) head = `${name}: ${description}`
else head = name || description
const featureParts: string[] = []
for (const feature of features) {
if (!feature?.enabled) continue
const cleaned = stripBangs(String(feature.text || ''))
if (isFeatureVisibleForFacing(feature, facing)) {
if (cleaned) parts.push(cleaned)
if (cleaned) featureParts.push(cleaned)
} else {
omittedFeatures.push({
id: String(feature.id || newId()),
@@ -336,10 +340,14 @@ export function compileCharacterForFacing(
}
}
let text = parts.join(', ')
let text = head
if (featureParts.length) {
const featureText = featureParts.join(', ')
text = text ? `${text}, ${featureText}` : featureText
}
const outfitText = stripBangs(outfitPromptText(outfit))
if (outfitText) {
text = text ? `${text}. Outfit: ${outfitText}` : outfitText
text = text ? `${text}. Outfit: ${outfitText}` : `Outfit: ${outfitText}`
}
return { text, omittedFeatures, ignoredBangs }
}
@@ -389,7 +397,7 @@ export function cloneSceneForNext(previous: PromptSequenceScene, nextIndex: numb
}
/**
* Legacy helper: base description (+ optional outfit). Name is never copied.
* Legacy helper for name/description/outfit (+ optional facing features).
* Prefer compileCharacterForFacing for feature-aware compile.
*/
export function formatCharacterLine(