// analyzers.js — Tres extracciones deterministas sobre el JSON del guión. export function buildSceneMap(json) { const scenes = json.scenes || []; return scenes.map(scene => ({ number: scene.scene_number, heading: scene.heading, location: scene.location, time: scene.time_of_day, start_page: scene.start_page ?? null, characters_present: charactersPresent(scene), action_summary: actionSummary(scene) })); } export function buildCharacterReport(json) { const scenes = json.scenes || []; const report = new Map(); for (const scene of scenes) { const speakers = new Set(); for (const el of scene.elements) { if (el.type !== 'character' || !el.name) continue; const name = stripParenthetical(el.name); if (!name) continue; speakers.add(name); if (!report.has(name)) { report.set(name, { name, scene_appearances: [], dialogue_count: 0, total_words_spoken: 0, first_appearance_scene: scene.scene_number, last_appearance_scene: scene.scene_number, scenes_with_other_characters: {} }); } const entry = report.get(name); if (entry.scene_appearances[entry.scene_appearances.length - 1] !== scene.scene_number) { entry.scene_appearances.push(scene.scene_number); } if (el.dialogue) { entry.dialogue_count += 1; entry.total_words_spoken += countWords(el.dialogue); } entry.last_appearance_scene = scene.scene_number; } // Co-apariciones: pares dentro del set de hablantes de la escena. const speakersArr = Array.from(speakers); for (const a of speakersArr) { const entry = report.get(a); for (const b of speakersArr) { if (a === b) continue; entry.scenes_with_other_characters[b] = (entry.scenes_with_other_characters[b] || 0) + 1; } } } return Array.from(report.values()).sort((a, b) => b.dialogue_count - a.dialogue_count); } export function buildScaleta(json) { const scenes = json.scenes || []; return scenes.map(scene => ({ scene_number: scene.scene_number, heading: scene.heading, beat_summary: beatSummary(scene) })); } // --- helpers --------------------------------------------------------------- function charactersPresent(scene) { const set = new Set(); for (const el of scene.elements) { if (el.type === 'character' && el.name) { set.add(stripParenthetical(el.name)); } } return Array.from(set); } function actionSummary(scene) { const firstAction = scene.elements.find(el => el.type === 'action' && el.text); if (!firstAction) return ''; return firstSentences(firstAction.text, 2); } function beatSummary(scene) { const lines = []; const firstAction = scene.elements.find(el => el.type === 'action' && el.text); if (firstAction) { lines.push(firstSentences(firstAction.text, 2)); } const firstDialogue = scene.elements.find(el => el.type === 'character' && el.dialogue); if (firstDialogue) { const speaker = firstDialogue.name ? `${stripParenthetical(firstDialogue.name)}: ` : ''; const line = firstSentences(firstDialogue.dialogue, 1); lines.push(`${speaker}${line}`); } return lines.slice(0, 3).join(' ').trim(); } function firstSentences(text, n) { if (!text) return ''; const cleaned = text.replace(/\s+/g, ' ').trim(); const parts = cleaned.split(/(?<=[.!?…])\s+/); return parts.slice(0, n).join(' '); } function stripParenthetical(name) { return (name || '').replace(/\s*\([^)]*\)\s*$/g, '').trim(); } function countWords(s) { if (!s) return 0; return s.trim().split(/\s+/).filter(Boolean).length; }