Project Files
dist / narrator.js
"use strict";
/**
* @file narrator.ts
* @description Full narration pipeline for vibevoice-tts.
*
* Combines:
* - Style presets (storytelling, children, christmas-tale, etc.)
* - Mood modifiers (warm, mysterious, energetic, etc.)
* - Language packs (Polish abbreviations, foreign words, acronyms)
* - Narrative structure detection (dialogue, scenes, chapters)
*
* Pipeline stages:
* 1. Load style preset + apply mood modifier
* 2. Load language pack
* 3. Expand language-specific abbreviations
* 4. Insert phonetic hints for foreign words
* 5. Detect narrative structure
* 6. Insert narrative-aware pauses
* 7. Insert auto direction tags
* 8. Chunk the script
* 9. Generate TTS recommendations
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.narrateScript = narrateScript;
const language_packs_1 = require("./language-packs");
const language_packs_2 = require("./language-packs");
const narrative_1 = require("./narrative");
const preparer_1 = require("./preparer");
// ---------------------------------------------------------------------------
// Phonetic Hint Markers
// ---------------------------------------------------------------------------
/**
* Insert phonetic hint markers for foreign words in the text.
* Uses [lang:pronunciation] format that the LLM can interpret.
*/
function insertPhoneticHints(text, language) {
const pack = (0, language_packs_2.getLanguagePack)(language);
const hints = [];
if (pack.foreignWords.length === 0 && pack.acronyms.length === 0) {
return { text, hints };
}
// Combine foreign words and acronyms
const allWords = [...new Set([...pack.foreignWords, ...pack.acronyms])];
// Sort by length (longest first) to avoid partial matches
allWords.sort((a, b) => b.length - a.length);
let result = text;
for (const word of allWords) {
const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const regex = new RegExp(`\\b${escaped}\\b`, "g");
let match;
const matches = [];
while ((match = regex.exec(result)) !== null) {
matches.push({ index: match.index, text: match[0] });
}
// Process matches in reverse to preserve indices
for (let i = matches.length - 1; i >= 0; i--) {
const m = matches[i];
const isAcronym = pack.acronyms.includes(m.text);
const pronunciation = isAcronym
? m.text.split("").join(" ") // Spell out letter by letter
: `[en]${m.text}[/en]`; // Mark as English
hints.push({
original: m.text,
pronunciation,
position: m.index,
reason: isAcronym ? "acronym" : "foreign-word",
});
// Insert hint marker
const hintMarker = ` [pronounce:${pronunciation}]`;
result =
result.slice(0, m.index + m.text.length) +
hintMarker +
result.slice(m.index + m.text.length);
}
}
return { text: result, hints };
}
// ---------------------------------------------------------------------------
// Abbreviation Expansion (Language-Specific)
// ---------------------------------------------------------------------------
/**
* Expand language-specific abbreviations in text.
*/
function expandLanguageAbbreviations(text, language) {
const pack = (0, language_packs_2.getLanguagePack)(language);
let result = text;
// Sort by length (longest first) to avoid partial matches
const entries = Object.entries(pack.abbreviations).sort((a, b) => b[0].length - a[0].length);
for (const [abbr, expansion] of entries) {
const escaped = abbr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
// Only expand first occurrence (like the prepare tool)
const regex = new RegExp(escaped);
const firstMatch = regex.exec(result);
if (firstMatch) {
result =
result.slice(0, firstMatch.index) +
expansion +
result.slice(firstMatch.index + abbr.length);
}
}
return result;
}
// ---------------------------------------------------------------------------
// Auto Direction Tag Insertion
// ---------------------------------------------------------------------------
/**
* Insert auto direction tags based on narrative structure.
*/
function insertAutoDirections(text, elements, stylePreset) {
if (!stylePreset.autoDirections || elements.length === 0) {
return text;
}
// Sort by position (reverse for insertion)
const sorted = [...elements].sort((a, b) => b.start - a.start);
let result = text;
for (const element of sorted) {
const directions = (0, narrative_1.getAutoDirections)(element.type);
if (directions.length === 0)
continue;
// Insert direction tag before the element
const tag = `[${directions[0]}]`;
result =
result.slice(0, element.start) + tag + " " + result.slice(element.start);
}
return result;
}
// ---------------------------------------------------------------------------
// Full Narration Pipeline
// ---------------------------------------------------------------------------
/**
* Run the full narration preparation pipeline.
*
* This is the comprehensive version of prepareScript that adds:
* - Style presets (storytelling, children, christmas-tale, etc.)
* - Mood modifiers (warm, mysterious, energetic, etc.)
* - Language-specific abbreviation expansion
* - Phonetic hints for foreign words
* - Narrative structure detection
* - Auto direction tag insertion
*/
function narrateScript(params) {
const { text, style = "storytelling", mood = "neutral", language = "en", chunkSize = 500, expandAbbreviations = true, phoneticHints = true, detectStructure = true, output = "script", } = params;
const warnings = [];
let processed = text;
// Step 1: Get style preset + apply mood
const basePreset = (0, language_packs_1.getStylePreset)(style);
const adjusted = (0, language_packs_1.applyMoodToPreset)(basePreset, mood);
// Step 2: Expand language-specific abbreviations
if (expandAbbreviations) {
processed = expandLanguageAbbreviations(processed, language);
}
// Step 3: Insert phonetic hints for foreign words
const phoneticResult = phoneticHints
? insertPhoneticHints(processed, language)
: { text: processed, hints: [] };
processed = phoneticResult.text;
if (phoneticResult.hints.length > 0) {
warnings.push(`Found ${phoneticResult.hints.length} foreign word(s)/acronym(s) with pronunciation hints.`);
}
// Step 4: Detect narrative structure
const structure = detectStructure ? (0, narrative_1.detectNarrativeStructure)(processed) : [];
if (structure.length > 0) {
const typeCounts = structure.reduce((acc, el) => {
acc[el.type] = (acc[el.type] || 0) + 1;
return acc;
}, {});
warnings.push(`Detected narrative structure: ${Object.entries(typeCounts)
.map(([t, c]) => `${c}x ${t}`)
.join(", ")}.`);
}
// Step 5: Insert narrative-aware pauses
if (detectStructure && structure.length > 0) {
processed = (0, narrative_1.insertNarrativePauses)(processed, structure, {
scenePauseLines: Math.round(adjusted.scenePause * 2),
chapterPauseLines: Math.round(adjusted.scenePause * 4),
climaxPauseLines: Math.round(adjusted.scenePause * 3),
transitionPauseLines: Math.round(adjusted.sentencePause * 2),
});
}
// Step 6: Split long paragraphs based on style
if (basePreset.maxSentences < 4) {
const splitParas = (0, preparer_1.splitParagraphs)(processed, basePreset.maxSentences);
processed = splitParas.join("\n\n");
}
// Step 7: Insert auto direction tags
if (basePreset.autoDirections && structure.length > 0) {
processed = insertAutoDirections(processed, structure, basePreset);
}
// Step 8: Chunk the script
const chunks = (0, preparer_1.chunkScript)(processed, chunkSize);
// Step 9: Generate TTS recommendations
const recommendations = {
temperature: adjusted.temperature,
speed: adjusted.speed,
wpm: basePreset.wpm,
chunkSize,
};
// Calculate totals
const totalWords = (0, preparer_1.countWords)(processed);
const estimatedMinutes = totalWords / recommendations.wpm;
// Additional warnings
if (totalWords === 0) {
warnings.push("Prepared script is empty after processing.");
}
if (chunks.length > 1) {
warnings.push(`Script split into ${chunks.length} chunks. Generate each separately and concatenate with 200ms crossfade.`);
}
if (totalWords > 4000) {
warnings.push(`Very long script (${totalWords} words, ~${estimatedMinutes.toFixed(1)} min). ` +
"Consider splitting into multiple episodes.");
}
// Build the output
let script = processed;
if (output === "json") {
script = JSON.stringify(chunks, null, 2);
}
return {
original: text,
script,
chunks,
structure,
phoneticHints: phoneticResult.hints,
expressions: [], // TODO: auto-insert expression tags
speakerChanges: [], // TODO: dynamic speaker emotion timeline
recommendations,
appliedStyle: {
...basePreset,
speed: adjusted.speed,
temperature: adjusted.temperature,
sentencePause: adjusted.sentencePause,
paragraphPause: adjusted.paragraphPause,
scenePause: adjusted.scenePause,
},
warnings,
totalWords,
estimatedMinutes,
language,
};
}
//# sourceMappingURL=narrator.js.map