Project Files
dist / index.js
"use strict";
/**
* @file index.ts
* @description VibeVoice TTS plugin — LM Studio plugin entry point.
*
* Registers two tools:
* - `format`: Basic text cleaning + multi-speaker script formatting
* - `prepare`: Full podcast script preparation pipeline
*
* VibeVoice does NOT support SSML. This plugin prepares plain text for
* VibeVoice, handling:
* - Text cleaning (smart quotes, emoji, HTML, URLs, annotations)
* - Multi-speaker script formatting ("Speaker N:" prefix format)
* - Voice/emotion annotations per speaker (for voice reference selection)
* - JSON array input for structured speaker data
* - Abbreviation expansion, pause markers, direction tags, chunking
*
* This plugin does NOT generate audio — it prepares formatted text for
* downstream VibeVoice TTS consumption.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.main = main;
exports.toolsProvider = toolsProvider;
const sdk_1 = require("@lmstudio/sdk");
const zod_1 = require("zod");
const processor_1 = require("./processor");
const preparer_1 = require("./preparer");
const narrator_1 = require("./narrator");
// ---------------------------------------------------------------------------
// Zod schema for the format tool parameters
// ---------------------------------------------------------------------------
const FormatParams = zod_1.z.object({
/** The input text to format for VibeVoice TTS. */
text: zod_1.z
.string()
.min(1)
.describe("The text to format for VibeVoice TTS. " +
'Supports plain text, multi-speaker "Speaker N:" prefix format, ' +
"or a JSON array of {speaker, text} objects."),
/** Speaker mode */
mode: zod_1.z
.enum(["single", "multi", "auto"])
.optional()
.default("auto")
.describe("Speaker mode. " +
'"single": one speaker, ignores "Speaker N:" prefixes. ' +
'"multi": parse "Speaker N:" prefixes (warns if none found). ' +
'"auto": detect prefixes automatically; fallback to single speaker.'),
/** Number of speakers */
speakers: zod_1.z
.number()
.int()
.min(1)
.max(4)
.optional()
.default(1)
.describe("Number of speakers (1-4). Used for voice/emotion array indexing. " +
"Default: 1."),
/** Voice names per speaker */
voices: zod_1.z
.array(zod_1.z.string())
.optional()
.describe('Voice names per speaker, e.g. ["Alice", "Frank"]. ' +
"Used as documentation — the user selects the matching voice reference " +
"audio for each VibeVoice speaker slot."),
/** Emotion labels per speaker */
emotions: zod_1.z
.array(zod_1.z.enum([
"neutral",
"happy",
"sad",
"angry",
"excited",
"calm",
"thoughtful",
"whisper",
"serious",
"energetic",
"warm",
"cold",
"mysterious",
"dramatic",
]))
.optional()
.describe("Emotion labels per speaker for voice reference selection. " +
"VibeVoice does NOT support inline emotion tags — emotion comes from " +
"the reference audio selected for each speaker slot. " +
"E.g. Speaker 1 = calm, Speaker 2 = excited."),
/** Emotion-to-speaker-slot mapping (PsiPi method) */
emotion_map: zod_1.z
.record(zod_1.z.enum([
"neutral",
"happy",
"sad",
"angry",
"excited",
"calm",
"thoughtful",
"whisper",
"serious",
"energetic",
"warm",
"cold",
"mysterious",
"dramatic",
]), zod_1.z.number().int().min(1).max(4))
.optional()
.describe("Map emotions to speaker slots (PsiPi method). Lines annotated with " +
"[emotion] or (emotion) are stripped of the tag and reassigned to " +
"the mapped slot. Default: neutral/calm to 1, happy/sad to 2, " +
"angry/excited to 3. Use the same voice with different emotional " +
"reference clips per slot to vary emotion."),
/** Apply text cleaning */
clean: zod_1.z
.boolean()
.optional()
.default(true)
.describe("Apply the VibeVoice text-cleaning pipeline. " +
"Strips smart quotes, Chinese quotation marks, emoji, HTML/XML tags, " +
"URLs (replaced with [URL]), non-speech annotations, " +
"tabs, and excess whitespace. Default: true."),
/** Output format */
output: zod_1.z
.enum(["plain", "script", "json"])
.optional()
.default("script")
.describe("Output format. " +
'"plain": just the cleaned text without speaker prefixes. ' +
'"script": "Speaker N:" prefix format (suitable for VibeVoice). ' +
'"json": structured array of {speaker, text} objects.'),
});
// ---------------------------------------------------------------------------
// Zod schema for the prepare tool parameters
// ---------------------------------------------------------------------------
const PrepareParams = zod_1.z.object({
text: zod_1.z
.string()
.min(1)
.describe("The raw podcast script text to prepare for VibeVoice TTS. " +
"Supports plain text, multi-speaker Speaker N: format, " +
"pause markers like [pause 1s], and direction tags like [emphasis]."),
chunkSize: zod_1.z
.number()
.int()
.min(100)
.max(2000)
.optional()
.default(500)
.describe("Target words per chunk for incremental TTS generation. " +
"500 words is optimal for VibeVoice quality. Default: 500."),
paragraphPauses: zod_1.z
.boolean()
.optional()
.default(true)
.describe("Insert empty-line pauses between paragraphs. " +
"VibeVoice interprets empty lines as natural pauses. Default: true."),
sentencePauses: zod_1.z
.boolean()
.optional()
.default(false)
.describe("Split long paragraphs into shorter ones (max 3 sentences each). " +
"Improves TTS quality for dense text. Default: false."),
maxSentencesPerParagraph: zod_1.z
.number()
.int()
.min(1)
.max(10)
.optional()
.default(3)
.describe("Maximum sentences per paragraph before auto-splitting. " +
"Only used when sentencePauses=true. Default: 3."),
expandAbbreviations: zod_1.z
.boolean()
.optional()
.default(true)
.describe("Expand common abbreviations (e.g. becomes for example, AI becomes A I). " +
"First occurrence only. Improves TTS pronunciation. Default: true."),
preserveDirections: zod_1.z
.boolean()
.optional()
.default(true)
.describe("Extract direction tags ([emphasis], [slower], etc.) as metadata " +
"hints instead of stripping them. Guides TTS param adjustments. Default: true."),
output: zod_1.z
.enum(["plain", "script", "json"])
.optional()
.default("script")
.describe("Output format. " +
'"plain": just the prepared text. ' +
'"script": Speaker N: prefix format. ' +
'"json": structured chunk array. Default: script.'),
});
// ---------------------------------------------------------------------------
// Zod schema for the narrate tool parameters
// ---------------------------------------------------------------------------
const NarrateParams = zod_1.z.object({
text: zod_1.z
.string()
.min(1)
.describe("The raw text to narrate for VibeVoice TTS. " +
"Supports plain text, multi-speaker format, " +
"expression tags like <laughs>, and direction tags."),
style: zod_1.z
.enum([
"storytelling",
"children",
"christmas-tale",
"news",
"technical",
"dramatic",
])
.optional()
.default("storytelling")
.describe("Narration style preset. " +
'"storytelling": balanced pauses, 0.9x speed, 145 WPM. ' +
'"children": longer pauses, 0.85x speed, 130 WPM. ' +
'"christmas-tale": warm pauses, 0.88x speed, 135 WPM. ' +
'"news": short pauses, 1.0x speed, 170 WPM. ' +
'"technical": moderate pauses, 0.95x speed, 155 WPM. ' +
'"dramatic": variable pauses, 0.92x speed, 140 WPM.'),
mood: zod_1.z
.enum([
"warm",
"mysterious",
"energetic",
"somber",
"playful",
"serious",
"neutral",
])
.optional()
.default("neutral")
.describe("Mood modifier within the style. Adjusts pause patterns and direction tag density. Default: neutral."),
language: zod_1.z
.enum(["en", "pl", "de", "fr", "es", "it", "pt", "ja", "ko", "zh"])
.optional()
.default("en")
.describe("Language code for language-specific processing (abbreviations, foreign words). Default: en."),
chunkSize: zod_1.z
.number()
.int()
.min(100)
.max(2000)
.optional()
.default(500)
.describe("Target words per chunk for incremental TTS generation. Default: 500."),
expandAbbreviations: zod_1.z
.boolean()
.optional()
.default(true)
.describe("Expand language-specific abbreviations (e.g., np. → na przykład). Default: true."),
phoneticHints: zod_1.z
.boolean()
.optional()
.default(true)
.describe("Insert phonetic hints for foreign words and acronyms. Default: true."),
detectStructure: zod_1.z
.boolean()
.optional()
.default(true)
.describe("Detect narrative structure (dialogue, scenes, chapters). Default: true."),
autoExpressions: zod_1.z
.boolean()
.optional()
.default(true)
.describe("Auto-insert expression tags based on narrative context. Default: true."),
output: zod_1.z
.enum(["plain", "script", "json"])
.optional()
.default("script")
.describe("Output format. plain: just text. script: Speaker N: format. json: structured chunks. Default: script."),
});
// ---------------------------------------------------------------------------
// Plugin entry point
// ---------------------------------------------------------------------------
async function main(context) {
context.withToolsProvider(toolsProvider);
}
// ---------------------------------------------------------------------------
// Tools provider — registers the `format` and `prepare` tools
// ---------------------------------------------------------------------------
/**
* Register the format and prepare tools with the LM Studio plugin runtime.
*/
async function toolsProvider(_ctl) {
const formatTool = (0, sdk_1.tool)({
name: "format",
description: "Format text for VibeVoice TTS (Text-To-Speech) engine output.\n\n" +
"VibeVoice does NOT support SSML — this tool prepares clean, structured " +
"plain text in the multi-speaker script format that VibeVoice consumes natively.\n\n" +
"CAPABILITIES:\n" +
" - Text cleaning — strips smart quotes, emoji, HTML tags, URLs, " +
"non-speech annotations, tabs, excess whitespace\n" +
' - Multi-speaker detection — parses "Speaker N:" prefix format ' +
"or accepts JSON array input\n" +
" - Voice/emotion annotations — documents which voice reference " +
"audio to use per speaker slot\n" +
' - Output formats — plain text, script with "Speaker N:" prefixes, ' +
"or structured JSON\n\n" +
"EMOTION NOTE:\n" +
"VibeVoice does NOT support inline emotion tags. Emotion is controlled " +
"by the reference audio sample selected for each speaker. " +
"The `emotions` parameter is documentation — it tells the user which " +
"emotion-level voice to assign to each speaker slot.\n\n" +
"SPEAKER FORMAT:\n" +
" Speaker 1: Hello, how are you?\n" +
" Speaker 2: I'm doing great!\n" +
" Speaker 1: That's wonderful to hear.\n\n" +
"This tool does NOT generate audio — it prepares formatted text " +
"input for VibeVoice TTS engines.",
parameters: {
text: FormatParams.shape.text,
mode: FormatParams.shape.mode,
speakers: FormatParams.shape.speakers,
voices: FormatParams.shape.voices,
emotions: FormatParams.shape.emotions,
clean: FormatParams.shape.clean,
output: FormatParams.shape.output,
},
implementation: async (params) => {
const { text, mode, speakers, voices, emotions, clean, output } = params;
return (0, processor_1.processText)(text, {
mode: mode ?? "auto",
speakers: speakers ?? 1,
voices: voices ?? [],
emotions: emotions ?? [],
clean: clean ?? true,
output: output ?? "script",
emotion_map: params.emotion_map,
});
},
});
const prepareTool = (0, sdk_1.tool)({
name: "prepare",
description: "Prepare a podcast script for VibeVoice TTS generation.\n\n" +
"This is the FULL preparation pipeline — beyond basic text cleaning, " +
"it handles everything needed for broadcast-quality TTS output:\n\n" +
"PREPARATION STEPS:\n" +
" - Abbreviation expansion — e.g. becomes for example, AI becomes A I (first use)\n" +
" - Direction tag extraction — [emphasis], [slower] become metadata hints\n" +
" - Pause marker conversion — [pause 1s] becomes empty lines (VibeVoice pauses)\n" +
" - Paragraph splitting — long paragraphs become 2-3 sentence chunks\n" +
" - Script chunking — ~500 words per chunk for optimal quality\n" +
" - TTS recommendations — temperature, speed, WPM metadata\n\n" +
"PAUSE MARKERS:\n" +
" [pause 0.3s] — between sentences\n" +
" [pause 0.5s] — between paragraphs\n" +
" [pause 1s] — between sections\n\n" +
"DIRECTION TAGS (metadata, not sent to TTS):\n" +
" [emphasis] [slower] [faster] [whisper] [louder] [softer]\n\n" +
"TTS RECOMMENDATIONS:\n" +
" Narration: temperature 0.6-0.7, speed 0.95x, 155 WPM\n" +
" Dialogue: temperature 0.8-0.9, speed 0.95x, 160 WPM\n\n" +
"WORKFLOW: prepare, then generate each chunk separately, " +
"concatenate with 200ms crossfade, add intro/outro, publish.",
parameters: {
text: PrepareParams.shape.text,
chunkSize: PrepareParams.shape.chunkSize,
paragraphPauses: PrepareParams.shape.paragraphPauses,
sentencePauses: PrepareParams.shape.sentencePauses,
maxSentencesPerParagraph: PrepareParams.shape.maxSentencesPerParagraph,
expandAbbreviations: PrepareParams.shape.expandAbbreviations,
preserveDirections: PrepareParams.shape.preserveDirections,
output: PrepareParams.shape.output,
},
implementation: async (params) => {
const { text, chunkSize, paragraphPauses, sentencePauses, maxSentencesPerParagraph, expandAbbreviations, preserveDirections, output, } = params;
return (0, preparer_1.prepareScript)(text, {
chunkSize: chunkSize ?? 500,
paragraphPauses: paragraphPauses ?? true,
sentencePauses: sentencePauses ?? false,
maxSentencesPerParagraph: maxSentencesPerParagraph ?? 3,
expandAbbreviations: expandAbbreviations ?? true,
preserveDirections: preserveDirections ?? true,
output: output ?? "script",
});
},
});
const narrateTool = (0, sdk_1.tool)({
name: "narrate",
description: "Prepare text for VibeVoice TTS with style-aware narration.\n\n" +
"This tool adds NARRATION STYLE, MOOD, and LANGUAGE support:\n\n" +
"STYLE PRESETS:\n" +
" - storytelling: balanced pauses, 0.9x speed, 145 WPM\n" +
" - children: longer pauses, 0.85x speed, 130 WPM\n" +
" - christmas-tale: warm pauses, 0.88x speed, 135 WPM\n" +
" - news: short pauses, 1.0x speed, 170 WPM\n" +
" - technical: moderate pauses, 0.95x speed, 155 WPM\n" +
" - dramatic: variable pauses, 0.92x speed, 140 WPM\n\n" +
"MOOD MODIFIERS:\n" +
" warm, mysterious, energetic, somber, playful, serious, neutral\n\n" +
"LANGUAGE PACKS:\n" +
" - Polish (pl): abbreviations (np., itd., m.in., tzn.), titles, addresses\n" +
" - English (en): baseline abbreviations\n" +
" - More languages configurable\n\n" +
"EXPRESSION TAGS:\n" +
" VibeVoice supports expression tags like <laughs>, <sighs>, <giggles>\n" +
" Any <tag> pattern is preserved — model can invent new ones freely\n" +
" Convert [expression] to <expression> automatically\n\n" +
"NARRATIVE DETECTION:\n" +
" - Dialogue detection (quotes, speaker changes)\n" +
" - Scene changes, chapter boundaries\n" +
" - Transition phrases, emotional intensity\n" +
" - Auto-pause insertion based on structure\n\n" +
"WORKFLOW: narrate with style, then generate each chunk separately,\n" +
"concatenate with 200ms crossfade, add intro/outro, publish.",
parameters: {
text: NarrateParams.shape.text,
style: NarrateParams.shape.style,
mood: NarrateParams.shape.mood,
language: NarrateParams.shape.language,
chunkSize: NarrateParams.shape.chunkSize,
expandAbbreviations: NarrateParams.shape.expandAbbreviations,
phoneticHints: NarrateParams.shape.phoneticHints,
detectStructure: NarrateParams.shape.detectStructure,
autoExpressions: NarrateParams.shape.autoExpressions,
output: NarrateParams.shape.output,
},
implementation: async (params) => {
const { text, style, mood, language, chunkSize, expandAbbreviations, phoneticHints, detectStructure, autoExpressions, output, } = params;
return (0, narrator_1.narrateScript)({
text,
style: style ?? "storytelling",
mood: mood ?? "neutral",
language: language ?? "en",
chunkSize: chunkSize ?? 500,
expandAbbreviations: expandAbbreviations ?? true,
phoneticHints: phoneticHints ?? true,
detectStructure: detectStructure ?? true,
autoExpressions: autoExpressions ?? true,
output: output ?? "script",
});
},
});
return [formatTool, prepareTool, narrateTool];
}
//# sourceMappingURL=index.js.map