diff --git a/packages/app/e2e/global-setup.ts b/packages/app/e2e/global-setup.ts index fba4a06a2..366d42267 100644 --- a/packages/app/e2e/global-setup.ts +++ b/packages/app/e2e/global-setup.ts @@ -156,7 +156,6 @@ export default async function globalSetup() { // Keep e2e bootstrap fast and deterministic; terminal/sidebar tests do not need speech. PASEO_DICTATION_ENABLED: "0", PASEO_VOICE_MODE_ENABLED: "0", - PASEO_LOCAL_AUTO_DOWNLOAD: "0", NODE_ENV: 'development', }, stdio: ['ignore', 'pipe', 'pipe'], diff --git a/packages/cli/src/commands/onboard.ts b/packages/cli/src/commands/onboard.ts index 7f589780d..26ec43c16 100644 --- a/packages/cli/src/commands/onboard.ts +++ b/packages/cli/src/commands/onboard.ts @@ -25,9 +25,6 @@ interface OnboardOptions extends DaemonStartOptions { } type OnboardPersistedConfig = PersistedConfig & { - providers?: PersistedConfig['providers'] & { - local?: PersistedConfig['providers'] extends { local?: infer T } ? T : { autoDownload?: boolean } - } features?: PersistedConfig['features'] & { dictation?: PersistedConfig['features'] extends { dictation?: infer T } ? T & { enabled?: boolean } @@ -103,13 +100,6 @@ function savePersistedConfig(paseoHome: string, config: OnboardPersistedConfig): function applyVoiceSelection(config: OnboardPersistedConfig, enabled: boolean): OnboardPersistedConfig { return { ...config, - providers: { - ...config.providers, - local: { - ...config.providers?.local, - autoDownload: enabled, - }, - }, features: { ...config.features, dictation: { @@ -366,7 +356,7 @@ export async function runOnboard(options: OnboardOptions): Promise { const config = loadConfig(paseoHome, { cli: toCliOverrides(options) }) const voiceStatus = voiceEnabled - ? 'Voice features enabled. Local speech models will download in the background if missing.' + ? 'Voice features enabled. Local speech models will be downloaded automatically if missing.' : 'Voice features disabled. Local speech models will not be downloaded.' log.message(voiceStatus) diff --git a/packages/cli/tests/17-onboard.test.ts b/packages/cli/tests/17-onboard.test.ts index 95a4cbeb0..e064961ae 100644 --- a/packages/cli/tests/17-onboard.test.ts +++ b/packages/cli/tests/17-onboard.test.ts @@ -46,14 +46,10 @@ try { dictation?: { enabled?: boolean } voiceMode?: { enabled?: boolean } } - providers?: { - local?: { autoDownload?: boolean } - } } assert.strictEqual(config.features?.dictation?.enabled, false, 'dictation.enabled should be false') assert.strictEqual(config.features?.voiceMode?.enabled, false, 'voiceMode.enabled should be false') - assert.strictEqual(config.providers?.local?.autoDownload, false, 'local.autoDownload should be false') const daemonLog = await readFile(join(paseoHome, 'daemon.log'), 'utf-8') assert( !daemonLog.includes('Ensuring local speech models'), diff --git a/packages/server/scripts/dev-runner.ts b/packages/server/scripts/dev-runner.ts index 1f303a1f9..de50c9eb0 100644 --- a/packages/server/scripts/dev-runner.ts +++ b/packages/server/scripts/dev-runner.ts @@ -1,7 +1,13 @@ import { fileURLToPath } from "url"; import { existsSync } from "node:fs"; +import dotenv from "dotenv"; import { runSupervisor } from "./supervisor.js"; +dotenv.config({ + path: fileURLToPath(new URL("../.env", import.meta.url)), + quiet: true, +}); + const WORKER_ENTRY = fileURLToPath(new URL("../src/server/index.ts", import.meta.url)); if (!existsSync(WORKER_ENTRY)) { throw new Error(`Dev worker entry not found: ${WORKER_ENTRY}`); diff --git a/packages/server/scripts/download-speech-models.ts b/packages/server/scripts/download-speech-models.ts index fe2b91667..0fe05cb93 100644 --- a/packages/server/scripts/download-speech-models.ts +++ b/packages/server/scripts/download-speech-models.ts @@ -42,5 +42,5 @@ function parseArgs(argv: string[]): { modelsDir: string; modelIds: LocalSpeechMo const logger = createRootLogger({ level: "info", format: "pretty" }); const { modelsDir, modelIds } = parseArgs(process.argv.slice(2)); -await ensureLocalSpeechModels({ modelsDir, modelIds, autoDownload: true, logger }); +await ensureLocalSpeechModels({ modelsDir, modelIds, logger }); logger.info({ modelsDir, modelIds }, "Done downloading speech models"); diff --git a/packages/server/scripts/generate-sherpa-tts-matrix.ts b/packages/server/scripts/generate-sherpa-tts-matrix.ts index 8bc52eced..0e30bc42a 100644 --- a/packages/server/scripts/generate-sherpa-tts-matrix.ts +++ b/packages/server/scripts/generate-sherpa-tts-matrix.ts @@ -284,7 +284,6 @@ logger.info( await ensureLocalSpeechModels({ modelsDir: options.modelsDir, modelIds: options.modelIds, - autoDownload: true, logger, }); diff --git a/packages/server/scripts/transcribe-local-wav.ts b/packages/server/scripts/transcribe-local-wav.ts index 3c7ff94f6..f056b3526 100644 --- a/packages/server/scripts/transcribe-local-wav.ts +++ b/packages/server/scripts/transcribe-local-wav.ts @@ -18,12 +18,11 @@ type CliOptions = { outPath?: string; model: LocalSttModelId; modelsDir: string; - autoDownload: boolean; }; function usage(): string { return [ - "Usage: npm run speech:transcribe:local -- [--out ] [--model ] [--models-dir ] [--no-auto-download]", + "Usage: npm run speech:transcribe:local -- [--out ] [--model ] [--models-dir ]", "", "Examples:", " npm run speech:transcribe:local -- ./sample.wav", @@ -52,7 +51,6 @@ function parseArgs(argv: string[]): CliOptions { let outPath: string | undefined; let model = LocalSttModelIdSchema.parse(process.env.PASEO_LOCAL_STT_MODEL ?? DEFAULT_LOCAL_STT_MODEL); let modelsDir = defaultModelsDir; - let autoDownload = true; for (let i = 0; i < argv.length; i++) { const arg = argv[i]; @@ -87,11 +85,6 @@ function parseArgs(argv: string[]): CliOptions { continue; } - if (arg === "--no-auto-download") { - autoDownload = false; - continue; - } - if (arg.startsWith("-")) { throw new Error(`Unknown option: ${arg}`); } @@ -108,7 +101,6 @@ function parseArgs(argv: string[]): CliOptions { ...(outPath ? { outPath } : {}), model, modelsDir, - autoDownload, }; } @@ -139,7 +131,6 @@ async function main(): Promise { providers, local: { modelsDir: options.modelsDir, - autoDownload: options.autoDownload, models: { dictationStt: options.model, voiceStt: options.model, diff --git a/packages/server/src/server/bootstrap.ts b/packages/server/src/server/bootstrap.ts index ee4f3ed29..ce58cd97c 100644 --- a/packages/server/src/server/bootstrap.ts +++ b/packages/server/src/server/bootstrap.ts @@ -63,7 +63,7 @@ import type { AgentProvider, } from "./agent/agent-sdk-types.js"; import type { AgentProviderRuntimeSettingsMap } from "./agent/provider-launch-config.js"; -import { acquirePidLock, releasePidLock, isLocked, PidLockError } from "./pid-lock.js"; +import { acquirePidLock, releasePidLock } from "./pid-lock.js"; import { isHostAllowed, type AllowedHostsConfig } from "./allowed-hosts.js"; import { createVoiceMcpSocketBridgeManager, @@ -137,16 +137,6 @@ export async function createPaseoDaemon( ): Promise { const logger = rootLogger.child({ module: "bootstrap" }); - // Fail fast before expensive bootstrap (speech/runtime/model checks) if another daemon is active. - const lockState = await isLocked(config.paseoHome); - if (lockState.locked) { - const existingLock = lockState.info; - throw new PidLockError( - `Another Paseo daemon is already running (PID ${existingLock?.pid ?? "unknown"}, started ${existingLock?.startedAt ?? "unknown"})`, - existingLock - ); - } - // Acquire PID lock before expensive bootstrap work so duplicate starts fail immediately. await acquirePidLock(config.paseoHome, config.listen); diff --git a/packages/server/src/server/daemon-e2e/connection-offer.e2e.test.ts b/packages/server/src/server/daemon-e2e/connection-offer.e2e.test.ts index eff0746a7..1bbe3d276 100644 --- a/packages/server/src/server/daemon-e2e/connection-offer.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/connection-offer.e2e.test.ts @@ -186,7 +186,8 @@ describe("ConnectionOfferV2 (daemon E2E)", () => { PASEO_HOME: tempHome, PASEO_LISTEN: `0.0.0.0:${port}`, OPENAI_API_KEY: "", - PASEO_LOCAL_AUTO_DOWNLOAD: "0", + PASEO_DICTATION_ENABLED: "0", + PASEO_VOICE_MODE_ENABLED: "0", PASEO_LOG_FORMAT: "json", }; diff --git a/packages/server/src/server/index.ts b/packages/server/src/server/index.ts index 80d687c64..ada1e4eba 100644 --- a/packages/server/src/server/index.ts +++ b/packages/server/src/server/index.ts @@ -1,9 +1,3 @@ -import dotenv from "dotenv"; -import { fileURLToPath } from "node:url"; -import path from "node:path"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -dotenv.config({ path: path.resolve(__dirname, "../../.env") }); import { createPaseoDaemon } from "./bootstrap.js"; import { loadConfig } from "./config.js"; import { resolvePaseoHome } from "./paseo-home.js"; diff --git a/packages/server/src/server/persisted-config.ts b/packages/server/src/server/persisted-config.ts index c2c2b8bd9..90ee10c33 100644 --- a/packages/server/src/server/persisted-config.ts +++ b/packages/server/src/server/persisted-config.ts @@ -22,7 +22,6 @@ const ProviderCredentialsSchema = z const LocalSpeechProviderSchema = z .object({ modelsDir: z.string().min(1).optional(), - autoDownload: z.boolean().optional(), }) .strict(); @@ -175,6 +174,34 @@ function getLogger(logger: LoggerLike | undefined): LoggerLike | undefined { return logger?.child({ module: "config" }); } +function stripDeprecatedLocalSpeechConfigFields(parsed: unknown): unknown { + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + return parsed; + } + + const root = { ...(parsed as Record) }; + const providers = root.providers; + if (!providers || typeof providers !== "object" || Array.isArray(providers)) { + return root; + } + + const providersRecord = { ...(providers as Record) }; + const local = providersRecord.local; + if (!local || typeof local !== "object" || Array.isArray(local)) { + root.providers = providersRecord; + return root; + } + + const localRecord = { ...(local as Record) }; + if ("autoDownload" in localRecord) { + delete localRecord.autoDownload; + } + + providersRecord.local = localRecord; + root.providers = providersRecord; + return root; +} + export function loadPersistedConfig( paseoHome: string, logger?: LoggerLike @@ -209,7 +236,8 @@ export function loadPersistedConfig( throw new Error(`[Config] Invalid JSON in ${configPath}: ${message}`); } - const result = PersistedConfigSchema.safeParse(parsed); + const migrated = stripDeprecatedLocalSpeechConfigFields(parsed); + const result = PersistedConfigSchema.safeParse(migrated); if (!result.success) { const issues = result.error.issues .map((i) => ` - ${i.path.join(".")}: ${i.message}`) diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 3ba3ded09..54a4a61fd 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -2879,7 +2879,6 @@ export class Session { await ensureLocalSpeechModels({ modelsDir, modelIds, - autoDownload: true, logger: this.sessionLogger, }); this.emit({ diff --git a/packages/server/src/server/speech/providers/local/config.ts b/packages/server/src/server/speech/providers/local/config.ts index e494b9842..93da426ce 100644 --- a/packages/server/src/server/speech/providers/local/config.ts +++ b/packages/server/src/server/speech/providers/local/config.ts @@ -24,7 +24,6 @@ export type LocalSpeechModelConfig = { export type LocalSpeechProviderConfig = { modelsDir: string; - autoDownload?: boolean; models: LocalSpeechModelConfig; }; @@ -36,17 +35,6 @@ export type { LocalSpeechModelId, LocalSttModelId, LocalTtsModelId }; const DEFAULT_LOCAL_MODELS_SUBDIR = path.join("models", "local-speech"); -const BooleanStringSchema = z - .string() - .trim() - .toLowerCase() - .pipe(z.enum(["1", "0", "true", "false", "yes", "no"])) - .transform((value) => value === "1" || value === "true" || value === "yes"); - -const OptionalBooleanFlagSchema = z - .union([z.boolean(), BooleanStringSchema]) - .optional(); - const NumberLikeSchema = z.union([ z.number(), z.string().trim().min(1), @@ -63,7 +51,6 @@ const OptionalIntegerSchema = NumberLikeSchema const LocalSpeechResolutionSchema = z.object({ includeProviderConfig: z.boolean(), modelsDir: z.string().trim().min(1), - autoDownload: OptionalBooleanFlagSchema.default(true), dictationLocalSttModel: LocalSttModelIdSchema.default(DEFAULT_LOCAL_STT_MODEL), voiceLocalSttModel: LocalSttModelIdSchema.default(DEFAULT_LOCAL_STT_MODEL), voiceLocalTtsModel: LocalTtsModelIdSchema.default(DEFAULT_LOCAL_TTS_MODEL), @@ -98,7 +85,7 @@ function shouldIncludeLocalProviderConfig(params: { return ( localRequestedByFeature || params.env.PASEO_LOCAL_MODELS_DIR !== undefined || - params.persisted.providers?.local !== undefined + params.persisted.providers?.local?.modelsDir !== undefined ); } @@ -116,9 +103,6 @@ export function resolveLocalSpeechConfig(params: { params.env.PASEO_LOCAL_MODELS_DIR ?? params.persisted.providers?.local?.modelsDir ?? path.join(params.paseoHome, DEFAULT_LOCAL_MODELS_SUBDIR), - autoDownload: - params.env.PASEO_LOCAL_AUTO_DOWNLOAD ?? - params.persisted.providers?.local?.autoDownload, dictationLocalSttModel: params.env.PASEO_DICTATION_LOCAL_STT_MODEL ?? persistedLocalFeatureModel( @@ -160,7 +144,6 @@ export function resolveLocalSpeechConfig(params: { parsed.includeProviderConfig ? { modelsDir: parsed.modelsDir, - autoDownload: parsed.autoDownload, models: { dictationStt: parsed.dictationLocalSttModel, voiceStt: parsed.voiceLocalSttModel, diff --git a/packages/server/src/server/speech/providers/local/models.ts b/packages/server/src/server/speech/providers/local/models.ts index ee2dee911..f9861c52e 100644 --- a/packages/server/src/server/speech/providers/local/models.ts +++ b/packages/server/src/server/speech/providers/local/models.ts @@ -36,13 +36,11 @@ export function getLocalSpeechModelDir(modelsDir: string, modelId: LocalSpeechMo export async function ensureLocalSpeechModels(options: { modelsDir: string; modelIds: LocalSpeechModelId[]; - autoDownload?: boolean; logger: import("pino").Logger; }): Promise> { return ensureSherpaOnnxModels({ modelsDir: options.modelsDir, modelIds: options.modelIds, - autoDownload: options.autoDownload ?? true, logger: options.logger, }); } diff --git a/packages/server/src/server/speech/providers/local/runtime.ts b/packages/server/src/server/speech/providers/local/runtime.ts index 44303f7a1..7c15bd6fd 100644 --- a/packages/server/src/server/speech/providers/local/runtime.ts +++ b/packages/server/src/server/speech/providers/local/runtime.ts @@ -36,7 +36,6 @@ type ResolvedLocalModels = { type LocalSpeechAvailability = { configured: boolean; modelsDir: string | null; - autoDownload: boolean | null; }; export type InitializedLocalSpeech = { @@ -79,7 +78,6 @@ export function getLocalSpeechAvailability( return { configured: Boolean(localConfig), modelsDir: localConfig?.modelsDir ?? null, - autoDownload: localConfig?.autoDownload ?? null, }; } @@ -186,12 +184,10 @@ export async function initializeLocalSpeechServices(params: { providers: RequestedSpeechProviders; speechConfig: PaseoSpeechConfig | null; logger: Logger; - modelEnsureAutoDownload?: boolean; }): Promise { const { providers, logger, speechConfig } = params; const localConfig = speechConfig?.local ?? null; const localModels = resolveConfiguredLocalModels(speechConfig); - const modelEnsureAutoDownload = params.modelEnsureAutoDownload ?? (localConfig?.autoDownload ?? true); let sttService: SpeechToTextProvider | null = null; let ttsService: TextToSpeechProvider | null = null; @@ -209,28 +205,25 @@ export async function initializeLocalSpeechServices(params: { { modelsDir: localConfig.modelsDir, modelIds: requiredLocalModelIds, - autoDownload: modelEnsureAutoDownload, }, "Ensuring local speech models" ); await ensureLocalSpeechModels({ modelsDir: localConfig.modelsDir, modelIds: requiredLocalModelIds, - autoDownload: modelEnsureAutoDownload, logger, }); } catch (err) { - logger.error( + logger.warn( { err, modelsDir: localConfig.modelsDir, modelIds: requiredLocalModelIds, - autoDownload: modelEnsureAutoDownload, hint: "Use `paseo speech models` to inspect status and " + "`paseo speech download --model ` to fetch missing models.", }, - "Failed to ensure local speech models" + "Local speech model bootstrap failed" ); } } @@ -256,14 +249,14 @@ export async function initializeLocalSpeechServices(params: { localSttEngines.set(modelId, created); return created; } catch (err) { - logger.error( + logger.warn( { err, modelsDir: localConfig.modelsDir, modelId, hint: buildModelDownloadHint(modelId), }, - "Failed to initialize local STT engine (models missing or invalid)" + "Local STT engine unavailable" ); return null; } @@ -340,14 +333,14 @@ export async function initializeLocalSpeechServices(params: { } ttsService = localVoiceTtsProvider; } catch (err) { - logger.error( + logger.warn( { err, modelsDir: localConfig.modelsDir, modelId: localModels.voiceLocalTtsModel, hint: buildModelDownloadHint(localModels.voiceLocalTtsModel), }, - "Failed to initialize local TTS engine (models missing or invalid)" + "Local TTS engine unavailable" ); } } diff --git a/packages/server/src/server/speech/providers/local/sherpa/model-downloader.test.ts b/packages/server/src/server/speech/providers/local/sherpa/model-downloader.test.ts index 6152a5236..31b59bf7d 100644 --- a/packages/server/src/server/speech/providers/local/sherpa/model-downloader.test.ts +++ b/packages/server/src/server/speech/providers/local/sherpa/model-downloader.test.ts @@ -35,25 +35,12 @@ describe("sherpa model downloader", () => { const out = await ensureSherpaOnnxModel({ modelsDir, modelId: "kitten-nano-en-v0_1-fp16", - autoDownload: false, logger, }); expect(out).toBe(modelDir); }); - test("ensureSherpaOnnxModel throws when missing and autoDownload is false", async () => { - const modelsDir = makeTmpDir(); - await expect( - ensureSherpaOnnxModel({ - modelsDir, - modelId: "zipformer-bilingual-zh-en-2023-02-20", - autoDownload: false, - logger, - }) - ).rejects.toThrow(/auto-download/i); - }); - test("ensureSherpaOnnxModel logs lifecycle events without progress spam", async () => { const modelsDir = makeTmpDir(); const infoMessages: string[] = []; @@ -82,7 +69,6 @@ describe("sherpa model downloader", () => { await ensureSherpaOnnxModel({ modelsDir, modelId: "pocket-tts-onnx-int8", - autoDownload: true, logger: loggerWithSpy, }); } finally { diff --git a/packages/server/src/server/speech/providers/local/sherpa/model-downloader.ts b/packages/server/src/server/speech/providers/local/sherpa/model-downloader.ts index 4ef2c11bc..c0cf10da1 100644 --- a/packages/server/src/server/speech/providers/local/sherpa/model-downloader.ts +++ b/packages/server/src/server/speech/providers/local/sherpa/model-downloader.ts @@ -11,7 +11,6 @@ import { getSherpaOnnxModelSpec, type SherpaOnnxModelId } from "./model-catalog. export type EnsureSherpaOnnxModelOptions = { modelsDir: string; modelId: SherpaOnnxModelId; - autoDownload: boolean; logger: pino.Logger; }; @@ -104,13 +103,6 @@ export async function ensureSherpaOnnxModel(options: EnsureSherpaOnnxModelOption return modelDir; } - if (!options.autoDownload) { - throw new Error( - `Missing local speech model files for ${options.modelId} in ${modelDir}. ` + - `Set PASEO_LOCAL_AUTO_DOWNLOAD=1 to auto-download.` - ); - } - logger.info({ modelsDir: options.modelsDir }, "Starting model download"); try { @@ -207,7 +199,6 @@ export async function ensureSherpaOnnxModel(options: EnsureSherpaOnnxModelOption export async function ensureSherpaOnnxModels(options: { modelsDir: string; modelIds: SherpaOnnxModelId[]; - autoDownload: boolean; logger: pino.Logger; }): Promise> { const uniq = Array.from(new Set(options.modelIds)); @@ -216,7 +207,6 @@ export async function ensureSherpaOnnxModels(options: { out[id] = await ensureSherpaOnnxModel({ modelsDir: options.modelsDir, modelId: id, - autoDownload: options.autoDownload, logger: options.logger, }); } diff --git a/packages/server/src/server/speech/providers/local/sherpa/speech-download.e2e.test.ts b/packages/server/src/server/speech/providers/local/sherpa/speech-download.e2e.test.ts index f567ad39b..287616202 100644 --- a/packages/server/src/server/speech/providers/local/sherpa/speech-download.e2e.test.ts +++ b/packages/server/src/server/speech/providers/local/sherpa/speech-download.e2e.test.ts @@ -106,7 +106,6 @@ describe("speech models (download E2E)", () => { await ensureSherpaOnnxModels({ modelsDir, modelIds, - autoDownload: true, logger, }); @@ -121,7 +120,6 @@ describe("speech models (download E2E)", () => { }, local: { modelsDir, - autoDownload: false, models: { dictationStt: set === "parakeet-pocket" diff --git a/packages/server/src/server/speech/speech-config-resolver.test.ts b/packages/server/src/server/speech/speech-config-resolver.test.ts index 5015cdb09..0e715af79 100644 --- a/packages/server/src/server/speech/speech-config-resolver.test.ts +++ b/packages/server/src/server/speech/speech-config-resolver.test.ts @@ -35,7 +35,6 @@ describe("resolveSpeechConfig", () => { }); expect(result.speech.local).toEqual({ modelsDir: path.join(paseoHome, "models", "local-speech"), - autoDownload: true, models: { dictationStt: "parakeet-tdt-0.6b-v2-int8", voiceStt: "parakeet-tdt-0.6b-v2-int8", @@ -67,7 +66,6 @@ describe("resolveSpeechConfig", () => { PASEO_VOICE_LOCAL_TTS_SPEAKER_ID: "5", PASEO_VOICE_LOCAL_TTS_SPEED: "1.35", PASEO_LOCAL_MODELS_DIR: "/tmp/models", - PASEO_LOCAL_AUTO_DOWNLOAD: "0", OPENAI_API_KEY: "env-key", PASEO_VOICE_STT_PROVIDER: "openai", PASEO_DICTATION_STT_PROVIDER: "local", @@ -82,7 +80,6 @@ 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", diff --git a/packages/server/src/server/speech/speech-runtime.test.ts b/packages/server/src/server/speech/speech-runtime.test.ts index fc2fe097c..b34b2f936 100644 --- a/packages/server/src/server/speech/speech-runtime.test.ts +++ b/packages/server/src/server/speech/speech-runtime.test.ts @@ -79,7 +79,6 @@ describe("initializeSpeechRuntime readiness", () => { availability: { configured: false, modelsDir: null, - autoDownload: null, }, cleanup: () => {}, }); @@ -115,7 +114,6 @@ describe("initializeSpeechRuntime readiness", () => { availability: { configured: false, modelsDir: null, - autoDownload: null, }, cleanup: () => {}, }); diff --git a/packages/server/src/server/speech/speech-runtime.ts b/packages/server/src/server/speech/speech-runtime.ts index 0492fb317..618a8d46b 100644 --- a/packages/server/src/server/speech/speech-runtime.ts +++ b/packages/server/src/server/speech/speech-runtime.ts @@ -205,7 +205,6 @@ function buildVoiceFeatureReadiness(params: { missingLocalModelIds: LocalSpeechModelId[]; backgroundDownloadInProgress: boolean; backgroundDownloadError: string | null; - localAutoDownloadEnabled: boolean; }): SpeechReadinessState { const enabled = params.realtimeVoice.enabled || params.dictation.enabled; if (!enabled) { @@ -246,7 +245,7 @@ function buildVoiceFeatureReadiness(params: { available: false, reasonCode: "models_missing", message: `Voice features are unavailable: missing local models (${joinModelIds(missingModelIds)}).`, - retryable: params.localAutoDownloadEnabled, + retryable: true, missingModelIds, }; } @@ -328,7 +327,6 @@ export async function initializeSpeechRuntime(params: { const openaiConfig = params.openaiConfig; const providers = resolveRequestedSpeechProviders(speechConfig); const requestedProviders = describeRequestedProviders(providers); - const localAutoDownloadEnabled = speechConfig?.local?.autoDownload ?? true; validateOpenAiCredentialRequirements({ providers, @@ -382,7 +380,6 @@ export async function initializeSpeechRuntime(params: { missingLocalModelIds, backgroundDownloadInProgress, backgroundDownloadError, - localAutoDownloadEnabled, }); return { generatedAt: new Date().toISOString(), @@ -459,12 +456,11 @@ export async function initializeSpeechRuntime(params: { }); }; - const reconcileServices = async (modelEnsureAutoDownload: boolean): Promise => { + const reconcileServices = async (): Promise => { const nextLocalSpeech = await initializeLocalSpeechServices({ providers, speechConfig, logger, - modelEnsureAutoDownload, }); const nextOpenAiSpeech = initializeOpenAiSpeechServices({ providers, @@ -521,13 +517,13 @@ export async function initializeSpeechRuntime(params: { } }; - const runReconcile = async (params: { modelEnsureAutoDownload: boolean }): Promise => { + const runReconcile = async (): Promise => { if (reconcileInFlight) { await reconcileInFlight; publishReadinessIfChanged(); return; } - reconcileInFlight = reconcileServices(params.modelEnsureAutoDownload).finally(() => { + reconcileInFlight = reconcileServices().finally(() => { reconcileInFlight = null; }); await reconcileInFlight; @@ -550,7 +546,7 @@ export async function initializeSpeechRuntime(params: { } const modelsDir = localModelConfig?.modelsDir ?? null; const modelIds = [...missingLocalModelIds]; - if (!modelsDir || modelIds.length === 0 || !localAutoDownloadEnabled) { + if (!modelsDir || modelIds.length === 0) { return; } @@ -571,10 +567,9 @@ export async function initializeSpeechRuntime(params: { await ensureLocalSpeechModels({ modelsDir, modelIds, - autoDownload: true, logger, }); - await runReconcile({ modelEnsureAutoDownload: false }); + await runReconcile(); backgroundDownloadError = null; } catch (error) { backgroundDownloadError = error instanceof Error ? error.message : String(error); @@ -610,12 +605,11 @@ export async function initializeSpeechRuntime(params: { missingLocalModelIds.length === 0 && !backgroundDownloadInProgress ) { - await runReconcile({ modelEnsureAutoDownload: false }); + await runReconcile(); } if ( missingLocalModelIds.length > 0 && - localAutoDownloadEnabled && !backgroundDownloadInProgress && !backgroundDownloadError ) { @@ -629,10 +623,10 @@ export async function initializeSpeechRuntime(params: { } }; - await runReconcile({ modelEnsureAutoDownload: false }); + await runReconcile(); const snapshot = computeReadinessSnapshot(); if (snapshot.voiceFeature.enabled && !snapshot.voiceFeature.available) { - if (missingLocalModelIds.length > 0 && localAutoDownloadEnabled) { + if (missingLocalModelIds.length > 0) { startBackgroundDownload(); } scheduleMonitor(); diff --git a/packages/website/public/schemas/paseo.config.v1.json b/packages/website/public/schemas/paseo.config.v1.json index 0bbe1d253..5afb19a64 100644 --- a/packages/website/public/schemas/paseo.config.v1.json +++ b/packages/website/public/schemas/paseo.config.v1.json @@ -95,9 +95,6 @@ "modelsDir": { "type": "string", "minLength": 1 - }, - "autoDownload": { - "type": "boolean" } }, "additionalProperties": false @@ -197,6 +194,9 @@ "dictation": { "type": "object", "properties": { + "enabled": { + "type": "boolean" + }, "stt": { "type": "object", "properties": { @@ -230,6 +230,9 @@ "voiceMode": { "type": "object", "properties": { + "enabled": { + "type": "boolean" + }, "llm": { "type": "object", "properties": { diff --git a/packages/website/src/routes/docs/configuration.tsx b/packages/website/src/routes/docs/configuration.tsx index d22f5b14a..dd5276134 100644 --- a/packages/website/src/routes/docs/configuration.tsx +++ b/packages/website/src/routes/docs/configuration.tsx @@ -177,7 +177,7 @@ docker run --rm -i \\
  • OPENAI_API_KEY — override OpenAI provider key
  • PASEO_VOICE_LLM_PROVIDER — override voice LLM provider (claude, codex, opencode)
  • PASEO_DICTATION_STT_PROVIDER, PASEO_VOICE_STT_PROVIDER, PASEO_VOICE_TTS_PROVIDER — override voice provider selection (local or openai)
  • -
  • PASEO_LOCAL_MODELS_DIR and PASEO_LOCAL_AUTO_DOWNLOAD — control local model directory and download behavior
  • +
  • PASEO_LOCAL_MODELS_DIR — control local model directory
  • PASEO_DICTATION_LOCAL_STT_MODEL — override local dictation STT model
  • PASEO_VOICE_LOCAL_STT_MODEL, PASEO_VOICE_LOCAL_TTS_MODEL — override local voice STT/TTS models
  • PASEO_VOICE_LOCAL_TTS_SPEAKER_ID, PASEO_VOICE_LOCAL_TTS_SPEED — optional local voice TTS tuning
  • diff --git a/packages/website/src/routes/docs/voice.tsx b/packages/website/src/routes/docs/voice.tsx index 4123df255..024b51f60 100644 --- a/packages/website/src/routes/docs/voice.tsx +++ b/packages/website/src/routes/docs/voice.tsx @@ -55,7 +55,7 @@ function VoiceDocs() {

    Missing models are downloaded at daemon startup into{' '} - $PASEO_HOME/models/local-speech when auto-download is enabled. + $PASEO_HOME/models/local-speech. Downloads happen only for missing files.

    @@ -71,8 +71,7 @@ function VoiceDocs() {
       },
       "providers": {
         "local": {
    -      "modelsDir": "~/.paseo/models/local-speech",
    -      "autoDownload": true
    +      "modelsDir": "~/.paseo/models/local-speech"
         }
       }
     }`}
    @@ -107,7 +106,7 @@ function VoiceDocs() {
             
    • OPENAI_API_KEY — OpenAI speech credentials
    • PASEO_VOICE_LLM_PROVIDER — voice agent provider override
    • -
    • PASEO_LOCAL_MODELS_DIR, PASEO_LOCAL_AUTO_DOWNLOAD — local model storage and download policy
    • +
    • PASEO_LOCAL_MODELS_DIR — local model storage directory
    • PASEO_DICTATION_LOCAL_STT_MODEL — local dictation STT model ID
    • PASEO_VOICE_LOCAL_STT_MODEL, PASEO_VOICE_LOCAL_TTS_MODEL — local voice STT/TTS model IDs
    • PASEO_VOICE_LOCAL_TTS_SPEAKER_ID, PASEO_VOICE_LOCAL_TTS_SPEED — optional local voice TTS tuning