Update files

This commit is contained in:
Mohamed Boudra
2026-02-06 23:49:40 +07:00
parent 4a4cff105f
commit 9bd26a46e1
23 changed files with 181 additions and 155 deletions
@@ -1,12 +1,16 @@
import { resolvePaseoHome } from "../src/server/paseo-home.js"; import { resolvePaseoHome } from "../src/server/paseo-home.js";
import { createRootLogger } from "../src/server/logger.js"; import { createRootLogger } from "../src/server/logger.js";
import { ensureSherpaOnnxModels } from "../src/server/speech/providers/local/sherpa/model-downloader.js"; import {
import type { SherpaOnnxModelId } from "../src/server/speech/providers/local/sherpa/model-catalog.js"; DEFAULT_LOCAL_STT_MODEL,
DEFAULT_LOCAL_TTS_MODEL,
ensureLocalSpeechModels,
type LocalSpeechModelId,
} from "../src/server/speech/providers/local/models.js";
function parseArgs(argv: string[]): { modelsDir: string; modelIds: SherpaOnnxModelId[] } { function parseArgs(argv: string[]): { modelsDir: string; modelIds: LocalSpeechModelId[] } {
const home = resolvePaseoHome(); const home = resolvePaseoHome();
let modelsDir = process.env.PASEO_SHERPA_ONNX_MODELS_DIR || `${home}/models/sherpa-onnx`; let modelsDir = process.env.PASEO_LOCAL_MODELS_DIR || `${home}/models/local-speech`;
const modelIds: SherpaOnnxModelId[] = []; const modelIds: LocalSpeechModelId[] = [];
for (let i = 0; i < argv.length; i++) { for (let i = 0; i < argv.length; i++) {
const arg = argv[i]; const arg = argv[i];
@@ -16,7 +20,7 @@ function parseArgs(argv: string[]): { modelsDir: string; modelIds: SherpaOnnxMod
continue; continue;
} }
if (arg === "--model") { if (arg === "--model") {
const id = argv[i + 1] as SherpaOnnxModelId | undefined; const id = argv[i + 1] as LocalSpeechModelId | undefined;
if (!id) { if (!id) {
throw new Error("--model requires a value"); throw new Error("--model requires a value");
} }
@@ -27,8 +31,8 @@ function parseArgs(argv: string[]): { modelsDir: string; modelIds: SherpaOnnxMod
} }
if (modelIds.length === 0) { if (modelIds.length === 0) {
const stt = (process.env.PASEO_SHERPA_STT_PRESET || "zipformer-bilingual-zh-en-2023-02-20") as SherpaOnnxModelId; const stt = (process.env.PASEO_LOCAL_STT_MODEL || DEFAULT_LOCAL_STT_MODEL) as LocalSpeechModelId;
const tts = (process.env.PASEO_SHERPA_TTS_PRESET || "pocket-tts-onnx-int8") as SherpaOnnxModelId; const tts = (process.env.PASEO_LOCAL_TTS_MODEL || DEFAULT_LOCAL_TTS_MODEL) as LocalSpeechModelId;
modelIds.push(stt, tts); modelIds.push(stt, tts);
} }
@@ -38,5 +42,5 @@ function parseArgs(argv: string[]): { modelsDir: string; modelIds: SherpaOnnxMod
const logger = createRootLogger({ level: "info", format: "pretty" }); const logger = createRootLogger({ level: "info", format: "pretty" });
const { modelsDir, modelIds } = parseArgs(process.argv.slice(2)); const { modelsDir, modelIds } = parseArgs(process.argv.slice(2));
await ensureSherpaOnnxModels({ modelsDir, modelIds, autoDownload: true, logger }); await ensureLocalSpeechModels({ modelsDir, modelIds, autoDownload: true, logger });
logger.info({ modelsDir, modelIds }, "Done downloading speech models"); logger.info({ modelsDir, modelIds }, "Done downloading speech models");
@@ -1,6 +1,6 @@
import { listSherpaOnnxModels } from "../src/server/speech/providers/local/sherpa/model-catalog.js"; import { listLocalSpeechModels } from "../src/server/speech/providers/local/models.js";
const models = listSherpaOnnxModels() const models = listLocalSpeechModels()
.slice() .slice()
.sort((a, b) => a.kind.localeCompare(b.kind) || a.id.localeCompare(b.id)); .sort((a, b) => a.kind.localeCompare(b.kind) || a.id.localeCompare(b.id));
@@ -18,11 +18,6 @@ describe("paseo daemon bootstrap", () => {
voiceStt: { provider: "openai", explicit: true }, voiceStt: { provider: "openai", explicit: true },
voiceTts: { provider: "openai", explicit: true }, voiceTts: { provider: "openai", explicit: true },
}, },
localModels: {
dictationStt: "parakeet-tdt-0.6b-v3-int8",
voiceStt: "parakeet-tdt-0.6b-v3-int8",
voiceTts: "pocket-tts-onnx-int8",
},
}, },
}); });
try { try {
@@ -68,11 +63,6 @@ describe("paseo daemon bootstrap", () => {
voiceStt: { provider: "openai", explicit: true }, voiceStt: { provider: "openai", explicit: true },
voiceTts: { provider: "openai", explicit: true }, voiceTts: { provider: "openai", explicit: true },
}, },
localModels: {
dictationStt: "parakeet-tdt-0.6b-v3-int8",
voiceStt: "parakeet-tdt-0.6b-v3-int8",
voiceTts: "pocket-tts-onnx-int8",
},
}, },
}; };
-7
View File
@@ -108,13 +108,6 @@ export type PaseoLocalSpeechConfig = LocalSpeechProviderConfig;
export type PaseoSpeechConfig = { export type PaseoSpeechConfig = {
providers: RequestedSpeechProviders; providers: RequestedSpeechProviders;
local?: PaseoLocalSpeechConfig; local?: PaseoLocalSpeechConfig;
localModels: {
dictationStt: string;
voiceStt: string;
voiceTts: string;
voiceTtsSpeakerId?: number;
voiceTtsSpeed?: number;
};
}; };
export type PaseoDaemonConfig = { export type PaseoDaemonConfig = {
+12 -14
View File
@@ -1,9 +1,10 @@
import path from "node:path"; import path from "node:path";
import { z } from "zod";
import type { PaseoDaemonConfig } from "./bootstrap.js"; import type { PaseoDaemonConfig } from "./bootstrap.js";
import { loadPersistedConfig } from "./persisted-config.js"; import { loadPersistedConfig } from "./persisted-config.js";
import type { AgentProvider } from "./agent/agent-sdk-types.js"; import type { AgentProvider } from "./agent/agent-sdk-types.js";
import { AGENT_PROVIDER_IDS } from "./agent/provider-manifest.js"; import { AgentProviderSchema } from "./agent/provider-manifest.js";
import { resolveSpeechConfig } from "./speech/speech-config-resolver.js"; import { resolveSpeechConfig } from "./speech/speech-config-resolver.js";
import { import {
mergeAllowedHosts, mergeAllowedHosts,
@@ -26,17 +27,14 @@ export type CliConfigOverrides = Partial<{
allowedHosts: AllowedHostsConfig; allowedHosts: AllowedHostsConfig;
}>; }>;
function parseVoiceLlmProviderId(value: unknown): AgentProvider | null { const OptionalVoiceLlmProviderSchema = z
if (typeof value !== "string") { .union([z.string(), z.null(), z.undefined()])
return null; .transform((value): string | null => (typeof value === "string" ? value.trim().toLowerCase() : null))
} .pipe(z.union([AgentProviderSchema, z.null()]));
const normalized = value.trim().toLowerCase();
if (!normalized) { function parseOptionalVoiceLlmProvider(value: unknown): AgentProvider | null {
return null; const parsed = OptionalVoiceLlmProviderSchema.safeParse(value);
} return parsed.success ? parsed.data : null;
return (AGENT_PROVIDER_IDS as readonly string[]).includes(normalized)
? (normalized as AgentProvider)
: null;
} }
export function loadConfig( export function loadConfig(
@@ -97,8 +95,8 @@ export function loadConfig(
persisted, persisted,
}); });
const envVoiceLlmProvider = parseVoiceLlmProviderId(env.PASEO_VOICE_LLM_PROVIDER); const envVoiceLlmProvider = parseOptionalVoiceLlmProvider(env.PASEO_VOICE_LLM_PROVIDER);
const persistedVoiceLlmProvider = parseVoiceLlmProviderId( const persistedVoiceLlmProvider = parseOptionalVoiceLlmProvider(
persisted.features?.voiceMode?.llm?.provider persisted.features?.voiceMode?.llm?.provider
); );
const voiceLlmProvider = envVoiceLlmProvider ?? persistedVoiceLlmProvider ?? null; const voiceLlmProvider = envVoiceLlmProvider ?? persistedVoiceLlmProvider ?? null;
@@ -101,11 +101,6 @@ describe("daemon client E2E", () => {
voiceStt: { provider: "openai" as const, explicit: true }, voiceStt: { provider: "openai" as const, explicit: true },
voiceTts: { provider: "openai" as const, explicit: true }, voiceTts: { provider: "openai" as const, explicit: true },
}, },
localModels: {
dictationStt: "parakeet-tdt-0.6b-v3-int8",
voiceStt: "parakeet-tdt-0.6b-v3-int8",
voiceTts: "pocket-tts-onnx-int8",
},
} }
: hasLocalSpeech : hasLocalSpeech
? { ? {
@@ -116,16 +111,16 @@ describe("daemon client E2E", () => {
}, },
local: { local: {
modelsDir: localModelsDir, modelsDir: localModelsDir,
}, models: {
localModels: { dictationStt:
dictationStt: process.env.PASEO_DICTATION_LOCAL_STT_MODEL ??
process.env.PASEO_DICTATION_LOCAL_STT_MODEL ?? "zipformer-bilingual-zh-en-2023-02-20",
"zipformer-bilingual-zh-en-2023-02-20", voiceStt:
voiceStt: process.env.PASEO_VOICE_LOCAL_STT_MODEL ??
process.env.PASEO_VOICE_LOCAL_STT_MODEL ?? "zipformer-bilingual-zh-en-2023-02-20",
"zipformer-bilingual-zh-en-2023-02-20", voiceTts:
voiceTts: process.env.PASEO_VOICE_LOCAL_TTS_MODEL ?? "kitten-nano-en-v0_1-fp16",
process.env.PASEO_VOICE_LOCAL_TTS_MODEL ?? "kitten-nano-en-v0_1-fp16", },
}, },
} }
: undefined; : undefined;
+13 -15
View File
@@ -103,13 +103,11 @@ import {
import { getProjectIcon } from "../utils/project-icon.js"; import { getProjectIcon } from "../utils/project-icon.js";
import { expandTilde } from "../utils/path.js"; import { expandTilde } from "../utils/path.js";
import { import {
ensureSherpaOnnxModels, ensureLocalSpeechModels,
getSherpaOnnxModelDir, getLocalSpeechModelDir,
} from "./speech/providers/local/sherpa/model-downloader.js"; listLocalSpeechModels,
import { type LocalSpeechModelId,
listSherpaOnnxModels, } from "./speech/providers/local/models.js";
type SherpaOnnxModelId,
} from "./speech/providers/local/sherpa/model-catalog.js";
import type pino from "pino"; import type pino from "pino";
const execAsync = promisify(exec); const execAsync = promisify(exec);
@@ -222,7 +220,7 @@ export type SessionOptions = {
stt?: SpeechToTextProvider | null; stt?: SpeechToTextProvider | null;
localModels?: { localModels?: {
modelsDir: string; modelsDir: string;
defaultModelIds: SherpaOnnxModelId[]; defaultModelIds: LocalSpeechModelId[];
}; };
}; };
}; };
@@ -371,7 +369,7 @@ export class Session {
private readonly voiceLlmModel: string | null; private readonly voiceLlmModel: string | null;
private readonly voiceAgentMcpStdio: VoiceMcpStdioConfig | null; private readonly voiceAgentMcpStdio: VoiceMcpStdioConfig | null;
private readonly localSpeechModelsDir: string; private readonly localSpeechModelsDir: string;
private readonly defaultLocalSpeechModelIds: SherpaOnnxModelId[]; private readonly defaultLocalSpeechModelIds: LocalSpeechModelId[];
private readonly registerVoiceSpeakHandler?: ( private readonly registerVoiceSpeakHandler?: (
agentId: string, agentId: string,
handler: VoiceSpeakHandler handler: VoiceSpeakHandler
@@ -1894,8 +1892,8 @@ export class Session {
const modelsDir = this.localSpeechModelsDir; const modelsDir = this.localSpeechModelsDir;
const models = await Promise.all( const models = await Promise.all(
listSherpaOnnxModels().map(async (model) => { listLocalSpeechModels().map(async (model) => {
const modelDir = getSherpaOnnxModelDir(modelsDir, model.id); const modelDir = getLocalSpeechModelDir(modelsDir, model.id);
const missingFiles: string[] = []; const missingFiles: string[] = [];
for (const rel of model.requiredFiles) { for (const rel of model.requiredFiles) {
const filePath = join(modelDir, rel); const filePath = join(modelDir, rel);
@@ -1943,8 +1941,8 @@ export class Session {
? msg.modelIds ? msg.modelIds
: this.defaultLocalSpeechModelIds; : this.defaultLocalSpeechModelIds;
const allModelIds = new Set(listSherpaOnnxModels().map((m) => m.id)); const allModelIds = new Set(listLocalSpeechModels().map((m) => m.id));
const invalid = modelIdsRaw.filter((id) => !allModelIds.has(id as SherpaOnnxModelId)); const invalid = modelIdsRaw.filter((id) => !allModelIds.has(id as LocalSpeechModelId));
if (invalid.length > 0) { if (invalid.length > 0) {
this.emit({ this.emit({
type: "speech_models_download_response", type: "speech_models_download_response",
@@ -1958,9 +1956,9 @@ export class Session {
return; return;
} }
const modelIds = modelIdsRaw as SherpaOnnxModelId[]; const modelIds = modelIdsRaw as LocalSpeechModelId[];
try { try {
await ensureSherpaOnnxModels({ await ensureLocalSpeechModels({
modelsDir, modelsDir,
modelIds, modelIds,
autoDownload: true, autoDownload: true,
@@ -12,20 +12,24 @@ import {
type LocalSpeechModelId, type LocalSpeechModelId,
type LocalSttModelId, type LocalSttModelId,
type LocalTtsModelId, type LocalTtsModelId,
} from "./sherpa/model-catalog.js"; } from "./models.js";
export type LocalSpeechModelConfig = {
dictationStt: LocalSttModelId;
voiceStt: LocalSttModelId;
voiceTts: LocalTtsModelId;
voiceTtsSpeakerId?: number;
voiceTtsSpeed?: number;
};
export type LocalSpeechProviderConfig = { export type LocalSpeechProviderConfig = {
modelsDir: string; modelsDir: string;
autoDownload?: boolean; autoDownload?: boolean;
models: LocalSpeechModelConfig;
}; };
export type ResolvedLocalSpeechConfig = { export type ResolvedLocalSpeechConfig = {
local: LocalSpeechProviderConfig | undefined; local: LocalSpeechProviderConfig | undefined;
dictationLocalSttModel: LocalSttModelId;
voiceLocalSttModel: LocalSttModelId;
voiceLocalTtsModel: LocalTtsModelId;
voiceLocalTtsSpeakerId?: number;
voiceLocalTtsSpeed?: number;
}; };
export type { LocalSpeechModelId, LocalSttModelId, LocalTtsModelId }; export type { LocalSpeechModelId, LocalSttModelId, LocalTtsModelId };
@@ -146,16 +150,18 @@ export function resolveLocalSpeechConfig(params: {
? { ? {
modelsDir: parsed.modelsDir, modelsDir: parsed.modelsDir,
autoDownload: parsed.autoDownload, autoDownload: parsed.autoDownload,
models: {
dictationStt: parsed.dictationLocalSttModel,
voiceStt: parsed.voiceLocalSttModel,
voiceTts: parsed.voiceLocalTtsModel,
...(parsed.voiceLocalTtsSpeakerId !== undefined
? { voiceTtsSpeakerId: parsed.voiceLocalTtsSpeakerId }
: {}),
...(parsed.voiceLocalTtsSpeed !== undefined
? { voiceTtsSpeed: parsed.voiceLocalTtsSpeed }
: {}),
},
} }
: undefined, : undefined,
dictationLocalSttModel: parsed.dictationLocalSttModel,
voiceLocalSttModel: parsed.voiceLocalSttModel,
voiceLocalTtsModel: parsed.voiceLocalTtsModel,
...(parsed.voiceLocalTtsSpeakerId !== undefined
? { voiceLocalTtsSpeakerId: parsed.voiceLocalTtsSpeakerId }
: {}),
...(parsed.voiceLocalTtsSpeed !== undefined
? { voiceLocalTtsSpeed: parsed.voiceLocalTtsSpeed }
: {}),
}; };
} }
@@ -0,0 +1,48 @@
import {
ensureSherpaOnnxModels,
getSherpaOnnxModelDir,
} from "./sherpa/model-downloader.js";
import {
DEFAULT_LOCAL_STT_MODEL,
DEFAULT_LOCAL_TTS_MODEL,
LocalSttModelIdSchema,
LocalTtsModelIdSchema,
listSherpaOnnxModels,
type LocalSpeechModelId,
type LocalSttModelId,
type LocalTtsModelId,
} from "./sherpa/model-catalog.js";
export {
DEFAULT_LOCAL_STT_MODEL,
DEFAULT_LOCAL_TTS_MODEL,
LocalSttModelIdSchema,
LocalTtsModelIdSchema,
type LocalSpeechModelId,
type LocalSttModelId,
type LocalTtsModelId,
};
export type LocalSpeechModelSpec = ReturnType<typeof listSherpaOnnxModels>[number];
export function listLocalSpeechModels(): LocalSpeechModelSpec[] {
return listSherpaOnnxModels();
}
export function getLocalSpeechModelDir(modelsDir: string, modelId: LocalSpeechModelId): string {
return getSherpaOnnxModelDir(modelsDir, modelId);
}
export async function ensureLocalSpeechModels(options: {
modelsDir: string;
modelIds: LocalSpeechModelId[];
autoDownload?: boolean;
logger: import("pino").Logger;
}): Promise<Record<LocalSpeechModelId, string>> {
return ensureSherpaOnnxModels({
modelsDir: options.modelsDir,
modelIds: options.modelIds,
autoDownload: options.autoDownload ?? true,
logger: options.logger,
});
}
@@ -5,10 +5,8 @@ import type { SpeechToTextProvider, TextToSpeechProvider } from "../../speech-pr
import type { RequestedSpeechProviders } from "../../speech-types.js"; import type { RequestedSpeechProviders } from "../../speech-types.js";
import { PocketTtsOnnxTTS } from "./pocket/pocket-tts-onnx.js"; import { PocketTtsOnnxTTS } from "./pocket/pocket-tts-onnx.js";
import { import {
ensureSherpaOnnxModels, ensureLocalSpeechModels,
getSherpaOnnxModelDir, getLocalSpeechModelDir,
} from "./sherpa/model-downloader.js";
import {
DEFAULT_LOCAL_STT_MODEL, DEFAULT_LOCAL_STT_MODEL,
DEFAULT_LOCAL_TTS_MODEL, DEFAULT_LOCAL_TTS_MODEL,
LocalSttModelIdSchema, LocalSttModelIdSchema,
@@ -16,7 +14,7 @@ import {
type LocalSpeechModelId, type LocalSpeechModelId,
type LocalSttModelId, type LocalSttModelId,
type LocalTtsModelId, type LocalTtsModelId,
} from "./sherpa/model-catalog.js"; } from "./models.js";
import { SherpaOfflineRecognizerEngine } from "./sherpa/sherpa-offline-recognizer.js"; import { SherpaOfflineRecognizerEngine } from "./sherpa/sherpa-offline-recognizer.js";
import { SherpaOnlineRecognizerEngine } from "./sherpa/sherpa-online-recognizer.js"; import { SherpaOnlineRecognizerEngine } from "./sherpa/sherpa-online-recognizer.js";
import { SherpaOnnxParakeetSTT } from "./sherpa/sherpa-parakeet-stt.js"; import { SherpaOnnxParakeetSTT } from "./sherpa/sherpa-parakeet-stt.js";
@@ -63,13 +61,13 @@ function resolveConfiguredLocalModels(
): ResolvedLocalModels { ): ResolvedLocalModels {
return { return {
dictationLocalSttModel: LocalSttModelIdSchema.parse( dictationLocalSttModel: LocalSttModelIdSchema.parse(
speechConfig?.localModels.dictationStt ?? DEFAULT_LOCAL_STT_MODEL speechConfig?.local?.models.dictationStt ?? DEFAULT_LOCAL_STT_MODEL
), ),
voiceLocalSttModel: LocalSttModelIdSchema.parse( voiceLocalSttModel: LocalSttModelIdSchema.parse(
speechConfig?.localModels.voiceStt ?? DEFAULT_LOCAL_STT_MODEL speechConfig?.local?.models.voiceStt ?? DEFAULT_LOCAL_STT_MODEL
), ),
voiceLocalTtsModel: LocalTtsModelIdSchema.parse( voiceLocalTtsModel: LocalTtsModelIdSchema.parse(
speechConfig?.localModels.voiceTts ?? DEFAULT_LOCAL_TTS_MODEL speechConfig?.local?.models.voiceTts ?? DEFAULT_LOCAL_TTS_MODEL
), ),
}; };
} }
@@ -110,7 +108,7 @@ async function createLocalSttEngine(params: {
const { modelId, modelsDir, logger } = params; const { modelId, modelsDir, logger } = params;
if (modelId === "parakeet-tdt-0.6b-v3-int8") { if (modelId === "parakeet-tdt-0.6b-v3-int8") {
const modelDir = getSherpaOnnxModelDir(modelsDir, modelId); const modelDir = getLocalSpeechModelDir(modelsDir, modelId);
return { return {
kind: "offline", kind: "offline",
engine: new SherpaOfflineRecognizerEngine( engine: new SherpaOfflineRecognizerEngine(
@@ -131,7 +129,7 @@ async function createLocalSttEngine(params: {
} }
if (modelId === "paraformer-bilingual-zh-en") { if (modelId === "paraformer-bilingual-zh-en") {
const modelDir = getSherpaOnnxModelDir(modelsDir, modelId); const modelDir = getLocalSpeechModelDir(modelsDir, modelId);
return { return {
kind: "online", kind: "online",
engine: new SherpaOnlineRecognizerEngine( engine: new SherpaOnlineRecognizerEngine(
@@ -151,7 +149,7 @@ async function createLocalSttEngine(params: {
} }
if (modelId === "zipformer-bilingual-zh-en-2023-02-20") { if (modelId === "zipformer-bilingual-zh-en-2023-02-20") {
const modelDir = getSherpaOnnxModelDir(modelsDir, modelId); const modelDir = getLocalSpeechModelDir(modelsDir, modelId);
return { return {
kind: "online", kind: "online",
engine: new SherpaOnlineRecognizerEngine( engine: new SherpaOnlineRecognizerEngine(
@@ -204,7 +202,7 @@ export async function initializeLocalSpeechServices(params: {
}, },
"Ensuring local speech models" "Ensuring local speech models"
); );
await ensureSherpaOnnxModels({ await ensureLocalSpeechModels({
modelsDir: localConfig.modelsDir, modelsDir: localConfig.modelsDir,
modelIds: requiredLocalModelIds, modelIds: requiredLocalModelIds,
autoDownload: localConfig.autoDownload ?? true, autoDownload: localConfig.autoDownload ?? true,
@@ -308,7 +306,7 @@ export async function initializeLocalSpeechServices(params: {
} else { } else {
try { try {
if (localModels.voiceLocalTtsModel === "pocket-tts-onnx-int8") { if (localModels.voiceLocalTtsModel === "pocket-tts-onnx-int8") {
const modelDir = getSherpaOnnxModelDir(localConfig.modelsDir, localModels.voiceLocalTtsModel); const modelDir = getLocalSpeechModelDir(localConfig.modelsDir, localModels.voiceLocalTtsModel);
localVoiceTtsProvider = await PocketTtsOnnxTTS.create( localVoiceTtsProvider = await PocketTtsOnnxTTS.create(
{ {
modelDir, modelDir,
@@ -318,13 +316,13 @@ export async function initializeLocalSpeechServices(params: {
logger logger
); );
} else { } else {
const modelDir = getSherpaOnnxModelDir(localConfig.modelsDir, localModels.voiceLocalTtsModel); const modelDir = getLocalSpeechModelDir(localConfig.modelsDir, localModels.voiceLocalTtsModel);
localVoiceTtsProvider = new SherpaOnnxTTS( localVoiceTtsProvider = new SherpaOnnxTTS(
{ {
preset: localModels.voiceLocalTtsModel, preset: localModels.voiceLocalTtsModel,
modelDir, modelDir,
speakerId: speechConfig?.localModels.voiceTtsSpeakerId, speakerId: speechConfig?.local?.models.voiceTtsSpeakerId,
speed: speechConfig?.localModels.voiceTtsSpeed, speed: speechConfig?.local?.models.voiceTtsSpeed,
}, },
logger logger
); );
@@ -209,7 +209,7 @@ export function listSherpaOnnxModels(): SherpaOnnxModelSpec[] {
export function getSherpaOnnxModelSpec(id: SherpaOnnxModelId): SherpaOnnxModelSpec { export function getSherpaOnnxModelSpec(id: SherpaOnnxModelId): SherpaOnnxModelSpec {
const spec = SHERPA_ONNX_MODEL_CATALOG[id]; const spec = SHERPA_ONNX_MODEL_CATALOG[id];
if (!spec) { if (!spec) {
throw new Error(`Unknown sherpa-onnx model id: ${id}`); throw new Error(`Unknown local speech model id: ${id}`);
} }
return { return {
id, id,
@@ -96,7 +96,7 @@ async function isNonEmptyFile(filePath: string): Promise<boolean> {
export async function ensureSherpaOnnxModel(options: EnsureSherpaOnnxModelOptions): Promise<string> { export async function ensureSherpaOnnxModel(options: EnsureSherpaOnnxModelOptions): Promise<string> {
const logger = options.logger.child({ const logger = options.logger.child({
module: "speech", module: "speech",
provider: "sherpa-onnx", provider: "local",
component: "model-downloader", component: "model-downloader",
modelId: options.modelId, modelId: options.modelId,
}); });
@@ -109,7 +109,7 @@ export async function ensureSherpaOnnxModel(options: EnsureSherpaOnnxModelOption
if (!options.autoDownload) { if (!options.autoDownload) {
throw new Error( throw new Error(
`Missing sherpa-onnx model files for ${options.modelId} in ${modelDir}. ` + `Missing local speech model files for ${options.modelId} in ${modelDir}. ` +
`Set PASEO_LOCAL_AUTO_DOWNLOAD=1 to auto-download.` `Set PASEO_LOCAL_AUTO_DOWNLOAD=1 to auto-download.`
); );
} }
@@ -34,7 +34,7 @@ export class SherpaOfflineRecognizerEngine {
private readonly logger: pino.Logger; private readonly logger: pino.Logger;
constructor(config: SherpaOfflineRecognizerConfig, logger: pino.Logger) { constructor(config: SherpaOfflineRecognizerConfig, logger: pino.Logger) {
this.logger = logger.child({ module: "speech", provider: "sherpa-onnx", component: "offline-recognizer" }); this.logger = logger.child({ module: "speech", provider: "local", component: "offline-recognizer" });
assertFileExists(config.model.encoder, "offline encoder"); assertFileExists(config.model.encoder, "offline encoder");
assertFileExists(config.model.decoder, "offline decoder"); assertFileExists(config.model.decoder, "offline decoder");
@@ -46,7 +46,7 @@ export class SherpaOnlineRecognizerEngine {
private readonly logger: pino.Logger; private readonly logger: pino.Logger;
constructor(config: SherpaOnlineRecognizerConfig, logger: pino.Logger) { constructor(config: SherpaOnlineRecognizerConfig, logger: pino.Logger) {
this.logger = logger.child({ module: "speech", provider: "sherpa-onnx", component: "online-recognizer" }); this.logger = logger.child({ module: "speech", provider: "local", component: "online-recognizer" });
const { model } = config; const { model } = config;
if (model.kind === "transducer") { if (model.kind === "transducer") {
@@ -25,7 +25,7 @@ export class SherpaOnnxParakeetSTT implements SpeechToTextProvider {
constructor(config: SherpaParakeetSttConfig, logger: pino.Logger) { constructor(config: SherpaParakeetSttConfig, logger: pino.Logger) {
this.engine = config.engine; this.engine = config.engine;
this.silencePeakThreshold = config.silencePeakThreshold ?? 300; this.silencePeakThreshold = config.silencePeakThreshold ?? 300;
this.logger = logger.child({ module: "speech", provider: "sherpa-onnx", component: "parakeet-stt" }); this.logger = logger.child({ module: "speech", provider: "local", component: "parakeet-stt" });
} }
public createSession(params: { public createSession(params: {
@@ -28,7 +28,7 @@ export class SherpaOnnxSTT implements SpeechToTextProvider {
this.engine = config.engine; this.engine = config.engine;
this.silencePeakThreshold = config.silencePeakThreshold ?? 300; this.silencePeakThreshold = config.silencePeakThreshold ?? 300;
this.tailPaddingMs = config.tailPaddingMs ?? 500; this.tailPaddingMs = config.tailPaddingMs ?? 500;
this.logger = logger.child({ module: "speech", provider: "sherpa-onnx", component: "stt" }); this.logger = logger.child({ module: "speech", provider: "local", component: "stt" });
} }
public createSession(params: { public createSession(params: {
@@ -33,7 +33,7 @@ export class SherpaOnnxTTS implements TextToSpeechProvider {
if (config.preset !== "kokoro-en-v0_19" && config.preset !== "kitten-nano-en-v0_1-fp16") { if (config.preset !== "kokoro-en-v0_19" && config.preset !== "kitten-nano-en-v0_1-fp16") {
throw new Error(`Unsupported Sherpa TTS preset: ${config.preset}`); throw new Error(`Unsupported Sherpa TTS preset: ${config.preset}`);
} }
this.logger = logger.child({ module: "speech", provider: "sherpa-onnx", component: "tts" }); this.logger = logger.child({ module: "speech", provider: "local", component: "tts" });
this.speakerId = config.speakerId ?? 0; this.speakerId = config.speakerId ?? 0;
this.speed = config.speed ?? 1.0; this.speed = config.speed ?? 1.0;
@@ -123,18 +123,18 @@ describe("speech models (download E2E)", () => {
local: { local: {
modelsDir, modelsDir,
autoDownload: false, autoDownload: false,
}, models: {
localModels: { dictationStt:
dictationStt: set === "parakeet-pocket"
set === "parakeet-pocket" ? "parakeet-tdt-0.6b-v3-int8"
? "parakeet-tdt-0.6b-v3-int8" : "zipformer-bilingual-zh-en-2023-02-20",
: "zipformer-bilingual-zh-en-2023-02-20", voiceStt:
voiceStt: set === "parakeet-pocket"
set === "parakeet-pocket" ? "parakeet-tdt-0.6b-v3-int8"
? "parakeet-tdt-0.6b-v3-int8" : "zipformer-bilingual-zh-en-2023-02-20",
: "zipformer-bilingual-zh-en-2023-02-20", voiceTts:
voiceTts: set === "parakeet-pocket" ? "pocket-tts-onnx-int8" : "kitten-nano-en-v0_1-fp16",
set === "parakeet-pocket" ? "pocket-tts-onnx-int8" : "kitten-nano-en-v0_1-fp16", },
}, },
}, },
}); });
@@ -33,10 +33,15 @@ describe("resolveSpeechConfig", () => {
expect(result.speech.local).toEqual({ expect(result.speech.local).toEqual({
modelsDir: path.join(paseoHome, "models", "local-speech"), modelsDir: path.join(paseoHome, "models", "local-speech"),
autoDownload: true, autoDownload: true,
models: {
dictationStt: "parakeet-tdt-0.6b-v3-int8",
voiceStt: "parakeet-tdt-0.6b-v3-int8",
voiceTts: "pocket-tts-onnx-int8",
},
}); });
expect(result.speech.localModels.dictationStt).toBe("parakeet-tdt-0.6b-v3-int8"); expect(result.speech.local?.models.dictationStt).toBe("parakeet-tdt-0.6b-v3-int8");
expect(result.speech.localModels.voiceStt).toBe("parakeet-tdt-0.6b-v3-int8"); expect(result.speech.local?.models.voiceStt).toBe("parakeet-tdt-0.6b-v3-int8");
expect(result.speech.localModels.voiceTts).toBe("pocket-tts-onnx-int8"); expect(result.speech.local?.models.voiceTts).toBe("pocket-tts-onnx-int8");
}); });
test("resolves feature-scoped local model env vars", () => { test("resolves feature-scoped local model env vars", () => {
@@ -73,6 +78,13 @@ describe("resolveSpeechConfig", () => {
expect(result.speech.local).toEqual({ expect(result.speech.local).toEqual({
modelsDir: "/tmp/models", modelsDir: "/tmp/models",
autoDownload: false, autoDownload: false,
models: {
dictationStt: "zipformer-bilingual-zh-en-2023-02-20",
voiceStt: "parakeet-tdt-0.6b-v3-int8",
voiceTts: "kitten-nano-en-v0_1-fp16",
voiceTtsSpeakerId: 5,
voiceTtsSpeed: 1.35,
},
}); });
expect(result.speech.providers.dictationStt).toEqual({ expect(result.speech.providers.dictationStt).toEqual({
provider: "local", provider: "local",
@@ -86,11 +98,11 @@ describe("resolveSpeechConfig", () => {
provider: "local", provider: "local",
explicit: true, explicit: true,
}); });
expect(result.speech.localModels.dictationStt).toBe("zipformer-bilingual-zh-en-2023-02-20"); expect(result.speech.local?.models.dictationStt).toBe("zipformer-bilingual-zh-en-2023-02-20");
expect(result.speech.localModels.voiceStt).toBe("parakeet-tdt-0.6b-v3-int8"); expect(result.speech.local?.models.voiceStt).toBe("parakeet-tdt-0.6b-v3-int8");
expect(result.speech.localModels.voiceTts).toBe("kitten-nano-en-v0_1-fp16"); expect(result.speech.local?.models.voiceTts).toBe("kitten-nano-en-v0_1-fp16");
expect(result.speech.localModels.voiceTtsSpeakerId).toBe(5); expect(result.speech.local?.models.voiceTtsSpeakerId).toBe(5);
expect(result.speech.localModels.voiceTtsSpeed).toBe(1.35); expect(result.speech.local?.models.voiceTtsSpeed).toBe(1.35);
expect(result.openai?.apiKey).toBe("env-key"); expect(result.openai?.apiKey).toBe("env-key");
expect(result.openai?.stt?.model).toBe("gpt-4o-transcribe"); expect(result.openai?.stt?.model).toBe("gpt-4o-transcribe");
}); });
@@ -108,8 +120,8 @@ describe("resolveSpeechConfig", () => {
persisted, persisted,
}); });
expect(result.speech.localModels.dictationStt).toBe("parakeet-tdt-0.6b-v3-int8"); expect(result.speech.local?.models.dictationStt).toBe("parakeet-tdt-0.6b-v3-int8");
expect(result.speech.localModels.voiceStt).toBe("parakeet-tdt-0.6b-v3-int8"); expect(result.speech.local?.models.voiceStt).toBe("parakeet-tdt-0.6b-v3-int8");
expect(result.speech.localModels.voiceTts).toBe("pocket-tts-onnx-int8"); expect(result.speech.local?.models.voiceTts).toBe("pocket-tts-onnx-int8");
}); });
}); });
@@ -100,17 +100,6 @@ export function resolveSpeechConfig(params: {
...(local.local ...(local.local
? { local: local.local } ? { local: local.local }
: {}), : {}),
localModels: {
dictationStt: local.dictationLocalSttModel,
voiceStt: local.voiceLocalSttModel,
voiceTts: local.voiceLocalTtsModel,
...(local.voiceLocalTtsSpeakerId !== undefined
? { voiceTtsSpeakerId: local.voiceLocalTtsSpeakerId }
: {}),
...(local.voiceLocalTtsSpeed !== undefined
? { voiceTtsSpeed: local.voiceLocalTtsSpeed }
: {}),
},
}, },
}; };
} }
@@ -53,11 +53,6 @@ function waitForSignal<T>(
voiceStt: { provider: "openai", explicit: true }, voiceStt: { provider: "openai", explicit: true },
voiceTts: { provider: "openai", explicit: true }, voiceTts: { provider: "openai", explicit: true },
}, },
localModels: {
dictationStt: "parakeet-tdt-0.6b-v3-int8",
voiceStt: "parakeet-tdt-0.6b-v3-int8",
voiceTts: "pocket-tts-onnx-int8",
},
}, },
voiceLlmProvider: "codex", voiceLlmProvider: "codex",
voiceLlmProviderExplicit: true, voiceLlmProviderExplicit: true,
@@ -20,7 +20,7 @@ import type { AgentProvider } from "./agent/agent-sdk-types.js";
import { PushTokenStore } from "./push/token-store.js"; import { PushTokenStore } from "./push/token-store.js";
import { PushService } from "./push/push-service.js"; import { PushService } from "./push/push-service.js";
import type { SpeechToTextProvider, TextToSpeechProvider } from "./speech/speech-provider.js"; import type { SpeechToTextProvider, TextToSpeechProvider } from "./speech/speech-provider.js";
import type { SherpaOnnxModelId } from "./speech/providers/local/sherpa/model-catalog.js"; import type { LocalSpeechModelId } from "./speech/providers/local/models.js";
import type { import type {
VoiceCallerContext, VoiceCallerContext,
VoiceMcpStdioConfig, VoiceMcpStdioConfig,
@@ -79,7 +79,7 @@ export class VoiceAssistantWebSocketServer {
stt?: SpeechToTextProvider | null; stt?: SpeechToTextProvider | null;
localModels?: { localModels?: {
modelsDir: string; modelsDir: string;
defaultModelIds: SherpaOnnxModelId[]; defaultModelIds: LocalSpeechModelId[];
}; };
} | null; } | null;
private readonly voice: { private readonly voice: {
@@ -119,7 +119,7 @@ export class VoiceAssistantWebSocketServer {
stt?: SpeechToTextProvider | null; stt?: SpeechToTextProvider | null;
localModels?: { localModels?: {
modelsDir: string; modelsDir: string;
defaultModelIds: SherpaOnnxModelId[]; defaultModelIds: LocalSpeechModelId[];
}; };
} }
) { ) {
+2 -2
View File
@@ -287,8 +287,8 @@ function FAQ() {
<FAQItem question="Is this free?"> <FAQItem question="Is this free?">
Paseo is free and open source. It wraps CLI tools like Claude Code and Paseo is free and open source. It wraps CLI tools like Claude Code and
Codex, which you'll need to have installed and configured with your Codex, which you'll need to have installed and configured with your
own credentials. Voice features currently require an OpenAI API key, own credentials. Voice is local-first by default and can optionally use
but local voice is coming soon. OpenAI speech providers if you configure them.
</FAQItem> </FAQItem>
<FAQItem question="Does my code leave my machine?"> <FAQItem question="Does my code leave my machine?">
Paseo itself doesn't send your code anywhere. Agents run locally and Paseo itself doesn't send your code anywhere. Agents run locally and