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