@sylweriusz/mcp-ai-voice
Version:
Enable AI agents to express themselves with natural voice synthesis. Zero-configuration multilingual speech that enhances AI interactions through asynchronous voice capabilities.
378 lines • 15.3 kB
JavaScript
;
/**
* Voice Intelligence Module - Language Discovery and Voice Optimization
* Implements language-centric architecture for automatic voice selection
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.VoiceIntelligence = void 0;
const child_process_1 = require("child_process");
const util_1 = require("util");
const os = __importStar(require("os"));
const execAsync = (0, util_1.promisify)(child_process_1.exec);
class VoiceIntelligence {
constructor() {
this.availableVoices = [];
this.languageMapping = {};
this.supportedLanguages = [];
this.platform = os.platform();
}
/**
* Initialize voice intelligence by discovering available voices and languages
*/
async initialize() {
console.error('🔍 Initializing Voice Intelligence Engine...');
try {
switch (this.platform) {
case 'darwin':
await this.discoverMacOSVoices();
break;
case 'win32':
await this.discoverWindowsVoices();
break;
case 'linux':
await this.discoverLinuxVoices();
break;
default:
console.error(`⚠️ Unsupported platform: ${this.platform}`);
}
this.buildLanguageMapping();
console.error(`✅ Discovered ${this.supportedLanguages.length} languages with ${this.availableVoices.length} voices`);
}
catch (error) {
console.error('❌ Voice discovery failed:', error);
// Fallback to default voice without languages
}
}
/**
* Discover voices on macOS using 'say -v ?'
*/
async discoverMacOSVoices() {
try {
const { stdout } = await execAsync('say -v ?');
const lines = stdout.split('\n').filter(line => line.trim());
for (const line of lines) {
// Parse format: "Name (Quality) Language_Code Comment"
// Example: "Krzysztof (Enhanced) pl_PL # Witaj, nazywam się Krzysztof."
const match = line.match(/^(.+?)\s+([a-z]{2}_[A-Z]{2})\s+#\s*(.*)$/);
if (match) {
const [, voiceNamePart, langCode, comment] = match;
const languageCode = langCode.split('_')[0]; // Extract language part
// Extract quality from voice name (e.g., "Krzysztof (Enhanced)" -> "Krzysztof", "Enhanced")
const voiceMatch = voiceNamePart.match(/^(.+?)(?:\s*\(([^)]+)\))?$/);
const voiceId = voiceMatch ? voiceMatch[1].trim() : voiceNamePart.trim();
const qualityIndicator = voiceMatch ? voiceMatch[2] : null;
const voiceInfo = {
id: voiceNamePart.trim(), // Keep full name for 'say' command
name: voiceNamePart.trim(),
language: this.getLanguageName(languageCode),
languageCode: languageCode,
quality: this.detectVoiceQuality(qualityIndicator || '', comment),
gender: this.detectGender(voiceId)
};
this.availableVoices.push(voiceInfo);
}
}
}
catch (error) {
console.error('macOS voice discovery error:', error);
}
}
/**
* Discover voices on Windows using PowerShell
*/
async discoverWindowsVoices() {
try {
const script = `
Add-Type -AssemblyName System.Speech
$synth = New-Object System.Speech.Synthesis.SpeechSynthesizer
$synth.GetInstalledVoices() | ForEach-Object {
$voice = $_.VoiceInfo
Write-Output "$($voice.Name)|$($voice.Culture.TwoLetterISOLanguageName)|$($voice.Gender)"
}
`;
const { stdout } = await execAsync(`powershell -Command "${script}"`);
const lines = stdout.split('\n').filter(line => line.trim());
for (const line of lines) {
const [name, langCode, gender] = line.split('|');
if (name && langCode) {
const voiceInfo = {
id: name,
name: name,
language: this.getLanguageName(langCode),
languageCode: langCode,
quality: 'standard',
gender: gender?.toLowerCase()
};
this.availableVoices.push(voiceInfo);
}
}
}
catch (error) {
console.error('Windows voice discovery error:', error);
}
}
/**
* Discover voices on Linux using espeak
*/
async discoverLinuxVoices() {
try {
const { stdout } = await execAsync('espeak --voices');
const lines = stdout.split('\n').slice(1).filter(line => line.trim()); // Skip header
for (const line of lines) {
const parts = line.trim().split(/\s+/);
if (parts.length >= 2) {
const langCode = parts[1].split('-')[0]; // Extract base language
const voiceInfo = {
id: parts[1],
name: parts[1],
language: this.getLanguageName(langCode),
languageCode: langCode,
quality: 'standard',
gender: parts[2]?.includes('F') ? 'female' : 'male'
};
this.availableVoices.push(voiceInfo);
}
}
}
catch (error) {
console.error('Linux voice discovery error:', error);
}
}
/**
* Build language to voice mapping with quality-first selection
* GDD v2.3: Premium > Enhanced > Standard, with gender preference only within same quality tier
*/
buildLanguageMapping() {
const languageGroups = {};
// Group voices by language
for (const voice of this.availableVoices) {
if (!languageGroups[voice.languageCode]) {
languageGroups[voice.languageCode] = [];
}
languageGroups[voice.languageCode].push(voice);
}
// Select best voice for each language using quality-first algorithm
for (const [langCode, voices] of Object.entries(languageGroups)) {
// GDD v2.3: Quality-first sorting with gender preference as tiebreaker
const sortedVoices = voices.sort((a, b) => {
const qualityOrder = { premium: 3, enhanced: 2, standard: 1 };
const aQuality = qualityOrder[a.quality || 'standard'];
const bQuality = qualityOrder[b.quality || 'standard'];
// Primary: Quality wins everything
if (aQuality !== bQuality)
return bQuality - aQuality;
// Secondary: Within same quality, prefer male voices (Claude is masculine)
if (a.gender === 'male' && b.gender !== 'male')
return -1;
if (a.gender !== 'male' && b.gender === 'male')
return 1;
return 0;
});
// Special case for English: Always ensure we have a voice (fallback to first available)
if (langCode === 'en' && sortedVoices.length === 0) {
// This shouldn't happen but provides safety net
console.error('⚠️ No English voices found - system misconfiguration');
}
this.languageMapping[langCode] = {
bestVoice: sortedVoices[0],
alternativeVoices: sortedVoices.slice(1)
};
this.supportedLanguages.push(langCode);
}
}
/**
* Detect voice quality from quality indicator or comment
*/
detectVoiceQuality(qualityIndicator, comment) {
const combined = `${qualityIndicator} ${comment}`.toLowerCase();
if (combined.includes('enhanced') || combined.includes('neural'))
return 'enhanced';
if (combined.includes('premium') || combined.includes('high quality'))
return 'premium';
return 'standard';
}
/**
* Detect gender from voice name
*/
detectGender(voiceId) {
const maleNames = ['daniel', 'alex', 'fred', 'bruce', 'krzysztof', 'marek', 'thomas', 'oliver', 'luca', 'diego'];
const femaleNames = ['samantha', 'victoria', 'karen', 'zofia', 'ewa', 'amelie', 'anna', 'elena', 'alice'];
const lowerName = voiceId.toLowerCase();
if (maleNames.some(name => lowerName.includes(name)))
return 'male';
if (femaleNames.some(name => lowerName.includes(name)))
return 'female';
// Default to male for consistency
return 'male';
}
/**
* Get human-readable language name from code
*/
getLanguageName(code) {
const languages = {
'en': 'English',
'pl': 'Polish',
'de': 'German',
'fr': 'French',
'es': 'Spanish',
'it': 'Italian',
'pt': 'Portuguese',
'ru': 'Russian',
'ja': 'Japanese',
'zh': 'Chinese'
};
return languages[code] || code.toUpperCase();
}
/**
* Get supported languages for tool schema
*/
getSupportedLanguages() {
return this.supportedLanguages;
}
/**
* Get best voice for a language
*/
getBestVoiceForLanguage(languageCode) {
return this.languageMapping[languageCode]?.bestVoice || null;
}
/**
* Get information about the voice that will be used for synthesis
*/
getUsedVoiceInfo(languageCode) {
if (languageCode) {
// Easter egg: Check if it's a voice name first
if (this.isVoiceName(languageCode)) {
const specificVoice = this.findVoiceByName(languageCode);
if (specificVoice) {
return `${specificVoice.name} (Easter Egg)`;
}
// If voice not found, indicate fallback
return `System Default (Voice "${languageCode}" not found)`;
}
else {
// Standard language code handling
const voice = this.getBestVoiceForLanguage(languageCode);
if (voice) {
return voice.name;
}
}
}
// Return platform default voice name
switch (this.platform) {
case 'darwin':
return 'System Default (macOS)';
case 'win32':
return 'System Default (Windows SAPI)';
case 'linux':
return 'System Default (espeak)';
default:
return 'System Default';
}
}
/**
* Check if parameter is a voice name rather than language code
*/
isVoiceName(param) {
return param.length > 2 && this.platform === 'darwin';
}
/**
* Find voice by exact name match (macOS only)
*/
findVoiceByName(voiceName) {
return this.availableVoices.find(voice => voice.id.toLowerCase() === voiceName.toLowerCase() ||
voice.name.toLowerCase() === voiceName.toLowerCase()) || null;
}
/**
* Get optimal voice command for platform
*/
getVoiceCommand(text, languageCode) {
const sanitizedText = this.sanitizeText(text);
switch (this.platform) {
case 'darwin': {
if (languageCode) {
// Easter egg: If it's longer than 2 chars, treat as voice name
if (this.isVoiceName(languageCode)) {
const specificVoice = this.findVoiceByName(languageCode);
if (specificVoice) {
return `say -v "${specificVoice.id}" "${sanitizedText}"`;
}
// If voice not found, fall through to default
}
else {
// Standard language code handling
const voice = this.getBestVoiceForLanguage(languageCode);
if (voice) {
return `say -v "${voice.id}" "${sanitizedText}"`;
}
}
}
return `say "${sanitizedText}"`;
}
case 'win32': {
let script = `Add-Type -AssemblyName System.Speech; $synth = New-Object System.Speech.Synthesis.SpeechSynthesizer;`;
if (languageCode) {
const voice = this.getBestVoiceForLanguage(languageCode);
if (voice) {
script += ` $synth.SelectVoice('${voice.id}');`;
}
}
script += ` $synth.Speak('${sanitizedText}')`;
return `powershell -Command "${script}"`;
}
case 'linux': {
if (languageCode) {
return `espeak -v ${languageCode} "${sanitizedText}"`;
}
return `espeak "${sanitizedText}"`;
}
default:
throw new Error(`Unsupported platform: ${this.platform}`);
}
}
/**
* Sanitize text for command line
*/
sanitizeText(text) {
return text
.replace(/"/g, '\\"')
.replace(/'/g, "\\'")
.replace(/\n/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
}
exports.VoiceIntelligence = VoiceIntelligence;
//# sourceMappingURL=voice-intelligence.js.map