feat: joel voiceover

This commit is contained in:
eric
2026-02-05 14:25:04 +01:00
parent 24b4c12e7d
commit 7dbe952929
6 changed files with 117 additions and 88 deletions

View File

@@ -14,91 +14,93 @@ const DEFAULT_STYLE = 0.25;
const DEFAULT_SPEED = 1.20
function clamp01(value: number): number {
return Math.max(0, Math.min(1, value));
return Math.max(0, Math.min(1, value));
}
export interface VoiceoverOptions {
text: string;
voiceId?: string;
modelId?: string;
stability?: number;
similarityBoost?: number;
style?: number;
speakerBoost?: boolean;
text: string;
voiceId?: string;
modelId?: string;
stability?: number;
similarityBoost?: number;
style?: number;
speakerBoost?: boolean;
speed?: number;
}
export class VoiceoverService {
async generate(options: VoiceoverOptions): Promise<Buffer> {
const apiKey = config.elevenlabs.apiKey;
if (!apiKey) {
throw new Error("Voiceover is not configured (missing ELEVENLABS_API_KEY).");
async generate(options: VoiceoverOptions): Promise<Buffer> {
const apiKey = config.elevenlabs.apiKey;
if (!apiKey) {
throw new Error("Voiceover is not configured (missing ELEVENLABS_API_KEY).");
}
const voiceId = options.voiceId || config.elevenlabs.voiceId;
if (!voiceId) {
throw new Error("Voiceover is missing a voice ID (set ELEVENLABS_VOICE_ID or pass one).");
}
const text = options.text.trim();
if (!text) {
throw new Error("Voiceover text is empty.");
}
const modelId = options.modelId || config.elevenlabs.modelId;
const voiceSettings = {
stability: clamp01(options.stability ?? DEFAULT_STABILITY),
similarity_boost: clamp01(options.similarityBoost ?? DEFAULT_SIMILARITY),
style: clamp01(options.style ?? DEFAULT_STYLE),
speed: options.speed ?? DEFAULT_SPEED,
use_speaker_boost: options.speakerBoost ?? true,
};
const url = new URL(`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}/stream`);
url.searchParams.set("output_format", DEFAULT_OUTPUT_FORMAT);
logger.debug("Generating voiceover", {
textLength: text.length,
voiceId,
modelId,
});
const response = await fetch(url.toString(), {
method: "POST",
headers: {
"xi-api-key": apiKey,
"Content-Type": "application/json",
"Accept": "audio/mpeg",
},
body: JSON.stringify({
text,
model_id: modelId,
voice_settings: voiceSettings,
}),
});
if (!response.ok) {
const errorBody = await response.text();
logger.error("ElevenLabs API error", {
status: response.status,
body: errorBody.slice(0, 300),
});
throw new Error(`ElevenLabs API error (HTTP ${response.status}).`);
}
const audioBuffer = await response.arrayBuffer();
return Buffer.from(audioBuffer);
}
const voiceId = options.voiceId || config.elevenlabs.voiceId;
if (!voiceId) {
throw new Error("Voiceover is missing a voice ID (set ELEVENLABS_VOICE_ID or pass one).");
async health(): Promise<boolean> {
return !!config.elevenlabs.apiKey;
}
const text = options.text.trim();
if (!text) {
throw new Error("Voiceover text is empty.");
}
const modelId = options.modelId || config.elevenlabs.modelId;
const voiceSettings = {
stability: clamp01(options.stability ?? DEFAULT_STABILITY),
similarity_boost: clamp01(options.similarityBoost ?? DEFAULT_SIMILARITY),
style: clamp01(options.style ?? DEFAULT_STYLE),
use_speaker_boost: options.speakerBoost ?? true,
};
const url = new URL(`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}/stream`);
url.searchParams.set("output_format", DEFAULT_OUTPUT_FORMAT);
logger.debug("Generating voiceover", {
textLength: text.length,
voiceId,
modelId,
});
const response = await fetch(url.toString(), {
method: "POST",
headers: {
"xi-api-key": apiKey,
"Content-Type": "application/json",
"Accept": "audio/mpeg",
},
body: JSON.stringify({
text,
model_id: modelId,
voice_settings: voiceSettings,
}),
});
if (!response.ok) {
const errorBody = await response.text();
logger.error("ElevenLabs API error", {
status: response.status,
body: errorBody.slice(0, 300),
});
throw new Error(`ElevenLabs API error (HTTP ${response.status}).`);
}
const audioBuffer = await response.arrayBuffer();
return Buffer.from(audioBuffer);
}
async health(): Promise<boolean> {
return !!config.elevenlabs.apiKey;
}
}
let voiceoverService: VoiceoverService | null = null;
export function getVoiceoverService(): VoiceoverService {
if (!voiceoverService) {
voiceoverService = new VoiceoverService();
}
return voiceoverService;
if (!voiceoverService) {
voiceoverService = new VoiceoverService();
}
return voiceoverService;
}