@mytechtoday/augment-extensions 2.3.7 → 2.5.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/augment-extensions/writing-standards/screenplay/commercials/CHARACTER-COUNT-TRACKING.md +226 -0
- package/augment-extensions/writing-standards/screenplay/commercials/CONFIGURATION.md +327 -0
- package/augment-extensions/writing-standards/screenplay/commercials/README.md +223 -0
- package/augment-extensions/writing-standards/screenplay/commercials/REFACTORING-SUMMARY.md +193 -0
- package/augment-extensions/writing-standards/screenplay/commercials/REFERENCES.md +157 -0
- package/augment-extensions/writing-standards/screenplay/commercials/VALIDATION-REPORT.md +109 -0
- package/augment-extensions/writing-standards/screenplay/commercials/examples/.gitkeep +0 -0
- package/augment-extensions/writing-standards/screenplay/commercials/module.json +111 -0
- package/augment-extensions/writing-standards/screenplay/commercials/rules/.gitkeep +0 -0
- package/augment-extensions/writing-standards/screenplay/commercials/rules/commercial-audience.md +343 -0
- package/augment-extensions/writing-standards/screenplay/commercials/rules/commercial-formats.md +396 -0
- package/augment-extensions/writing-standards/screenplay/commercials/rules/commercial-persuasion.md +387 -0
- package/augment-extensions/writing-standards/screenplay/commercials/rules/commercial-scripts.md +531 -0
- package/augment-extensions/writing-standards/screenplay/commercials/rules/commercial-techniques.md +912 -0
- package/augment-extensions/writing-standards/screenplay/commercials/rules/commercial-types.md +700 -0
- package/augment-extensions/writing-standards/screenplay/commercials/rules/commercials.md +285 -0
- package/augment-extensions/writing-standards/screenplay/commercials/scripts/validate-character-count.ts +284 -0
- 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 +5 -1
|
@@ -11,6 +11,7 @@ const validator_1 = require("./validator");
|
|
|
11
11
|
const scene_segmenter_1 = require("./scene-segmenter");
|
|
12
12
|
const context_builder_1 = require("./context-builder");
|
|
13
13
|
const metadata_extractor_1 = require("./metadata-extractor");
|
|
14
|
+
const ai_blocking_extractor_1 = require("./ai-blocking-extractor");
|
|
14
15
|
/**
|
|
15
16
|
* Default generator implementation
|
|
16
17
|
*/
|
|
@@ -18,27 +19,37 @@ class ShotListGenerator {
|
|
|
18
19
|
constructor(styleGuidelines) {
|
|
19
20
|
this.validator = (0, validator_1.createValidator)();
|
|
20
21
|
this.styleGuidelines = null;
|
|
22
|
+
this.characterBlockingCache = new Map();
|
|
23
|
+
// Store style guidelines first
|
|
24
|
+
this.styleGuidelines = styleGuidelines || null;
|
|
21
25
|
// Initialize modules with default configurations
|
|
22
26
|
this.segmenter = new scene_segmenter_1.SceneSegmenter({
|
|
23
27
|
maxShotLength: 30, // 30 seconds max
|
|
24
28
|
targetShotLength: 15, // 15 seconds target
|
|
25
29
|
minShotLength: 5 // 5 seconds minimum
|
|
26
30
|
});
|
|
31
|
+
// Pass style guidelines to ContextBuilder so it can filter appropriately
|
|
27
32
|
this.contextBuilder = new context_builder_1.ContextBuilder({
|
|
28
33
|
includeAtmosphere: true,
|
|
29
34
|
includeWeather: true,
|
|
30
|
-
trackCharacterEmotions: true
|
|
35
|
+
trackCharacterEmotions: true,
|
|
36
|
+
styleGuidelines: this.styleGuidelines
|
|
31
37
|
});
|
|
32
38
|
this.metadataExtractor = new metadata_extractor_1.MetadataExtractor({
|
|
33
39
|
extractTechnicalNotes: true,
|
|
34
40
|
inferFromContext: true
|
|
35
41
|
});
|
|
36
|
-
this.
|
|
42
|
+
this.blockingExtractor = new ai_blocking_extractor_1.AIBlockingExtractor(undefined, this.styleGuidelines);
|
|
37
43
|
}
|
|
38
44
|
/**
|
|
39
45
|
* Generate shot list from parsed screenplay
|
|
40
46
|
*/
|
|
41
|
-
generate(scenes, config) {
|
|
47
|
+
async generate(scenes, config) {
|
|
48
|
+
// CRITICAL: Reset all state at the start of each screenplay generation
|
|
49
|
+
// This prevents Character Bible contamination between different screenplays
|
|
50
|
+
this.contextBuilder.reset();
|
|
51
|
+
this.characterBlockingCache.clear();
|
|
52
|
+
this.blockingExtractor.clearCache(); // Clear character description cache
|
|
42
53
|
const shots = [];
|
|
43
54
|
let shotNumber = 1;
|
|
44
55
|
const warnings = [];
|
|
@@ -46,7 +57,7 @@ class ShotListGenerator {
|
|
|
46
57
|
for (const scene of scenes) {
|
|
47
58
|
// TODO: Implement scene segmentation (bd-shot-list-3.2)
|
|
48
59
|
// For now, create one shot per scene as placeholder
|
|
49
|
-
const sceneShots = this.segmentScene(scene, shotNumber, config);
|
|
60
|
+
const sceneShots = await this.segmentScene(scene, shotNumber, config);
|
|
50
61
|
shots.push(...sceneShots);
|
|
51
62
|
shotNumber += sceneShots.length;
|
|
52
63
|
}
|
|
@@ -96,7 +107,7 @@ class ShotListGenerator {
|
|
|
96
107
|
* Implemented: bd-shot-list-3.2
|
|
97
108
|
* Enhanced: bd-2b68 (Requirement 2: Intelligent Shot Splitting)
|
|
98
109
|
*/
|
|
99
|
-
segmentScene(scene, startShotNumber, config) {
|
|
110
|
+
async segmentScene(scene, startShotNumber, config) {
|
|
100
111
|
// Build scene-level context once
|
|
101
112
|
const sceneContext = this.contextBuilder.buildSceneContext(scene);
|
|
102
113
|
// Segment scene into shots
|
|
@@ -108,20 +119,30 @@ class ShotListGenerator {
|
|
|
108
119
|
const segment = segments[i];
|
|
109
120
|
const isFirstShot = i === 0;
|
|
110
121
|
// Build character states for this segment
|
|
111
|
-
const characters = this.contextBuilder.buildCharacterStates(segment.elements, sceneContext);
|
|
122
|
+
const characters = await this.contextBuilder.buildCharacterStates(segment.elements, sceneContext);
|
|
112
123
|
// Extract metadata for this segment
|
|
113
124
|
const metadata = this.metadataExtractor.extract(segment.elements, isFirstShot);
|
|
114
125
|
// Apply style guidelines if available
|
|
115
126
|
if (this.styleGuidelines) {
|
|
116
127
|
this.applyStyleToMetadata(metadata, this.styleGuidelines);
|
|
117
128
|
}
|
|
129
|
+
// Extract blocking using AI
|
|
130
|
+
const blockingData = await this.extractBlockingWithAI(segment.elements, characters);
|
|
131
|
+
// Apply AI-generated character descriptions to character states
|
|
132
|
+
this.applyCharacterDescriptions(characters, blockingData.characterDescriptions);
|
|
118
133
|
// Build structured description components
|
|
119
134
|
const set = this.buildSetDescription(sceneContext);
|
|
120
|
-
const description = this.buildVisualDescription(sceneContext);
|
|
121
|
-
const actions =
|
|
135
|
+
const description = blockingData.setDescription || this.buildVisualDescription(sceneContext);
|
|
136
|
+
const actions = blockingData.characterActions.length > 0
|
|
137
|
+
? blockingData.characterActions.join('. ')
|
|
138
|
+
: 'No specific actions in this shot';
|
|
122
139
|
const dialogue = this.extractDialogue(segment.elements);
|
|
123
|
-
const blocking = this.
|
|
124
|
-
const sfx =
|
|
140
|
+
const blocking = this.buildBlockingFromAI(blockingData.characterPositions, characters);
|
|
141
|
+
const sfx = config.muteSfx
|
|
142
|
+
? 'No sound effects specified'
|
|
143
|
+
: (blockingData.soundEffects.length > 0
|
|
144
|
+
? blockingData.soundEffects.join('. ')
|
|
145
|
+
: 'No sound effects specified');
|
|
125
146
|
const techDetails = this.buildTechDetails(metadata);
|
|
126
147
|
// Calculate total character count from all components
|
|
127
148
|
const totalCharacterCount = set.length + description.length + actions.length +
|
|
@@ -129,7 +150,7 @@ class ShotListGenerator {
|
|
|
129
150
|
// Check if shot exceeds max duration (Requirement 2)
|
|
130
151
|
if (segment.estimatedDuration > config.maxShotLength) {
|
|
131
152
|
// Split into sub-shots
|
|
132
|
-
const subShots = this.splitIntoSubShots(segment, currentShotNumber, scene, sceneContext, config);
|
|
153
|
+
const subShots = await this.splitIntoSubShots(segment, currentShotNumber, scene, sceneContext, config);
|
|
133
154
|
shots.push(...subShots);
|
|
134
155
|
currentShotNumber += subShots.length;
|
|
135
156
|
}
|
|
@@ -163,7 +184,7 @@ class ShotListGenerator {
|
|
|
163
184
|
* Requirement 2: Intelligent Shot Splitting for Long Durations
|
|
164
185
|
* Recursively splits sub-shots that still exceed maxShotLength
|
|
165
186
|
*/
|
|
166
|
-
splitIntoSubShots(segment, baseShotNumber, scene, sceneContext, config) {
|
|
187
|
+
async splitIntoSubShots(segment, baseShotNumber, scene, sceneContext, config) {
|
|
167
188
|
const subShots = [];
|
|
168
189
|
const elements = segment.elements;
|
|
169
190
|
const targetDuration = config.maxShotLength;
|
|
@@ -180,20 +201,30 @@ class ShotListGenerator {
|
|
|
180
201
|
? `${baseShotNumber}.${i + 1}` // Nested: "3a.1", "3a.2" or "3a.1.1", "3a.1.2"
|
|
181
202
|
: `${baseShotNumber}${String.fromCharCode(97 + i)}`; // First level: "3a", "3b"
|
|
182
203
|
// Build character states for this sub-shot
|
|
183
|
-
const characters = this.contextBuilder.buildCharacterStates(subShotElements, sceneContext);
|
|
204
|
+
const characters = await this.contextBuilder.buildCharacterStates(subShotElements, sceneContext);
|
|
184
205
|
// Extract metadata for this sub-shot
|
|
185
206
|
const metadata = this.metadataExtractor.extract(subShotElements, i === 0);
|
|
186
207
|
// Apply style guidelines if available
|
|
187
208
|
if (this.styleGuidelines) {
|
|
188
209
|
this.applyStyleToMetadata(metadata, this.styleGuidelines);
|
|
189
210
|
}
|
|
211
|
+
// Extract blocking using AI
|
|
212
|
+
const blockingData = await this.extractBlockingWithAI(subShotElements, characters);
|
|
213
|
+
// Apply AI-generated character descriptions to character states
|
|
214
|
+
this.applyCharacterDescriptions(characters, blockingData.characterDescriptions);
|
|
190
215
|
// Build structured description components
|
|
191
216
|
const set = this.buildSetDescription(sceneContext);
|
|
192
|
-
const description = this.buildVisualDescription(sceneContext);
|
|
193
|
-
const actions =
|
|
217
|
+
const description = blockingData.setDescription || this.buildVisualDescription(sceneContext);
|
|
218
|
+
const actions = blockingData.characterActions.length > 0
|
|
219
|
+
? blockingData.characterActions.join('. ')
|
|
220
|
+
: 'No specific actions in this shot';
|
|
194
221
|
const dialogue = this.extractDialogue(subShotElements);
|
|
195
|
-
const blocking = this.
|
|
196
|
-
const sfx =
|
|
222
|
+
const blocking = this.buildBlockingFromAI(blockingData.characterPositions, characters);
|
|
223
|
+
const sfx = config.muteSfx
|
|
224
|
+
? 'No sound effects specified'
|
|
225
|
+
: (blockingData.soundEffects.length > 0
|
|
226
|
+
? blockingData.soundEffects.join('. ')
|
|
227
|
+
: 'No sound effects specified');
|
|
197
228
|
const techDetails = this.buildTechDetails(metadata);
|
|
198
229
|
// Calculate total character count from all components
|
|
199
230
|
const totalCharacterCount = set.length + description.length + actions.length +
|
|
@@ -211,7 +242,7 @@ class ShotListGenerator {
|
|
|
211
242
|
breakReason: 'duration'
|
|
212
243
|
};
|
|
213
244
|
// Recursively split this sub-shot
|
|
214
|
-
const nestedSubShots = this.splitIntoSubShots(nestedSegment, subShotNumber, scene, sceneContext, config);
|
|
245
|
+
const nestedSubShots = await this.splitIntoSubShots(nestedSegment, subShotNumber, scene, sceneContext, config);
|
|
215
246
|
subShots.push(...nestedSubShots);
|
|
216
247
|
}
|
|
217
248
|
// If we have a single dialogue element, split the dialogue text
|
|
@@ -225,7 +256,7 @@ class ShotListGenerator {
|
|
|
225
256
|
breakReason: 'duration'
|
|
226
257
|
};
|
|
227
258
|
// Recursively split the dialogue-based segment
|
|
228
|
-
const dialogueSubShots = this.splitIntoSubShots(dialogueSegment, subShotNumber, scene, sceneContext, config);
|
|
259
|
+
const dialogueSubShots = await this.splitIntoSubShots(dialogueSegment, subShotNumber, scene, sceneContext, config);
|
|
229
260
|
subShots.push(...dialogueSubShots);
|
|
230
261
|
}
|
|
231
262
|
// Single non-dialogue element that can't be split further
|
|
@@ -412,21 +443,78 @@ class ShotListGenerator {
|
|
|
412
443
|
}
|
|
413
444
|
/**
|
|
414
445
|
* Build character actions from scene elements
|
|
446
|
+
* Includes both explicit actions and inferred actions from dialogue
|
|
415
447
|
*/
|
|
416
448
|
buildActions(elements) {
|
|
449
|
+
const actionList = [];
|
|
450
|
+
// Extract explicit action descriptions
|
|
451
|
+
// Include ALL actions (character entrances, movements, and general actions)
|
|
452
|
+
// Only filter out pure sound effects
|
|
417
453
|
const actionElements = elements.filter(el => el.type === 'action');
|
|
418
|
-
|
|
454
|
+
const explicitActions = actionElements
|
|
455
|
+
.map(el => el.text)
|
|
456
|
+
.filter(text => {
|
|
457
|
+
// Keep the action if it's NOT a pure sound effect
|
|
458
|
+
// Character entrances should always be included
|
|
459
|
+
return !this.isSoundEffect(text) || this.isCharacterEntrance(text);
|
|
460
|
+
});
|
|
461
|
+
actionList.push(...explicitActions);
|
|
462
|
+
// Infer actions from dialogue (e.g., spell-casting, shouting, etc.)
|
|
463
|
+
const dialogueElements = elements.filter((el) => el.type === 'dialogue');
|
|
464
|
+
for (const dialogueEl of dialogueElements) {
|
|
465
|
+
const inferredAction = this.inferActionFromDialogue(dialogueEl);
|
|
466
|
+
if (inferredAction) {
|
|
467
|
+
actionList.push(inferredAction);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (actionList.length === 0) {
|
|
419
471
|
return 'No specific actions in this shot';
|
|
420
472
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
473
|
+
return actionList.join('. ');
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Infer character actions from dialogue content
|
|
477
|
+
* Examples:
|
|
478
|
+
* - Spell incantations -> "waves staff and casts spell"
|
|
479
|
+
* - Shouting -> "shouts"
|
|
480
|
+
* - Whispering -> "whispers"
|
|
481
|
+
*/
|
|
482
|
+
inferActionFromDialogue(dialogueEl) {
|
|
483
|
+
const characterName = dialogueEl.dialogue.character.name;
|
|
484
|
+
const speech = dialogueEl.dialogue.speech;
|
|
485
|
+
const parenthetical = dialogueEl.dialogue.parenthetical?.toLowerCase() || '';
|
|
486
|
+
// Detect spell incantations - ONLY specific magical phrases
|
|
487
|
+
// Removed overly broad Latin-like pattern that matched treaty names
|
|
488
|
+
const spellPatterns = [
|
|
489
|
+
/\b(Expecto|Lumos|Accio|Wingardium|Expelliarmus|Avada)/i, // Harry Potter spells
|
|
490
|
+
/\b(Abracadabra|Alakazam|Hocus Pocus|Presto)/i, // Classic magic words
|
|
491
|
+
];
|
|
492
|
+
const isSpell = spellPatterns.some(pattern => pattern.test(speech));
|
|
493
|
+
if (isSpell) {
|
|
494
|
+
// Check if character has a staff/wand in their props
|
|
495
|
+
// For now, assume wizards have staffs/wands
|
|
496
|
+
return `${characterName} waves staff and casts spell`;
|
|
497
|
+
}
|
|
498
|
+
// Detect shouting (ALL CAPS or exclamation marks)
|
|
499
|
+
if (speech === speech.toUpperCase() && speech.length > 10) {
|
|
500
|
+
return `${characterName} shouts`;
|
|
501
|
+
}
|
|
502
|
+
// Detect whispering from parenthetical
|
|
503
|
+
if (parenthetical.includes('whisper')) {
|
|
504
|
+
return `${characterName} whispers`;
|
|
505
|
+
}
|
|
506
|
+
// Detect other emotional delivery
|
|
507
|
+
if (parenthetical.includes('angry') || parenthetical.includes('furious')) {
|
|
508
|
+
return `${characterName} speaks angrily`;
|
|
509
|
+
}
|
|
510
|
+
if (parenthetical.includes('sad') || parenthetical.includes('crying')) {
|
|
511
|
+
return `${characterName} speaks sadly`;
|
|
512
|
+
}
|
|
513
|
+
return null;
|
|
427
514
|
}
|
|
428
515
|
/**
|
|
429
516
|
* Build character blocking (positions and movements)
|
|
517
|
+
* DEPRECATED: Use buildBlockingFromAI() instead
|
|
430
518
|
*/
|
|
431
519
|
buildBlocking(characters) {
|
|
432
520
|
if (characters.length === 0) {
|
|
@@ -448,19 +536,135 @@ class ShotListGenerator {
|
|
|
448
536
|
? blockingParts.join('. ')
|
|
449
537
|
: 'Characters maintain their positions';
|
|
450
538
|
}
|
|
539
|
+
/**
|
|
540
|
+
* Extract blocking using AI-powered spatial inference
|
|
541
|
+
*/
|
|
542
|
+
async extractBlockingWithAI(elements, characters) {
|
|
543
|
+
// Extract action lines
|
|
544
|
+
const actionLines = elements
|
|
545
|
+
.filter(el => el.type === 'action')
|
|
546
|
+
.map(el => el.text);
|
|
547
|
+
if (actionLines.length === 0) {
|
|
548
|
+
return {
|
|
549
|
+
characterPositions: [],
|
|
550
|
+
characterDescriptions: [],
|
|
551
|
+
setDescription: '',
|
|
552
|
+
characterActions: [],
|
|
553
|
+
soundEffects: []
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
// Get character names
|
|
557
|
+
const characterNames = characters.map(c => c.name);
|
|
558
|
+
// Call AI blocking extractor
|
|
559
|
+
console.log(`Extracting blocking with AI for ${characterNames.length} characters...`);
|
|
560
|
+
const result = await this.blockingExtractor.extractBlocking(actionLines, characterNames, this.characterBlockingCache);
|
|
561
|
+
// Update cache with new positions
|
|
562
|
+
for (const pos of result.characterPositions) {
|
|
563
|
+
this.characterBlockingCache.set(pos.character, pos);
|
|
564
|
+
}
|
|
565
|
+
return result;
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Build blocking description from AI-extracted positions
|
|
569
|
+
*/
|
|
570
|
+
buildBlockingFromAI(positions, characters) {
|
|
571
|
+
if (positions.length === 0) {
|
|
572
|
+
// Fallback to cached positions
|
|
573
|
+
const cachedBlocking = [];
|
|
574
|
+
for (const char of characters) {
|
|
575
|
+
const cached = this.characterBlockingCache.get(char.name);
|
|
576
|
+
if (cached) {
|
|
577
|
+
cachedBlocking.push(this.formatBlockingPosition(cached));
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return cachedBlocking.length > 0
|
|
581
|
+
? cachedBlocking.join('\n')
|
|
582
|
+
: 'Characters maintain their positions';
|
|
583
|
+
}
|
|
584
|
+
return positions.map(pos => this.formatBlockingPosition(pos)).join('\n');
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Format a single character blocking position
|
|
588
|
+
*/
|
|
589
|
+
formatBlockingPosition(pos) {
|
|
590
|
+
const parts = [pos.character + ':'];
|
|
591
|
+
if (pos.action) {
|
|
592
|
+
parts.push(pos.action);
|
|
593
|
+
}
|
|
594
|
+
if (pos.position) {
|
|
595
|
+
parts.push(pos.position);
|
|
596
|
+
}
|
|
597
|
+
if (pos.stagePosition) {
|
|
598
|
+
parts.push(`(${pos.stagePosition})`);
|
|
599
|
+
}
|
|
600
|
+
if (pos.relativePosition) {
|
|
601
|
+
parts.push(`[${pos.relativePosition}]`);
|
|
602
|
+
}
|
|
603
|
+
return parts.join(' ');
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Apply AI-generated character descriptions to character states
|
|
607
|
+
*/
|
|
608
|
+
applyCharacterDescriptions(characters, descriptions) {
|
|
609
|
+
for (const desc of descriptions) {
|
|
610
|
+
const char = characters.find(c => c.name === desc.character);
|
|
611
|
+
if (char) {
|
|
612
|
+
if (desc.physicalAppearance) {
|
|
613
|
+
char.physicalAppearance = desc.physicalAppearance;
|
|
614
|
+
}
|
|
615
|
+
if (desc.wardrobe) {
|
|
616
|
+
char.wardrobe = desc.wardrobe;
|
|
617
|
+
}
|
|
618
|
+
if (desc.emotion) {
|
|
619
|
+
char.emotion = desc.emotion;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
451
624
|
/**
|
|
452
625
|
* Build sound effects from scene elements
|
|
626
|
+
* Filters out character entrances/exits which should be in Actions, not SFX
|
|
453
627
|
*/
|
|
454
628
|
buildSFX(elements) {
|
|
455
|
-
const
|
|
629
|
+
const sfxList = [];
|
|
630
|
+
// Check dialogue for intercom references
|
|
631
|
+
const dialogueElements = elements.filter(el => el.type === 'dialogue');
|
|
632
|
+
for (const dialogueEl of dialogueElements) {
|
|
633
|
+
const parenthetical = dialogueEl.dialogue.parenthetical?.toLowerCase() || '';
|
|
634
|
+
if (parenthetical.includes('intercom') || parenthetical.includes('over intercom')) {
|
|
635
|
+
sfxList.push('Intercom buzz/chirp');
|
|
636
|
+
}
|
|
637
|
+
}
|
|
456
638
|
// Extract sound effects from action lines
|
|
457
|
-
const
|
|
639
|
+
const actionElements = elements.filter(el => el.type === 'action');
|
|
640
|
+
const actionSfx = actionElements
|
|
458
641
|
.map(el => el.text)
|
|
459
|
-
.filter(text => this.isSoundEffect(text));
|
|
460
|
-
|
|
642
|
+
.filter(text => this.isSoundEffect(text) && !this.isCharacterEntrance(text));
|
|
643
|
+
sfxList.push(...actionSfx);
|
|
644
|
+
if (sfxList.length === 0) {
|
|
461
645
|
return 'No sound effects specified';
|
|
462
646
|
}
|
|
463
|
-
return
|
|
647
|
+
return sfxList.join('. ');
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Check if text describes a character entrance or exit
|
|
651
|
+
* These should be in Actions, not SFX
|
|
652
|
+
*/
|
|
653
|
+
isCharacterEntrance(text) {
|
|
654
|
+
const entranceKeywords = [
|
|
655
|
+
'enters', 'enter', 'entering',
|
|
656
|
+
'exits', 'exit', 'exiting',
|
|
657
|
+
'arrives', 'arrive', 'arriving',
|
|
658
|
+
'leaves', 'leave', 'leaving',
|
|
659
|
+
'walks in', 'walks out',
|
|
660
|
+
'comes in', 'goes out'
|
|
661
|
+
];
|
|
662
|
+
const lowerText = text.toLowerCase();
|
|
663
|
+
// Check if text contains character introduction pattern: "NAME (description) enters"
|
|
664
|
+
const hasCharacterIntro = /[A-Z][A-Z\s]+\s*\([^)]+\)/.test(text);
|
|
665
|
+
// Check for entrance keywords
|
|
666
|
+
const hasEntranceKeyword = entranceKeywords.some(keyword => lowerText.includes(keyword));
|
|
667
|
+
return hasCharacterIntro || hasEntranceKeyword;
|
|
464
668
|
}
|
|
465
669
|
/**
|
|
466
670
|
* Check if text describes a sound effect
|
|
@@ -498,19 +702,35 @@ class ShotListGenerator {
|
|
|
498
702
|
* Build verbose context description for generative AI
|
|
499
703
|
* Target: Extremely detailed set/environment description (1000+ characters)
|
|
500
704
|
* PRIORITY: Extract concrete details from screenplay, avoid generic filler
|
|
705
|
+
* STYLE-AWARE: Apply style-specific visual characteristics
|
|
706
|
+
* QUALITY: Environment only - no character actions
|
|
501
707
|
*/
|
|
502
708
|
buildVerboseContext(context) {
|
|
503
709
|
const parts = [];
|
|
504
710
|
// Start with the actual set description from the screenplay
|
|
505
711
|
// This should contain the concrete visual details
|
|
506
712
|
if (context.description && context.description.length > 50) {
|
|
507
|
-
//
|
|
508
|
-
|
|
713
|
+
// Clean the description to remove character actions
|
|
714
|
+
const environmentOnly = this.extractEnvironmentDescription(context.description);
|
|
715
|
+
if (environmentOnly) {
|
|
716
|
+
parts.push(environmentOnly);
|
|
717
|
+
}
|
|
718
|
+
else {
|
|
719
|
+
// Fallback if cleaning removed everything
|
|
720
|
+
parts.push(`${context.set} fills the frame`);
|
|
721
|
+
}
|
|
509
722
|
}
|
|
510
723
|
else {
|
|
511
724
|
// Fallback: Build from available context
|
|
512
725
|
parts.push(`${context.set} fills the frame`);
|
|
513
726
|
}
|
|
727
|
+
// Apply style-specific visual characteristics
|
|
728
|
+
if (this.styleGuidelines) {
|
|
729
|
+
const styleEnhancements = this.getStyleVisualCharacteristics(context);
|
|
730
|
+
if (styleEnhancements) {
|
|
731
|
+
parts.push(styleEnhancements);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
514
734
|
// Add lighting details if specific
|
|
515
735
|
if (context.lighting && !context.lighting.includes('natural') && !context.lighting.includes('standard')) {
|
|
516
736
|
parts.push(`Lighting: ${context.lighting}`);
|
|
@@ -519,7 +739,7 @@ class ShotListGenerator {
|
|
|
519
739
|
if (context.atmosphere && context.atmosphere !== 'neutral') {
|
|
520
740
|
parts.push(`Atmosphere: ${context.atmosphere}`);
|
|
521
741
|
}
|
|
522
|
-
// Add weather if present
|
|
742
|
+
// Add weather if present (already filtered by ContextBuilder for space settings)
|
|
523
743
|
if (context.weather) {
|
|
524
744
|
parts.push(`Weather: ${context.weather}`);
|
|
525
745
|
}
|
|
@@ -533,6 +753,116 @@ class ShotListGenerator {
|
|
|
533
753
|
}
|
|
534
754
|
return parts.join('. ');
|
|
535
755
|
}
|
|
756
|
+
/**
|
|
757
|
+
* Extract environment description only, removing character actions
|
|
758
|
+
* Keeps: Set details, props, lighting, atmosphere
|
|
759
|
+
* Removes: Character introductions, character actions
|
|
760
|
+
*/
|
|
761
|
+
extractEnvironmentDescription(description) {
|
|
762
|
+
// First, remove character introductions with parenthetical descriptions
|
|
763
|
+
// Example: "WIZARD CLIF HIGH (mid-70s, magnificent long white beard...) stands at..."
|
|
764
|
+
// This removes the entire character introduction clause
|
|
765
|
+
let cleaned = description.replace(/[A-Z][A-Z\s]+\([^)]+\)\s+[^.]*\./g, '');
|
|
766
|
+
// Split into sentences to process individually
|
|
767
|
+
const sentences = cleaned.split(/\.\s+/);
|
|
768
|
+
const cleanedSentences = [];
|
|
769
|
+
for (let sentence of sentences) {
|
|
770
|
+
// Skip if this is primarily a character action sentence
|
|
771
|
+
// Check if sentence starts with or contains character action patterns
|
|
772
|
+
if (this.isCharacterActionSentence(sentence)) {
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
// Trim and check if anything meaningful remains
|
|
776
|
+
sentence = sentence.trim();
|
|
777
|
+
// Keep sentences that have substantial content
|
|
778
|
+
if (sentence.length > 15) {
|
|
779
|
+
cleanedSentences.push(sentence);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
return cleanedSentences.join('. ').trim();
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Check if a sentence is primarily about character actions
|
|
786
|
+
* Returns true only if the sentence describes what a character is DOING
|
|
787
|
+
* Returns false if it's describing the environment, even if it contains action-like verbs
|
|
788
|
+
*/
|
|
789
|
+
isCharacterActionSentence(sentence) {
|
|
790
|
+
const lowerSentence = sentence.toLowerCase();
|
|
791
|
+
// Check if sentence contains character name patterns (ALL CAPS words or known character names)
|
|
792
|
+
const hasCharacterName = /\b[A-Z]{2,}(?:\s+[A-Z]{2,})*\b/.test(sentence);
|
|
793
|
+
// Character action patterns: "CHARACTER + ACTION VERB"
|
|
794
|
+
// Example: "WIZARD stands at the center", "Clif pauses", "HEIDI enters"
|
|
795
|
+
const characterActionPatterns = [
|
|
796
|
+
/\b(stands?|stood|standing)\s+(at|in|near|by)/i,
|
|
797
|
+
/\b(sits?|sat|sitting)\s+(at|in|on|down)/i,
|
|
798
|
+
/\b(walks?|walked|walking)\s+(to|toward|into|through)/i,
|
|
799
|
+
/\b(enters?|entered|entering)\b/i,
|
|
800
|
+
/\b(exits?|exited|exiting)\b/i,
|
|
801
|
+
/\b(waves?|waved|waving)\s+(his|her|their|a)/i,
|
|
802
|
+
/\b(pauses?|paused|pausing)\b/i,
|
|
803
|
+
/\b(sighs?|sighed|sighing)\b/i,
|
|
804
|
+
/\b(looks?|looked|looking)\s+(at|toward|up|down)/i,
|
|
805
|
+
/\b(turns?|turned|turning)\s+(to|toward|around)/i,
|
|
806
|
+
/\b(moves?|moved|moving)\s+(to|toward|closer)/i,
|
|
807
|
+
/\b(gestures?|gestured|gesturing)\b/i,
|
|
808
|
+
/\b(points?|pointed|pointing)\s+(at|to|toward)/i,
|
|
809
|
+
/\b(reaches?|reached|reaching)\s+(for|toward|out)/i,
|
|
810
|
+
/\b(grabs?|grabbed|grabbing)\b/i
|
|
811
|
+
];
|
|
812
|
+
// Check if sentence matches character action patterns
|
|
813
|
+
const hasCharacterAction = characterActionPatterns.some(pattern => pattern.test(lowerSentence));
|
|
814
|
+
// Return true only if it has both a character reference AND an action pattern
|
|
815
|
+
return hasCharacterName && hasCharacterAction;
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Check if a sentence is a character introduction
|
|
819
|
+
* Example: "WIZARD CLIF HIGH (mid-70s, magnificent long white beard...)"
|
|
820
|
+
*/
|
|
821
|
+
isCharacterIntroduction(sentence) {
|
|
822
|
+
// Character introductions typically have:
|
|
823
|
+
// 1. All-caps name
|
|
824
|
+
// 2. Parenthetical description with age, appearance, etc.
|
|
825
|
+
const hasAllCapsName = /[A-Z]{2,}\s+[A-Z]{2,}/.test(sentence);
|
|
826
|
+
const hasParentheticalDescription = /\([^)]*\)/.test(sentence);
|
|
827
|
+
return hasAllCapsName && hasParentheticalDescription;
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Get style-specific visual characteristics based on active style guidelines
|
|
831
|
+
* Returns visual descriptors that match the franchise/format aesthetic
|
|
832
|
+
*/
|
|
833
|
+
getStyleVisualCharacteristics(context) {
|
|
834
|
+
if (!this.styleGuidelines || !this.styleGuidelines.appliedStyles) {
|
|
835
|
+
return null;
|
|
836
|
+
}
|
|
837
|
+
const characteristics = [];
|
|
838
|
+
const appliedStylesLower = this.styleGuidelines.appliedStyles.map(s => s.toLowerCase());
|
|
839
|
+
// Check for Star Trek franchise style
|
|
840
|
+
if (appliedStylesLower.some(s => s.includes('star trek'))) {
|
|
841
|
+
// Apply Star Trek visual aesthetic
|
|
842
|
+
characteristics.push('Clean futurism with bright, optimistic lighting');
|
|
843
|
+
characteristics.push('Sleek, utopian design with smooth surfaces and advanced technology');
|
|
844
|
+
// Check if this is a bridge/command center
|
|
845
|
+
const location = context.set.toLowerCase();
|
|
846
|
+
if (location.includes('bridge') || location.includes('command')) {
|
|
847
|
+
characteristics.push('Command stations with illuminated displays and ergonomic controls');
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
// Check for SNL comedy format style
|
|
851
|
+
if (appliedStylesLower.some(s => s.includes('saturday night live') || s.includes('snl'))) {
|
|
852
|
+
// Apply SNL visual style
|
|
853
|
+
characteristics.push('Exaggerated, absurd visual comedy elements');
|
|
854
|
+
characteristics.push('Fast-paced, chaotic energy with physical comedy potential');
|
|
855
|
+
// Emphasize the comedic tone
|
|
856
|
+
if (context.atmosphere && context.atmosphere !== 'neutral') {
|
|
857
|
+
characteristics.push(`Absurdly ${context.atmosphere} atmosphere for comedic effect`);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
// Check for other franchises
|
|
861
|
+
if (appliedStylesLower.some(s => s.includes('star wars'))) {
|
|
862
|
+
characteristics.push('Lived-in, used-future aesthetic with weathered technology');
|
|
863
|
+
}
|
|
864
|
+
return characteristics.length > 0 ? characteristics.join('. ') : null;
|
|
865
|
+
}
|
|
536
866
|
/**
|
|
537
867
|
* Build verbose character description for generative AI
|
|
538
868
|
* Target: 500-800 characters per character with complete visual detail
|