Local streaming STT/TTS with Parakeet + Pocket TTS
- Add provider-agnostic streaming speech interfaces - Local STT via Sherpa (Zipformer/Paraformer) + Parakeet v3 (offline) - Local TTS via Pocket TTS ONNX (onnxruntime-node) with chunked streaming - Auto-download models into PASEO_HOME and add Node download script - Add E2E download test that round-trips TTS->STT
This commit is contained in:
@@ -76,6 +76,51 @@ function parseOpenAIConfig(
|
||||
};
|
||||
}
|
||||
|
||||
function parseSpeechProviderId(value: unknown): "openai" | "sherpa" | null {
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
if (normalized === "openai") return "openai";
|
||||
if (normalized === "sherpa" || normalized === "sherpa-onnx" || normalized === "local") {
|
||||
return "sherpa";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeSherpaSttPreset(value: string): string {
|
||||
const raw = value.trim();
|
||||
const normalized = raw.toLowerCase();
|
||||
if (normalized === "zipformer" || normalized === "zipformer-bilingual") {
|
||||
return "zipformer-bilingual-zh-en-2023-02-20";
|
||||
}
|
||||
if (normalized === "paraformer") {
|
||||
return "paraformer-bilingual-zh-en";
|
||||
}
|
||||
if (normalized === "parakeet" || normalized === "parakeet-v3" || normalized === "parakeet-tdt") {
|
||||
return "parakeet-tdt-0.6b-v3-int8";
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
function normalizeSherpaTtsPreset(value: string): string {
|
||||
const raw = value.trim();
|
||||
const normalized = raw.toLowerCase();
|
||||
if (normalized === "pocket" || normalized === "pocket-tts") {
|
||||
return "pocket-tts-onnx-int8";
|
||||
}
|
||||
if (normalized === "kitten") {
|
||||
return "kitten-nano-en-v0_1-fp16";
|
||||
}
|
||||
if (normalized === "kokoro") {
|
||||
return "kokoro-en-v0_19";
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
export function loadConfig(
|
||||
paseoHome: string,
|
||||
options?: {
|
||||
@@ -137,6 +182,70 @@ export function loadConfig(
|
||||
voiceTtsVoice: persisted.features?.voiceMode?.tts?.voice,
|
||||
});
|
||||
|
||||
const dictationSttProvider =
|
||||
parseSpeechProviderId(env.PASEO_DICTATION_STT_PROVIDER) ??
|
||||
parseSpeechProviderId(persisted.features?.dictation?.stt?.provider) ??
|
||||
"sherpa";
|
||||
|
||||
const voiceSttProvider =
|
||||
parseSpeechProviderId(env.PASEO_VOICE_STT_PROVIDER) ??
|
||||
parseSpeechProviderId(persisted.features?.voiceMode?.stt?.provider) ??
|
||||
"sherpa";
|
||||
|
||||
const voiceTtsProvider =
|
||||
parseSpeechProviderId(env.PASEO_VOICE_TTS_PROVIDER) ??
|
||||
parseSpeechProviderId(persisted.features?.voiceMode?.tts?.provider) ??
|
||||
"sherpa";
|
||||
|
||||
const shouldConfigureSherpa =
|
||||
dictationSttProvider === "sherpa" ||
|
||||
voiceSttProvider === "sherpa" ||
|
||||
voiceTtsProvider === "sherpa" ||
|
||||
typeof env.PASEO_SHERPA_ONNX_MODELS_DIR === "string" ||
|
||||
Boolean(persisted.providers?.sherpaOnnx);
|
||||
|
||||
const sherpaModelsDir =
|
||||
(env.PASEO_SHERPA_ONNX_MODELS_DIR ?? persisted.providers?.sherpaOnnx?.modelsDir)?.trim() ||
|
||||
path.join(paseoHome, "models", "sherpa-onnx");
|
||||
|
||||
const sherpaOnnx = shouldConfigureSherpa
|
||||
? {
|
||||
modelsDir: sherpaModelsDir,
|
||||
autoDownload:
|
||||
env.PASEO_SHERPA_ONNX_AUTO_DOWNLOAD !== undefined
|
||||
? env.PASEO_SHERPA_ONNX_AUTO_DOWNLOAD === "1"
|
||||
: persisted.providers?.sherpaOnnx?.autoDownload ??
|
||||
// In tests we should never hit the network unexpectedly.
|
||||
Boolean(env.VITEST) === false,
|
||||
stt: {
|
||||
preset: normalizeSherpaSttPreset(
|
||||
(env.PASEO_SHERPA_STT_PRESET ?? persisted.providers?.sherpaOnnx?.stt?.preset)?.trim() ||
|
||||
(persisted.features?.voiceMode?.stt?.preset ??
|
||||
persisted.features?.dictation?.stt?.preset)?.trim() ||
|
||||
"zipformer-bilingual-zh-en-2023-02-20"
|
||||
),
|
||||
},
|
||||
tts: {
|
||||
preset: normalizeSherpaTtsPreset(
|
||||
(env.PASEO_SHERPA_TTS_PRESET ??
|
||||
persisted.providers?.sherpaOnnx?.tts?.preset ??
|
||||
persisted.features?.voiceMode?.tts?.preset)?.trim() ||
|
||||
(env.VITEST ? "kitten-nano-en-v0_1-fp16" : "pocket-tts-onnx-int8")
|
||||
),
|
||||
speakerId:
|
||||
env.PASEO_SHERPA_TTS_SPEAKER_ID !== undefined
|
||||
? Number.parseInt(env.PASEO_SHERPA_TTS_SPEAKER_ID, 10)
|
||||
: persisted.providers?.sherpaOnnx?.tts?.speakerId ??
|
||||
persisted.features?.voiceMode?.tts?.speakerId,
|
||||
speed:
|
||||
env.PASEO_SHERPA_TTS_SPEED !== undefined
|
||||
? Number.parseFloat(env.PASEO_SHERPA_TTS_SPEED)
|
||||
: persisted.providers?.sherpaOnnx?.tts?.speed ??
|
||||
persisted.features?.voiceMode?.tts?.speed,
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const openrouterApiKey =
|
||||
env.OPENROUTER_API_KEY ?? persisted.providers?.openrouter?.apiKey ?? null;
|
||||
const voiceLlmModel = persisted.features?.voiceMode?.llm?.model ?? null;
|
||||
@@ -158,6 +267,12 @@ export function loadConfig(
|
||||
relayPublicEndpoint,
|
||||
appBaseUrl,
|
||||
openai,
|
||||
speech: {
|
||||
dictationSttProvider,
|
||||
voiceSttProvider,
|
||||
voiceTtsProvider,
|
||||
...(sherpaOnnx ? { sherpaOnnx } : {}),
|
||||
},
|
||||
openrouterApiKey,
|
||||
voiceLlmModel,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user