@mytechtoday/augment-extensions 2.3.7 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/dist/cli.js +2 -0
- package/cli/dist/cli.js.map +1 -1
- package/cli/dist/commands/generate-shot-list/generator/ai-blocking-extractor.d.ts +58 -0
- package/cli/dist/commands/generate-shot-list/generator/ai-blocking-extractor.d.ts.map +1 -0
- package/cli/dist/commands/generate-shot-list/generator/ai-blocking-extractor.js +275 -0
- package/cli/dist/commands/generate-shot-list/generator/ai-blocking-extractor.js.map +1 -0
- package/cli/dist/commands/generate-shot-list/generator/ai-entity-extractor.d.ts +41 -0
- package/cli/dist/commands/generate-shot-list/generator/ai-entity-extractor.d.ts.map +1 -0
- package/cli/dist/commands/generate-shot-list/generator/ai-entity-extractor.js +155 -0
- package/cli/dist/commands/generate-shot-list/generator/ai-entity-extractor.js.map +1 -0
- package/cli/dist/commands/generate-shot-list/generator/context-builder.d.ts +65 -2
- package/cli/dist/commands/generate-shot-list/generator/context-builder.d.ts.map +1 -1
- package/cli/dist/commands/generate-shot-list/generator/context-builder.js +394 -98
- package/cli/dist/commands/generate-shot-list/generator/context-builder.js.map +1 -1
- package/cli/dist/commands/generate-shot-list/generator/index.d.ts +59 -1
- package/cli/dist/commands/generate-shot-list/generator/index.d.ts.map +1 -1
- package/cli/dist/commands/generate-shot-list/generator/index.js +364 -34
- package/cli/dist/commands/generate-shot-list/generator/index.js.map +1 -1
- package/cli/dist/commands/generate-shot-list/generator/types.d.ts +3 -2
- package/cli/dist/commands/generate-shot-list/generator/types.d.ts.map +1 -1
- package/cli/dist/commands/generate-shot-list/generator/validator.d.ts +33 -0
- package/cli/dist/commands/generate-shot-list/generator/validator.d.ts.map +1 -1
- package/cli/dist/commands/generate-shot-list/generator/validator.js +167 -0
- package/cli/dist/commands/generate-shot-list/generator/validator.js.map +1 -1
- package/cli/dist/commands/generate-shot-list/help-text.d.ts +1 -1
- package/cli/dist/commands/generate-shot-list/help-text.d.ts.map +1 -1
- package/cli/dist/commands/generate-shot-list/help-text.js +11 -0
- package/cli/dist/commands/generate-shot-list/help-text.js.map +1 -1
- package/cli/dist/commands/generate-shot-list.d.ts +1 -0
- package/cli/dist/commands/generate-shot-list.d.ts.map +1 -1
- package/cli/dist/commands/generate-shot-list.js +7 -4
- package/cli/dist/commands/generate-shot-list.js.map +1 -1
- package/package.json +2 -1
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
14
|
exports.ContextBuilder = void 0;
|
|
15
|
+
const ai_entity_extractor_1 = require("./ai-entity-extractor");
|
|
15
16
|
/**
|
|
16
17
|
* Context Builder class
|
|
17
18
|
* Requirement 9: Track character blocking continuity across shots
|
|
@@ -23,7 +24,93 @@ class ContextBuilder {
|
|
|
23
24
|
this.characterBlockingHistory = new Map();
|
|
24
25
|
this.characterBible = new Map(); // Requirement 10
|
|
25
26
|
this.setBible = new Map(); // Requirement 11
|
|
27
|
+
this.styleGuidelines = null;
|
|
26
28
|
this.config = config;
|
|
29
|
+
this.styleGuidelines = config.styleGuidelines || null;
|
|
30
|
+
this.aiExtractor = new ai_entity_extractor_1.AIEntityExtractor();
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Normalize character name for fuzzy matching
|
|
34
|
+
* Handles variations like "THE CAPTAIN" vs "CAPTAIN", "A WIZARD" vs "WIZARD"
|
|
35
|
+
*
|
|
36
|
+
* Rules:
|
|
37
|
+
* - Remove leading articles: THE, A, AN
|
|
38
|
+
* - Trim whitespace
|
|
39
|
+
* - Convert to uppercase for consistent comparison
|
|
40
|
+
*
|
|
41
|
+
* Examples:
|
|
42
|
+
* - "THE CAPTAIN" -> "CAPTAIN"
|
|
43
|
+
* - "A WIZARD" -> "WIZARD"
|
|
44
|
+
* - "AN OFFICER" -> "OFFICER"
|
|
45
|
+
* - "CAPTAIN" -> "CAPTAIN"
|
|
46
|
+
*/
|
|
47
|
+
normalizeCharacterName(name) {
|
|
48
|
+
return name
|
|
49
|
+
.trim()
|
|
50
|
+
.toUpperCase()
|
|
51
|
+
.replace(/^(THE|A|AN)\s+/, ''); // Remove leading articles
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Find character in Bible using fuzzy matching
|
|
55
|
+
* Tries exact match first, then normalized match, then partial match
|
|
56
|
+
*
|
|
57
|
+
* @param characterName - The character name to search for
|
|
58
|
+
* @returns The Bible entry if found, null otherwise
|
|
59
|
+
*/
|
|
60
|
+
findCharacterInBible(characterName) {
|
|
61
|
+
// Try exact match first
|
|
62
|
+
let bibleEntry = this.characterBible.get(characterName);
|
|
63
|
+
if (bibleEntry)
|
|
64
|
+
return bibleEntry;
|
|
65
|
+
// Try normalized match
|
|
66
|
+
const normalizedName = this.normalizeCharacterName(characterName);
|
|
67
|
+
for (const [bibleKey, bibleValue] of this.characterBible.entries()) {
|
|
68
|
+
if (this.normalizeCharacterName(bibleKey) === normalizedName) {
|
|
69
|
+
return bibleValue;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Try partial match (e.g., "CLIF" matches "WIZARD CLIF HIGH")
|
|
73
|
+
for (const [bibleKey, bibleValue] of this.characterBible.entries()) {
|
|
74
|
+
if (bibleKey.includes(characterName) || characterName.includes(bibleKey)) {
|
|
75
|
+
return bibleValue;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Get or create character in Bible using fuzzy matching
|
|
82
|
+
* Returns the canonical name (the one stored in the Bible)
|
|
83
|
+
*
|
|
84
|
+
* @param characterName - The character name to search for
|
|
85
|
+
* @returns Object with bibleEntry and canonicalName
|
|
86
|
+
*/
|
|
87
|
+
getOrCreateCharacterInBible(characterName) {
|
|
88
|
+
// Try to find existing entry
|
|
89
|
+
const existingEntry = this.findCharacterInBible(characterName);
|
|
90
|
+
if (existingEntry) {
|
|
91
|
+
return { bibleEntry: existingEntry, canonicalName: existingEntry.name };
|
|
92
|
+
}
|
|
93
|
+
// Create new entry
|
|
94
|
+
const newEntry = {
|
|
95
|
+
name: characterName,
|
|
96
|
+
wardrobe: '',
|
|
97
|
+
physicalAppearance: '',
|
|
98
|
+
props: [],
|
|
99
|
+
lastSeenPosition: 'in scene',
|
|
100
|
+
lastSeenEmotion: undefined
|
|
101
|
+
};
|
|
102
|
+
this.characterBible.set(characterName, newEntry);
|
|
103
|
+
return { bibleEntry: newEntry, canonicalName: characterName };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Reset all state (Character Bible, Set Bible, blocking history)
|
|
107
|
+
* Call this at the start of each new screenplay generation to prevent contamination
|
|
108
|
+
*/
|
|
109
|
+
reset() {
|
|
110
|
+
this.characterBlockingHistory.clear();
|
|
111
|
+
this.characterBible.clear();
|
|
112
|
+
this.setBible.clear();
|
|
113
|
+
console.log('ContextBuilder state reset - all Bibles and history cleared');
|
|
27
114
|
}
|
|
28
115
|
/**
|
|
29
116
|
* Build scene-level context from scene heading and elements
|
|
@@ -87,31 +174,31 @@ class ContextBuilder {
|
|
|
87
174
|
* Build character states from scene elements
|
|
88
175
|
* Requirement 9: Maintain character blocking continuity across shots
|
|
89
176
|
* Requirement 10: Use Character Bible to maintain rich, consistent character descriptions
|
|
90
|
-
* PRIORITY:
|
|
177
|
+
* PRIORITY: Use AI-powered fuzzy logic to extract characters following MPAA/AMPAS standards
|
|
91
178
|
*/
|
|
92
|
-
buildCharacterStates(elements, sceneContext) {
|
|
179
|
+
async buildCharacterStates(elements, sceneContext) {
|
|
93
180
|
const characterMap = new Map();
|
|
94
|
-
// FIRST PASS:
|
|
95
|
-
const characterNames =
|
|
181
|
+
// FIRST PASS: Use AI to extract characters from scene text
|
|
182
|
+
const characterNames = await this.extractCharactersWithAI(elements, sceneContext);
|
|
183
|
+
// ALSO check Character Bible for mixed-case character name references
|
|
184
|
+
// Example: "Clif pauses" should match "CLIF" or "WIZARD CLIF HIGH" from Bible
|
|
185
|
+
// This handles cases where a character was introduced in a previous segment
|
|
96
186
|
for (const element of elements) {
|
|
97
|
-
if (element.type === '
|
|
98
|
-
const dialogueElement = element;
|
|
99
|
-
const characterName = dialogueElement.dialogue.character.name;
|
|
100
|
-
if (!characterNames.includes(characterName)) {
|
|
101
|
-
characterNames.push(characterName);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
else if (element.type === 'action') {
|
|
105
|
-
// Also extract character names from action lines (ALL CAPS names)
|
|
187
|
+
if (element.type === 'action') {
|
|
106
188
|
const actionElement = element;
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
189
|
+
for (const [bibleName, bibleEntry] of this.characterBible.entries()) {
|
|
190
|
+
const nameParts = bibleName.split(/\s+/);
|
|
191
|
+
for (const part of nameParts) {
|
|
192
|
+
if (part.length > 2) {
|
|
193
|
+
const regex = new RegExp(`\\b${part}\\b`, 'i'); // Case-insensitive match
|
|
194
|
+
if (regex.test(actionElement.text)) {
|
|
195
|
+
// Found a reference to this character - add the full Bible name
|
|
196
|
+
if (!characterNames.includes(bibleName)) {
|
|
197
|
+
characterNames.push(bibleName);
|
|
198
|
+
}
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
115
202
|
}
|
|
116
203
|
}
|
|
117
204
|
}
|
|
@@ -129,48 +216,35 @@ class ContextBuilder {
|
|
|
129
216
|
if (element.type === 'dialogue') {
|
|
130
217
|
const dialogueElement = element;
|
|
131
218
|
const characterName = dialogueElement.dialogue.character.name;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
// If no exact match, try to find a Bible entry that contains this character name
|
|
136
|
-
// E.g., "CLIF" should match "WIZARD CLIF HIGH"
|
|
137
|
-
if (!bibleEntry) {
|
|
138
|
-
for (const [bibleKey, bibleValue] of this.characterBible.entries()) {
|
|
139
|
-
if (bibleKey.includes(characterName) || characterName.includes(bibleKey)) {
|
|
140
|
-
bibleEntry = bibleValue;
|
|
141
|
-
break;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
219
|
+
// Use fuzzy matching to find or create character in Bible
|
|
220
|
+
let { bibleEntry, canonicalName } = this.getOrCreateCharacterInBible(characterName);
|
|
221
|
+
if (!characterMap.has(canonicalName)) {
|
|
145
222
|
const previousState = this.characterBlockingHistory.get(characterName);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
physicalAppearance
|
|
152
|
-
|
|
153
|
-
lastSeenPosition
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
this.characterBible.set(characterName, bibleEntry);
|
|
223
|
+
// If we found an existing entry but it's under a different name, update from previous state
|
|
224
|
+
if (canonicalName !== characterName && previousState) {
|
|
225
|
+
if (previousState.wardrobe)
|
|
226
|
+
bibleEntry.wardrobe = previousState.wardrobe;
|
|
227
|
+
if (previousState.physicalAppearance)
|
|
228
|
+
bibleEntry.physicalAppearance = previousState.physicalAppearance;
|
|
229
|
+
if (previousState.position)
|
|
230
|
+
bibleEntry.lastSeenPosition = previousState.position;
|
|
231
|
+
if (previousState.emotion)
|
|
232
|
+
bibleEntry.lastSeenEmotion = previousState.emotion;
|
|
157
233
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
bibleEntry =
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
props: [],
|
|
165
|
-
lastSeenPosition: 'in scene',
|
|
166
|
-
lastSeenEmotion: undefined
|
|
167
|
-
};
|
|
168
|
-
this.characterBible.set(characterName, bibleEntry);
|
|
234
|
+
// If this is a new entry and we have previous state, populate from it
|
|
235
|
+
if (!bibleEntry.wardrobe && !bibleEntry.physicalAppearance && previousState) {
|
|
236
|
+
bibleEntry.wardrobe = previousState.wardrobe || '';
|
|
237
|
+
bibleEntry.physicalAppearance = previousState.physicalAppearance || '';
|
|
238
|
+
bibleEntry.lastSeenPosition = previousState.position || 'in scene';
|
|
239
|
+
bibleEntry.lastSeenEmotion = previousState.emotion;
|
|
169
240
|
}
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
241
|
+
// Check if character is V.O. (voice over) - they're not visible
|
|
242
|
+
const isVoiceOver = dialogueElement.dialogue.character.extension?.includes('V.O.');
|
|
243
|
+
const position = isVoiceOver ? 'not visible' : bibleEntry.lastSeenPosition;
|
|
244
|
+
// Build rich character state from Bible (use canonical name for consistency)
|
|
245
|
+
characterMap.set(canonicalName, {
|
|
246
|
+
name: canonicalName,
|
|
247
|
+
position: position,
|
|
174
248
|
appearance: `${bibleEntry.physicalAppearance}${bibleEntry.props.length > 0 ? ', carrying ' + bibleEntry.props.join(', ') : ''}`,
|
|
175
249
|
wardrobe: bibleEntry.wardrobe,
|
|
176
250
|
physicalAppearance: bibleEntry.physicalAppearance,
|
|
@@ -179,16 +253,18 @@ class ContextBuilder {
|
|
|
179
253
|
});
|
|
180
254
|
}
|
|
181
255
|
else {
|
|
256
|
+
// Update position if V.O. (voice over)
|
|
257
|
+
const isVoiceOver = dialogueElement.dialogue.character.extension?.includes('V.O.');
|
|
258
|
+
if (isVoiceOver) {
|
|
259
|
+
characterMap.get(canonicalName).position = 'not visible';
|
|
260
|
+
}
|
|
182
261
|
// Update emotion if tracking
|
|
183
262
|
if (this.config.trackCharacterEmotions) {
|
|
184
263
|
const emotion = this.extractEmotion(dialogueElement);
|
|
185
264
|
if (emotion) {
|
|
186
|
-
characterMap.get(
|
|
187
|
-
// Update Bible
|
|
188
|
-
|
|
189
|
-
if (bibleEntry) {
|
|
190
|
-
bibleEntry.lastSeenEmotion = emotion;
|
|
191
|
-
}
|
|
265
|
+
characterMap.get(canonicalName).emotion = emotion;
|
|
266
|
+
// Update Bible (already have bibleEntry from above)
|
|
267
|
+
bibleEntry.lastSeenEmotion = emotion;
|
|
192
268
|
}
|
|
193
269
|
}
|
|
194
270
|
}
|
|
@@ -198,14 +274,56 @@ class ContextBuilder {
|
|
|
198
274
|
this.updateCharacterStatesFromAction(actionElement, characterMap);
|
|
199
275
|
}
|
|
200
276
|
}
|
|
277
|
+
// FOURTH PASS: Process any character names found in action lines that weren't in dialogue
|
|
278
|
+
// This handles cases like "Clif pauses" where the character is referenced but doesn't speak
|
|
279
|
+
for (const characterName of characterNames) {
|
|
280
|
+
// Use fuzzy matching to find or create character in Bible
|
|
281
|
+
let { bibleEntry, canonicalName } = this.getOrCreateCharacterInBible(characterName);
|
|
282
|
+
if (!characterMap.has(canonicalName)) {
|
|
283
|
+
const previousState = this.characterBlockingHistory.get(characterName);
|
|
284
|
+
// If we found an existing entry but it's under a different name, update from previous state
|
|
285
|
+
if (canonicalName !== characterName && previousState) {
|
|
286
|
+
if (previousState.wardrobe)
|
|
287
|
+
bibleEntry.wardrobe = previousState.wardrobe;
|
|
288
|
+
if (previousState.physicalAppearance)
|
|
289
|
+
bibleEntry.physicalAppearance = previousState.physicalAppearance;
|
|
290
|
+
if (previousState.position)
|
|
291
|
+
bibleEntry.lastSeenPosition = previousState.position;
|
|
292
|
+
if (previousState.emotion)
|
|
293
|
+
bibleEntry.lastSeenEmotion = previousState.emotion;
|
|
294
|
+
}
|
|
295
|
+
// If this is a new entry and we have previous state, populate from it
|
|
296
|
+
if (!bibleEntry.wardrobe && !bibleEntry.physicalAppearance && previousState) {
|
|
297
|
+
bibleEntry.wardrobe = previousState.wardrobe || '';
|
|
298
|
+
bibleEntry.physicalAppearance = previousState.physicalAppearance || '';
|
|
299
|
+
bibleEntry.lastSeenPosition = previousState.position || 'in scene';
|
|
300
|
+
bibleEntry.lastSeenEmotion = previousState.emotion;
|
|
301
|
+
}
|
|
302
|
+
// Build character state from Bible (use canonical name for consistency)
|
|
303
|
+
characterMap.set(canonicalName, {
|
|
304
|
+
name: canonicalName,
|
|
305
|
+
position: bibleEntry.lastSeenPosition,
|
|
306
|
+
appearance: `${bibleEntry.physicalAppearance}${bibleEntry.props.length > 0 ? ', carrying ' + bibleEntry.props.join(', ') : ''}`,
|
|
307
|
+
wardrobe: bibleEntry.wardrobe,
|
|
308
|
+
physicalAppearance: bibleEntry.physicalAppearance,
|
|
309
|
+
emotion: bibleEntry.lastSeenEmotion,
|
|
310
|
+
action: undefined
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
201
314
|
// Update blocking history and Character Bible for continuity
|
|
202
315
|
for (const [name, state] of characterMap.entries()) {
|
|
203
316
|
this.characterBlockingHistory.set(name, { ...state });
|
|
204
|
-
// Update Character Bible with latest information
|
|
205
|
-
const bibleEntry = this.
|
|
317
|
+
// Update Character Bible with latest information (using fuzzy matching)
|
|
318
|
+
const bibleEntry = this.findCharacterInBible(name);
|
|
206
319
|
if (bibleEntry) {
|
|
320
|
+
// Normalize position: "enters scene" becomes "in scene" after first appearance
|
|
321
|
+
let normalizedPosition = state.position;
|
|
322
|
+
if (normalizedPosition === 'enters scene') {
|
|
323
|
+
normalizedPosition = 'in scene';
|
|
324
|
+
}
|
|
207
325
|
if (state.position)
|
|
208
|
-
bibleEntry.lastSeenPosition =
|
|
326
|
+
bibleEntry.lastSeenPosition = normalizedPosition;
|
|
209
327
|
if (state.wardrobe)
|
|
210
328
|
bibleEntry.wardrobe = state.wardrobe;
|
|
211
329
|
if (state.physicalAppearance)
|
|
@@ -395,10 +513,41 @@ class ContextBuilder {
|
|
|
395
513
|
}
|
|
396
514
|
return undefined;
|
|
397
515
|
}
|
|
516
|
+
/**
|
|
517
|
+
* Check if the scene is set in space (spaceship, space station, etc.)
|
|
518
|
+
* where weather doesn't make sense
|
|
519
|
+
*/
|
|
520
|
+
isSpaceSetting(scene) {
|
|
521
|
+
const location = scene.heading.location.toLowerCase();
|
|
522
|
+
const intExt = scene.heading.intExt.toLowerCase();
|
|
523
|
+
// Check for space-related keywords in location
|
|
524
|
+
const spaceKeywords = [
|
|
525
|
+
'enterprise', 'starship', 'spaceship', 'space station',
|
|
526
|
+
'bridge', 'engineering', 'sickbay', 'transporter room',
|
|
527
|
+
'cargo bay', 'shuttle bay', 'ready room', 'observation lounge',
|
|
528
|
+
'holodeck', 'jefferies tube', 'turbolift',
|
|
529
|
+
'death star', 'millennium falcon', 'star destroyer',
|
|
530
|
+
'serenity', 'galactica', 'voyager', 'defiant', 'discovery'
|
|
531
|
+
];
|
|
532
|
+
// If it's an interior space setting, no weather
|
|
533
|
+
if (intExt === 'int' && spaceKeywords.some(keyword => location.includes(keyword))) {
|
|
534
|
+
return true;
|
|
535
|
+
}
|
|
536
|
+
// Check for explicit "space" in location
|
|
537
|
+
if (location.includes('space') || location.includes('orbit')) {
|
|
538
|
+
return true;
|
|
539
|
+
}
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
398
542
|
/**
|
|
399
543
|
* Extract weather from scene elements
|
|
544
|
+
* Style-aware: respects franchise settings (no weather on spaceships!)
|
|
400
545
|
*/
|
|
401
546
|
extractWeather(scene) {
|
|
547
|
+
// Check if this is a space setting where weather doesn't make sense
|
|
548
|
+
if (this.isSpaceSetting(scene)) {
|
|
549
|
+
return undefined;
|
|
550
|
+
}
|
|
402
551
|
const weatherKeywords = ['rain', 'snow', 'fog', 'storm', 'sunny', 'cloudy', 'wind'];
|
|
403
552
|
for (const element of scene.elements) {
|
|
404
553
|
const text = element.text.toLowerCase();
|
|
@@ -412,13 +561,42 @@ class ContextBuilder {
|
|
|
412
561
|
}
|
|
413
562
|
/**
|
|
414
563
|
* Extract emotion from dialogue element
|
|
564
|
+
* Handles both parenthetical and character extension (V.O., O.S., etc.)
|
|
415
565
|
*/
|
|
416
566
|
extractEmotion(dialogueElement) {
|
|
417
|
-
|
|
567
|
+
const parts = [];
|
|
568
|
+
// Check character extension (V.O., O.S., etc.)
|
|
569
|
+
let hasVoiceOver = false;
|
|
570
|
+
let hasOffScreen = false;
|
|
571
|
+
if (dialogueElement.dialogue.character.extension) {
|
|
572
|
+
const ext = dialogueElement.dialogue.character.extension.replace(/[()]/g, '').trim();
|
|
573
|
+
if (ext === 'V.O.') {
|
|
574
|
+
parts.push('voice over');
|
|
575
|
+
hasVoiceOver = true;
|
|
576
|
+
}
|
|
577
|
+
else if (ext === 'O.S.') {
|
|
578
|
+
parts.push('off screen');
|
|
579
|
+
hasOffScreen = true;
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
parts.push(ext.toLowerCase());
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
// Check parenthetical for additional context (e.g., "over intercom")
|
|
418
586
|
if (dialogueElement.dialogue.parenthetical) {
|
|
419
|
-
|
|
587
|
+
let paren = dialogueElement.dialogue.parenthetical.replace(/[()]/g, '').trim();
|
|
588
|
+
// Skip if it's redundant with the extension
|
|
589
|
+
if (paren.toLowerCase().includes('v.o.') || paren.toLowerCase().includes('o.s.')) {
|
|
590
|
+
return parts.length > 0 ? parts.join(' ') : undefined;
|
|
591
|
+
}
|
|
592
|
+
// If we have voice over and parenthetical starts with "over", merge them
|
|
593
|
+
// "voice over" + "over intercom" -> "voice over intercom"
|
|
594
|
+
if (hasVoiceOver && paren.toLowerCase().startsWith('over ')) {
|
|
595
|
+
paren = paren.substring(5); // Remove "over " prefix
|
|
596
|
+
}
|
|
597
|
+
parts.push(paren);
|
|
420
598
|
}
|
|
421
|
-
return undefined;
|
|
599
|
+
return parts.length > 0 ? parts.join(' ') : undefined;
|
|
422
600
|
}
|
|
423
601
|
/**
|
|
424
602
|
* Extract ALL character introductions from an action line
|
|
@@ -467,27 +645,29 @@ class ContextBuilder {
|
|
|
467
645
|
physicalParts.push(part);
|
|
468
646
|
}
|
|
469
647
|
}
|
|
470
|
-
//
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
wardrobe: wardrobeParts.join(', '),
|
|
476
|
-
physicalAppearance: physicalParts.join(', '),
|
|
477
|
-
props: [],
|
|
478
|
-
lastSeenPosition: 'in scene',
|
|
479
|
-
lastSeenEmotion: undefined
|
|
480
|
-
};
|
|
481
|
-
this.characterBible.set(matchedName, bibleEntry);
|
|
648
|
+
// Detect if this is an entrance or exit
|
|
649
|
+
const lowerText = text.toLowerCase();
|
|
650
|
+
let position = 'in scene';
|
|
651
|
+
if (lowerText.includes('enters') || lowerText.includes('enter')) {
|
|
652
|
+
position = 'enters scene';
|
|
482
653
|
}
|
|
483
|
-
else {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
654
|
+
else if (lowerText.includes('exits') || lowerText.includes('exit')) {
|
|
655
|
+
position = 'exits scene';
|
|
656
|
+
}
|
|
657
|
+
// Enrich wardrobe with specific colors based on cinematic style
|
|
658
|
+
const enrichedWardrobe = this.enrichWardrobeWithColors(wardrobeParts.join(', '), matchedName, text);
|
|
659
|
+
// Update or create Character Bible entry using the matched name (with fuzzy matching)
|
|
660
|
+
let { bibleEntry } = this.getOrCreateCharacterInBible(matchedName);
|
|
661
|
+
// Update entry with extracted details
|
|
662
|
+
if (physicalParts.length > 0 && !bibleEntry.physicalAppearance) {
|
|
663
|
+
bibleEntry.physicalAppearance = physicalParts.join(', ');
|
|
664
|
+
}
|
|
665
|
+
if (wardrobeParts.length > 0 && !bibleEntry.wardrobe) {
|
|
666
|
+
bibleEntry.wardrobe = enrichedWardrobe;
|
|
667
|
+
}
|
|
668
|
+
// Update position if this is an entrance/exit
|
|
669
|
+
if (position !== 'in scene') {
|
|
670
|
+
bibleEntry.lastSeenPosition = position;
|
|
491
671
|
}
|
|
492
672
|
}
|
|
493
673
|
}
|
|
@@ -537,14 +717,14 @@ class ContextBuilder {
|
|
|
537
717
|
if (intro) {
|
|
538
718
|
if (intro.physicalAppearance) {
|
|
539
719
|
state.physicalAppearance = intro.physicalAppearance;
|
|
540
|
-
const bibleEntry = this.
|
|
720
|
+
const bibleEntry = this.findCharacterInBible(name);
|
|
541
721
|
if (bibleEntry) {
|
|
542
722
|
bibleEntry.physicalAppearance = intro.physicalAppearance;
|
|
543
723
|
}
|
|
544
724
|
}
|
|
545
725
|
if (intro.wardrobe) {
|
|
546
726
|
state.wardrobe = intro.wardrobe;
|
|
547
|
-
const bibleEntry = this.
|
|
727
|
+
const bibleEntry = this.findCharacterInBible(name);
|
|
548
728
|
if (bibleEntry) {
|
|
549
729
|
bibleEntry.wardrobe = intro.wardrobe;
|
|
550
730
|
}
|
|
@@ -552,8 +732,14 @@ class ContextBuilder {
|
|
|
552
732
|
}
|
|
553
733
|
}
|
|
554
734
|
for (const [name, state] of characterMap.entries()) {
|
|
555
|
-
|
|
735
|
+
// Check for character name (case-insensitive, and check for partial matches)
|
|
736
|
+
// Example: "CLIF" should match "Clif" or "WIZARD CLIF HIGH" should match "Clif"
|
|
737
|
+
const nameParts = name.split(/\s+/);
|
|
738
|
+
const textLower = text.toLowerCase();
|
|
739
|
+
const nameMatches = nameParts.some(part => part.length > 2 && textLower.includes(part.toLowerCase()));
|
|
740
|
+
if (nameMatches || text.includes(name)) {
|
|
556
741
|
state.action = text;
|
|
742
|
+
console.log(`[DEBUG] Assigning action to ${name}: "${text.substring(0, 100)}..."`);
|
|
557
743
|
// Extract position/blocking keywords
|
|
558
744
|
const positionKeywords = [
|
|
559
745
|
'stands', 'sits', 'walks', 'enters', 'exits', 'moves',
|
|
@@ -562,6 +748,15 @@ class ContextBuilder {
|
|
|
562
748
|
];
|
|
563
749
|
for (const keyword of positionKeywords) {
|
|
564
750
|
if (text.toLowerCase().includes(keyword)) {
|
|
751
|
+
// Special handling for "enters" and "exits" - use simple position
|
|
752
|
+
if (keyword === 'enters' && text.toLowerCase().includes('enters')) {
|
|
753
|
+
state.position = 'enters scene';
|
|
754
|
+
break;
|
|
755
|
+
}
|
|
756
|
+
else if (keyword === 'exits' && text.toLowerCase().includes('exits')) {
|
|
757
|
+
state.position = 'exits scene';
|
|
758
|
+
break;
|
|
759
|
+
}
|
|
565
760
|
// Extract sentence containing the keyword for position context
|
|
566
761
|
const sentences = text.split(/[.!?]/);
|
|
567
762
|
for (const sentence of sentences) {
|
|
@@ -595,7 +790,7 @@ class ContextBuilder {
|
|
|
595
790
|
// Join multiple sentences for verbose description
|
|
596
791
|
state.wardrobe = relevantSentences.join('. ');
|
|
597
792
|
// Update Character Bible
|
|
598
|
-
const bibleEntry = this.
|
|
793
|
+
const bibleEntry = this.findCharacterInBible(name);
|
|
599
794
|
if (bibleEntry) {
|
|
600
795
|
bibleEntry.wardrobe = relevantSentences.join('. ');
|
|
601
796
|
}
|
|
@@ -627,7 +822,7 @@ class ContextBuilder {
|
|
|
627
822
|
// Join multiple sentences for verbose description
|
|
628
823
|
state.physicalAppearance = relevantSentences.join('. ');
|
|
629
824
|
// Update Character Bible
|
|
630
|
-
const bibleEntry = this.
|
|
825
|
+
const bibleEntry = this.findCharacterInBible(name);
|
|
631
826
|
if (bibleEntry) {
|
|
632
827
|
bibleEntry.physicalAppearance = relevantSentences.join('. ');
|
|
633
828
|
}
|
|
@@ -648,7 +843,7 @@ class ContextBuilder {
|
|
|
648
843
|
for (const sentence of sentences) {
|
|
649
844
|
if (sentence.toLowerCase().includes(keyword) && sentence.includes(name)) {
|
|
650
845
|
// Update Character Bible with prop
|
|
651
|
-
const bibleEntry = this.
|
|
846
|
+
const bibleEntry = this.findCharacterInBible(name);
|
|
652
847
|
if (bibleEntry) {
|
|
653
848
|
const prop = sentence.trim();
|
|
654
849
|
if (!bibleEntry.props.includes(prop)) {
|
|
@@ -664,6 +859,107 @@ class ContextBuilder {
|
|
|
664
859
|
}
|
|
665
860
|
}
|
|
666
861
|
}
|
|
862
|
+
/**
|
|
863
|
+
* Enrich wardrobe descriptions with specific colors based on cinematic style
|
|
864
|
+
* Maintains visual continuity by adding particular details
|
|
865
|
+
*
|
|
866
|
+
* Star Trek uniform colors:
|
|
867
|
+
* - Command: Gold/Yellow
|
|
868
|
+
* - Science/Medical: Blue
|
|
869
|
+
* - Engineering/Security: Red
|
|
870
|
+
*/
|
|
871
|
+
enrichWardrobeWithColors(wardrobe, characterName, fullText) {
|
|
872
|
+
if (!wardrobe || !this.styleGuidelines) {
|
|
873
|
+
return wardrobe;
|
|
874
|
+
}
|
|
875
|
+
// Check if this is a Star Trek style
|
|
876
|
+
const isStarTrek = this.styleGuidelines.appliedStyles.some(style => style.toLowerCase().includes('star trek'));
|
|
877
|
+
if (!isStarTrek) {
|
|
878
|
+
return wardrobe; // Only enrich for Star Trek for now
|
|
879
|
+
}
|
|
880
|
+
// If wardrobe already has color, don't override
|
|
881
|
+
const hasColor = /\b(red|blue|gold|yellow|green|black|white|gray|grey|purple|orange|brown)\b/i.test(wardrobe);
|
|
882
|
+
if (hasColor) {
|
|
883
|
+
return wardrobe;
|
|
884
|
+
}
|
|
885
|
+
// Determine department from wardrobe or character context
|
|
886
|
+
const lowerWardrobe = wardrobe.toLowerCase();
|
|
887
|
+
const lowerText = fullText.toLowerCase();
|
|
888
|
+
let color = '';
|
|
889
|
+
// Engineering/Security = Red
|
|
890
|
+
if (lowerWardrobe.includes('engineering') || lowerWardrobe.includes('security') ||
|
|
891
|
+
lowerText.includes('engineer') || lowerText.includes('security')) {
|
|
892
|
+
color = 'red';
|
|
893
|
+
}
|
|
894
|
+
// Science/Medical = Blue
|
|
895
|
+
else if (lowerWardrobe.includes('science') || lowerWardrobe.includes('medical') ||
|
|
896
|
+
lowerText.includes('science') || lowerText.includes('medical') || lowerText.includes('doctor')) {
|
|
897
|
+
color = 'blue';
|
|
898
|
+
}
|
|
899
|
+
// Command = Gold
|
|
900
|
+
else if (lowerWardrobe.includes('command') || lowerWardrobe.includes('captain') ||
|
|
901
|
+
lowerText.includes('captain') || lowerText.includes('commander')) {
|
|
902
|
+
color = 'gold';
|
|
903
|
+
}
|
|
904
|
+
// Default to red for generic Starfleet uniforms
|
|
905
|
+
else if (lowerWardrobe.includes('starfleet') || lowerWardrobe.includes('uniform')) {
|
|
906
|
+
color = 'red'; // Default to engineering red
|
|
907
|
+
}
|
|
908
|
+
// Add color to wardrobe description
|
|
909
|
+
if (color) {
|
|
910
|
+
// Insert color before "uniform" or "robe" if present
|
|
911
|
+
if (lowerWardrobe.includes('uniform')) {
|
|
912
|
+
return wardrobe.replace(/uniform/i, `${color} uniform`);
|
|
913
|
+
}
|
|
914
|
+
else if (lowerWardrobe.includes('robe')) {
|
|
915
|
+
return wardrobe.replace(/robe/i, `${color} robe`);
|
|
916
|
+
}
|
|
917
|
+
else {
|
|
918
|
+
// Prepend color
|
|
919
|
+
return `${color} ${wardrobe}`;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
return wardrobe;
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Extract characters using AI-powered fuzzy logic
|
|
926
|
+
* Follows MPAA and AMPAS Nicholl Fellowship screenplay standards
|
|
927
|
+
*/
|
|
928
|
+
async extractCharactersWithAI(elements, sceneContext) {
|
|
929
|
+
// Build scene text from all elements
|
|
930
|
+
const sceneText = elements.map(el => {
|
|
931
|
+
if (el.type === 'dialogue') {
|
|
932
|
+
const dialogueEl = el;
|
|
933
|
+
return `${dialogueEl.dialogue.character.name}\n${dialogueEl.dialogue.speech}`;
|
|
934
|
+
}
|
|
935
|
+
else if (el.type === 'action') {
|
|
936
|
+
const actionEl = el;
|
|
937
|
+
return actionEl.text;
|
|
938
|
+
}
|
|
939
|
+
return '';
|
|
940
|
+
}).join('\n\n');
|
|
941
|
+
// Use AI extractor
|
|
942
|
+
const result = await this.aiExtractor.extractEntities(sceneText, sceneContext.set);
|
|
943
|
+
// Combine AI-extracted characters with dialogue characters
|
|
944
|
+
const characterNames = [];
|
|
945
|
+
// Add dialogue characters (always include these)
|
|
946
|
+
for (const element of elements) {
|
|
947
|
+
if (element.type === 'dialogue') {
|
|
948
|
+
const dialogueElement = element;
|
|
949
|
+
const characterName = dialogueElement.dialogue.character.name;
|
|
950
|
+
if (!characterNames.includes(characterName)) {
|
|
951
|
+
characterNames.push(characterName);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
// Add AI-extracted characters
|
|
956
|
+
for (const character of result.characters) {
|
|
957
|
+
if (!characterNames.includes(character)) {
|
|
958
|
+
characterNames.push(character);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
return characterNames;
|
|
962
|
+
}
|
|
667
963
|
}
|
|
668
964
|
exports.ContextBuilder = ContextBuilder;
|
|
669
965
|
//# sourceMappingURL=context-builder.js.map
|