Add MicroslopSam TTS, Flextalk voice profiles, effects, and engine controls
This commit is contained in:
@@ -10,7 +10,7 @@ import { AudioManager } from "./audio.js";
|
||||
import type { CaptureSegment } from "./capture.js";
|
||||
import { CapturePipeline } from "./capture.js";
|
||||
import type { Stt } from "./stt.js";
|
||||
import { Tts, type TtsOptions, type TtsVoice } from "./tts.js";
|
||||
import { Tts, type TtsOptions, type TtsProvider, type TtsVoice } from "./tts.js";
|
||||
import type { TranscriptSegment, TranscriptSession, TranscriptStore } from "./transcript.js";
|
||||
import { DEFAULT_VAD, type VadConfig } from "./vad.js";
|
||||
|
||||
@@ -50,10 +50,12 @@ export class AudioService {
|
||||
private readonly speakerIntervals: SpeakerInterval[] = [];
|
||||
private readonly speakerNames = new Map<string, string>();
|
||||
private playing = false;
|
||||
private duckEnabled = true;
|
||||
private nextId = 1;
|
||||
private ducked = false;
|
||||
private selfId?: string;
|
||||
private capture?: CapturePipeline;
|
||||
private captureAdoptTimer?: ReturnType<typeof setInterval>;
|
||||
private session?: TranscriptSession;
|
||||
private segmentCount = 0;
|
||||
private initialized = false;
|
||||
@@ -94,6 +96,7 @@ export class AudioService {
|
||||
stt: this.stt.status(),
|
||||
queue: this.queue.length,
|
||||
speaking: this.playing,
|
||||
duckEnabled: this.duckEnabled,
|
||||
capturing: !!this.capture,
|
||||
sessionId: this.session?.id ?? null,
|
||||
segments: this.segmentCount,
|
||||
@@ -101,6 +104,16 @@ export class AudioService {
|
||||
};
|
||||
}
|
||||
|
||||
/** Enable or disable ducking the TTS while others speak. */
|
||||
setDuckEnabled(on: boolean): void {
|
||||
this.duckEnabled = on;
|
||||
void this.applyDuck(this.shouldDuck());
|
||||
}
|
||||
|
||||
private shouldDuck(): boolean {
|
||||
return this.duckEnabled && this.playing && this.speakers.size > 0;
|
||||
}
|
||||
|
||||
async speak(text: string, options: TtsOptions = {}): Promise<SpeakResult> {
|
||||
await this.init();
|
||||
|
||||
@@ -109,12 +122,18 @@ export class AudioService {
|
||||
|
||||
const tts = this.tts.status();
|
||||
const provider = options.provider ?? tts.defaultProvider;
|
||||
if (provider === "local" && !tts.local) {
|
||||
throw new Error("local TTS is not configured (VESKSLOP_TTS_CMD)");
|
||||
}
|
||||
if (provider === "api" && !tts.api) {
|
||||
throw new Error("API TTS is not configured (VESKSLOP_TTS_API_URL)");
|
||||
}
|
||||
if (provider === "command" && !tts.command) {
|
||||
throw new Error("the custom TTS engine is not configured (VESKSLOP_TTS_CMD)");
|
||||
}
|
||||
if (provider === "espeak" && !tts.espeak) {
|
||||
throw new Error("espeak-ng is not installed");
|
||||
}
|
||||
if (provider === "local" && !tts.local) {
|
||||
throw new Error("local TTS is not configured (VESKSLOP_TTS_CMD)");
|
||||
}
|
||||
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed) throw new Error("text is required");
|
||||
@@ -156,7 +175,7 @@ export class AudioService {
|
||||
}
|
||||
|
||||
this.pruneIntervals();
|
||||
await this.applyDuck(this.playing && this.speakers.size > 0);
|
||||
await this.applyDuck(this.shouldDuck());
|
||||
}
|
||||
|
||||
async startCapture(): Promise<{ sessionId: string }> {
|
||||
@@ -185,6 +204,8 @@ export class AudioService {
|
||||
});
|
||||
|
||||
this.capture.start();
|
||||
void this.manager.adoptClientStreams();
|
||||
this.captureAdoptTimer = setInterval(() => void this.manager.adoptClientStreams(), 5000);
|
||||
this.session = session;
|
||||
this.options.onTranscript?.({ type: "TRANSCRIPT_SESSION", session });
|
||||
|
||||
@@ -192,6 +213,8 @@ export class AudioService {
|
||||
}
|
||||
|
||||
stopCapture(): void {
|
||||
clearInterval(this.captureAdoptTimer);
|
||||
this.captureAdoptTimer = undefined;
|
||||
this.capture?.stop();
|
||||
this.capture = undefined;
|
||||
|
||||
@@ -209,8 +232,12 @@ export class AudioService {
|
||||
return this.manager.startMonitor(onData, onError);
|
||||
}
|
||||
|
||||
ttsVoices(): Promise<TtsVoice[]> {
|
||||
return this.tts.voices();
|
||||
ttsVoices(engine?: TtsProvider): Promise<TtsVoice[]> {
|
||||
return this.tts.voices(engine);
|
||||
}
|
||||
|
||||
warmTts(engine?: TtsProvider) {
|
||||
return this.tts.warm(engine);
|
||||
}
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
@@ -259,7 +286,10 @@ export class AudioService {
|
||||
|
||||
try {
|
||||
const text = await this.stt.transcribe(record.wav);
|
||||
if (!text) return;
|
||||
if (!text) {
|
||||
console.warn(`[capture] transcription empty for ${record.wav}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.transcripts.setSegmentText(record.id, text);
|
||||
this.options.onTranscript?.({ type: "TRANSCRIPT_TEXT", id: record.id, text });
|
||||
@@ -321,7 +351,7 @@ export class AudioService {
|
||||
this.playing = true;
|
||||
try {
|
||||
const file = await this.tts.synthesize(item.text, item.options);
|
||||
await this.applyDuck(this.speakers.size > 0);
|
||||
await this.applyDuck(this.shouldDuck());
|
||||
await this.manager.play(file);
|
||||
} catch (error) {
|
||||
console.error("[audio] speak failed:", (error as Error).message);
|
||||
|
||||
Reference in New Issue
Block a user