@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.
Files changed (33) hide show
  1. package/cli/dist/cli.js +2 -0
  2. package/cli/dist/cli.js.map +1 -1
  3. package/cli/dist/commands/generate-shot-list/generator/ai-blocking-extractor.d.ts +58 -0
  4. package/cli/dist/commands/generate-shot-list/generator/ai-blocking-extractor.d.ts.map +1 -0
  5. package/cli/dist/commands/generate-shot-list/generator/ai-blocking-extractor.js +275 -0
  6. package/cli/dist/commands/generate-shot-list/generator/ai-blocking-extractor.js.map +1 -0
  7. package/cli/dist/commands/generate-shot-list/generator/ai-entity-extractor.d.ts +41 -0
  8. package/cli/dist/commands/generate-shot-list/generator/ai-entity-extractor.d.ts.map +1 -0
  9. package/cli/dist/commands/generate-shot-list/generator/ai-entity-extractor.js +155 -0
  10. package/cli/dist/commands/generate-shot-list/generator/ai-entity-extractor.js.map +1 -0
  11. package/cli/dist/commands/generate-shot-list/generator/context-builder.d.ts +65 -2
  12. package/cli/dist/commands/generate-shot-list/generator/context-builder.d.ts.map +1 -1
  13. package/cli/dist/commands/generate-shot-list/generator/context-builder.js +394 -98
  14. package/cli/dist/commands/generate-shot-list/generator/context-builder.js.map +1 -1
  15. package/cli/dist/commands/generate-shot-list/generator/index.d.ts +59 -1
  16. package/cli/dist/commands/generate-shot-list/generator/index.d.ts.map +1 -1
  17. package/cli/dist/commands/generate-shot-list/generator/index.js +364 -34
  18. package/cli/dist/commands/generate-shot-list/generator/index.js.map +1 -1
  19. package/cli/dist/commands/generate-shot-list/generator/types.d.ts +3 -2
  20. package/cli/dist/commands/generate-shot-list/generator/types.d.ts.map +1 -1
  21. package/cli/dist/commands/generate-shot-list/generator/validator.d.ts +33 -0
  22. package/cli/dist/commands/generate-shot-list/generator/validator.d.ts.map +1 -1
  23. package/cli/dist/commands/generate-shot-list/generator/validator.js +167 -0
  24. package/cli/dist/commands/generate-shot-list/generator/validator.js.map +1 -1
  25. package/cli/dist/commands/generate-shot-list/help-text.d.ts +1 -1
  26. package/cli/dist/commands/generate-shot-list/help-text.d.ts.map +1 -1
  27. package/cli/dist/commands/generate-shot-list/help-text.js +11 -0
  28. package/cli/dist/commands/generate-shot-list/help-text.js.map +1 -1
  29. package/cli/dist/commands/generate-shot-list.d.ts +1 -0
  30. package/cli/dist/commands/generate-shot-list.d.ts.map +1 -1
  31. package/cli/dist/commands/generate-shot-list.js +7 -4
  32. package/cli/dist/commands/generate-shot-list.js.map +1 -1
  33. 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: Extract character introductions from action lines using known character names
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: Collect ALL character names from dialogue AND action lines
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 === 'dialogue') {
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 regex = /\b([A-Z]{2,}(?:\s+[A-Z]{2,})*)\b/g;
108
- let match;
109
- while ((match = regex.exec(actionElement.text)) !== null) {
110
- const name = match[1].trim();
111
- // Skip common words that are all caps
112
- if (!['INT', 'EXT', 'DAY', 'NIGHT', 'CONTINUOUS', 'LATER', 'PADD', 'FLU'].includes(name) &&
113
- !characterNames.includes(name)) {
114
- characterNames.push(name);
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
- if (!characterMap.has(characterName)) {
133
- // Check Character Bible first (try exact match, then partial match)
134
- let bibleEntry = this.characterBible.get(characterName);
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
- if (!bibleEntry && previousState) {
147
- // Create Character Bible entry from previous state
148
- bibleEntry = {
149
- name: characterName,
150
- wardrobe: previousState.wardrobe || '',
151
- physicalAppearance: previousState.physicalAppearance || '',
152
- props: [],
153
- lastSeenPosition: previousState.position || 'in scene',
154
- lastSeenEmotion: previousState.emotion
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
- else if (!bibleEntry) {
159
- // Create minimal Character Bible entry (will be populated from action lines)
160
- bibleEntry = {
161
- name: characterName,
162
- wardrobe: '',
163
- physicalAppearance: '',
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
- // Build rich character state from Bible
171
- characterMap.set(characterName, {
172
- name: characterName,
173
- position: bibleEntry.lastSeenPosition,
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(characterName).emotion = emotion;
187
- // Update Bible
188
- const bibleEntry = this.characterBible.get(characterName);
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.characterBible.get(name);
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 = state.position;
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
- // Check parenthetical for emotion
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
- return dialogueElement.dialogue.parenthetical.replace(/[()]/g, '').trim();
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
- // Update or create Character Bible entry using the matched name
471
- let bibleEntry = this.characterBible.get(matchedName);
472
- if (!bibleEntry) {
473
- bibleEntry = {
474
- name: matchedName,
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
- // Update existing entry if we have more details
485
- if (physicalParts.length > 0 && !bibleEntry.physicalAppearance) {
486
- bibleEntry.physicalAppearance = physicalParts.join(', ');
487
- }
488
- if (wardrobeParts.length > 0 && !bibleEntry.wardrobe) {
489
- bibleEntry.wardrobe = wardrobeParts.join(', ');
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.characterBible.get(name);
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.characterBible.get(name);
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
- if (text.includes(name)) {
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.characterBible.get(name);
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.characterBible.get(name);
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.characterBible.get(name);
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