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