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:
@@ -123,7 +123,7 @@
|
|||||||
maxlength="120"
|
maxlength="120"
|
||||||
placeholder="Name"
|
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>
|
||||||
<label class="block text-sm">
|
<label class="block text-sm">
|
||||||
<span class="mb-1 block text-[11px] text-zinc-500">Base description</span>
|
<span class="mb-1 block text-[11px] text-zinc-500">Base description</span>
|
||||||
|
|||||||
@@ -37,12 +37,15 @@ const {
|
|||||||
} = await import(pathToFileURL(tmp).href)
|
} = await import(pathToFileURL(tmp).href)
|
||||||
unlinkSync(tmp)
|
unlinkSync(tmp)
|
||||||
|
|
||||||
test('formatCharacterLine copies base and outfit, never name', () => {
|
test('formatCharacterLine copies name, description, and outfit', () => {
|
||||||
assert.equal(formatCharacterLine({ name: 'Ada', description: 'tall' }), 'tall')
|
assert.equal(formatCharacterLine({ name: 'Ada', description: 'tall' }), 'Ada: tall')
|
||||||
assert.equal(
|
assert.equal(
|
||||||
formatCharacterLine({ name: 'Ada', description: 'tall' }, { name: 'Work', description: 'blue jacket' }),
|
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', () => {
|
test('stripBangs removes inline ignore spans and cleans commas', () => {
|
||||||
@@ -178,8 +181,7 @@ test('legacy sequence without sets still assembles', () => {
|
|||||||
updatedAt: 1
|
updatedAt: 1
|
||||||
})
|
})
|
||||||
assert.deepEqual(legacy.sets, [])
|
assert.deepEqual(legacy.sets, [])
|
||||||
assert.equal(assembleSequenceText(legacy), 'oil\n\ntall\n\nwalk')
|
assert.equal(assembleSequenceText(legacy), 'oil\n\nAda: tall\n\nwalk')
|
||||||
assert.ok(!assembleSequenceText(legacy).includes('Ada'))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('cloneSceneForNext copies setStates including framingId', () => {
|
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' }],
|
characterStates: [{ characterId: 'c1', on: true, outfitId: 'o1' }],
|
||||||
prompt: 'hi'
|
prompt: 'hi'
|
||||||
}, [ada], [])
|
}, [ada], [])
|
||||||
assert.equal(text, 'tall. Outfit: red dress\n\nhi')
|
assert.equal(text, 'Ada: tall. Outfit: red dress\n\nhi')
|
||||||
assert.ok(!text.includes('Ada'))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('ensureSceneStatesForCharacters turns new character on only for current scene', () => {
|
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 order = ['settings', 'prompt', 'characters']
|
||||||
const text = assembleSceneText(scene, [ada], [set], order)
|
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('open yard'))
|
||||||
assert.ok(!text.includes('Courtyard'))
|
assert.ok(!text.includes('Courtyard'))
|
||||||
assert.ok(!text.includes('wide'))
|
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)
|
const full = assembleSequenceText(legacy)
|
||||||
assert.equal(
|
assert.equal(
|
||||||
full,
|
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('Hall'))
|
||||||
assert.ok(!full.includes('wide'))
|
assert.ok(!full.includes('wide'))
|
||||||
assert.ok(!full.includes('Ada'))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('bang strip applies to set framing and character text before join', () => {
|
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 }],
|
characterStates: [{ characterId: 'c1', on: true, outfitId: null }],
|
||||||
prompt: ''
|
prompt: ''
|
||||||
}, [ada], [set])
|
}, [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('brown eyes'))
|
||||||
assert.ok(!text.includes('scar'))
|
assert.ok(!text.includes('scar'))
|
||||||
assert.ok(!text.includes('prop note'))
|
assert.ok(!text.includes('prop note'))
|
||||||
assert.ok(!text.includes('!'))
|
assert.ok(!text.includes('!'))
|
||||||
assert.ok(!text.includes('Ada'))
|
|
||||||
assert.ok(!text.includes('Yard'))
|
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')
|
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.length, 1)
|
||||||
assert.equal(away.omittedFeatures[0].text, 'brown eyes')
|
assert.equal(away.omittedFeatures[0].text, 'brown eyes')
|
||||||
assert.equal(away.omittedFeatures[0].reason, 'camera')
|
assert.equal(away.omittedFeatures[0].reason, 'camera')
|
||||||
|
|
||||||
const toward = compileCharacterForFacing(ada, 'toward')
|
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'))
|
assert.ok(toward.omittedFeatures.some(item => item.text === 'long braid'))
|
||||||
|
|
||||||
const hands = compileCharacterForFacing(ada, 'hands')
|
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'))
|
assert.ok(hands.omittedFeatures.some(item => item.text === 'brown eyes'))
|
||||||
|
|
||||||
const out = compileCharacterForFacing(ada, 'out')
|
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 }],
|
characterStates: [{ characterId: 'c1', on: true, outfitId: null }],
|
||||||
prompt: 'walks'
|
prompt: 'walks'
|
||||||
}, [ada], [set])
|
}, [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('brown eyes'))
|
||||||
assert.ok(!sceneText.includes('Hall'))
|
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
@@ -53,9 +53,9 @@ export type PromptSequenceFeature = {
|
|||||||
|
|
||||||
export type PromptSequenceCharacter = {
|
export type PromptSequenceCharacter = {
|
||||||
id: string
|
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
|
name: string
|
||||||
/** Base description; always included when in frame (facing !== out). */
|
/** Base description; included with the name when in frame (facing !== out). */
|
||||||
description: string
|
description: string
|
||||||
features: PromptSequenceFeature[]
|
features: PromptSequenceFeature[]
|
||||||
outfits: PromptSequenceOutfit[]
|
outfits: PromptSequenceOutfit[]
|
||||||
@@ -283,7 +283,8 @@ export type CompiledCharacterFacing = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Compile one character for a camera facing.
|
* 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(
|
export function compileCharacterForFacing(
|
||||||
character: {
|
character: {
|
||||||
@@ -318,15 +319,18 @@ export function compileCharacterForFacing(
|
|||||||
return { text: '', omittedFeatures, ignoredBangs }
|
return { text: '', omittedFeatures, ignoredBangs }
|
||||||
}
|
}
|
||||||
|
|
||||||
const parts: string[] = []
|
const name = stripBangs(String(character?.name || ''))
|
||||||
const base = stripBangs(String(character?.description || ''))
|
const description = stripBangs(String(character?.description || ''))
|
||||||
if (base) parts.push(base)
|
let head = ''
|
||||||
|
if (name && description) head = `${name}: ${description}`
|
||||||
|
else head = name || description
|
||||||
|
|
||||||
|
const featureParts: string[] = []
|
||||||
for (const feature of features) {
|
for (const feature of features) {
|
||||||
if (!feature?.enabled) continue
|
if (!feature?.enabled) continue
|
||||||
const cleaned = stripBangs(String(feature.text || ''))
|
const cleaned = stripBangs(String(feature.text || ''))
|
||||||
if (isFeatureVisibleForFacing(feature, facing)) {
|
if (isFeatureVisibleForFacing(feature, facing)) {
|
||||||
if (cleaned) parts.push(cleaned)
|
if (cleaned) featureParts.push(cleaned)
|
||||||
} else {
|
} else {
|
||||||
omittedFeatures.push({
|
omittedFeatures.push({
|
||||||
id: String(feature.id || newId()),
|
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))
|
const outfitText = stripBangs(outfitPromptText(outfit))
|
||||||
if (outfitText) {
|
if (outfitText) {
|
||||||
text = text ? `${text}. Outfit: ${outfitText}` : outfitText
|
text = text ? `${text}. Outfit: ${outfitText}` : `Outfit: ${outfitText}`
|
||||||
}
|
}
|
||||||
return { text, omittedFeatures, ignoredBangs }
|
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.
|
* Prefer compileCharacterForFacing for feature-aware compile.
|
||||||
*/
|
*/
|
||||||
export function formatCharacterLine(
|
export function formatCharacterLine(
|
||||||
|
|||||||
Reference in New Issue
Block a user