From 4da0a99a9eb520a43391fe3a33880cdb5f894c69 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 20 Apr 2026 14:50:58 +0100 Subject: [PATCH] refactor: share speech provider helpers --- src/plugin-sdk/speech.ts | 67 ++++---------------------------- src/tts/tts-core.ts | 68 ++++----------------------------- src/tts/tts-provider-helpers.ts | 57 +++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 120 deletions(-) create mode 100644 src/tts/tts-provider-helpers.ts diff --git a/src/plugin-sdk/speech.ts b/src/plugin-sdk/speech.ts index 3115da936f1..4f3ea1a64f6 100644 --- a/src/plugin-sdk/speech.ts +++ b/src/plugin-sdk/speech.ts @@ -1,6 +1,3 @@ -import { rmSync } from "node:fs"; -import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; - // Public speech helpers for bundled or third-party plugins. // // Keep this surface provider-facing: types, validation, directive parsing, and @@ -42,60 +39,10 @@ export { trimToUndefined, truncateErrorDetail, } from "../tts/provider-error-utils.js"; - -const TEMP_FILE_CLEANUP_DELAY_MS = 5 * 60 * 1000; // 5 minutes - -export function requireInRange(value: number, min: number, max: number, label: string): void { - if (!Number.isFinite(value) || value < min || value > max) { - throw new Error(`${label} must be between ${min} and ${max}`); - } -} - -export function normalizeLanguageCode(code?: string): string | undefined { - const trimmed = code?.trim(); - if (!trimmed) { - return undefined; - } - const normalized = normalizeLowercaseStringOrEmpty(trimmed); - if (!/^[a-z]{2}$/.test(normalized)) { - throw new Error("languageCode must be a 2-letter ISO 639-1 code (e.g. en, de, fr)"); - } - return normalized; -} - -export function normalizeApplyTextNormalization(mode?: string): "auto" | "on" | "off" | undefined { - const trimmed = mode?.trim(); - if (!trimmed) { - return undefined; - } - const normalized = normalizeLowercaseStringOrEmpty(trimmed); - if (normalized === "auto" || normalized === "on" || normalized === "off") { - return normalized; - } - throw new Error("applyTextNormalization must be one of: auto, on, off"); -} - -export function normalizeSeed(seed?: number): number | undefined { - if (seed == null) { - return undefined; - } - const next = Math.floor(seed); - if (!Number.isFinite(next) || next < 0 || next > 4_294_967_295) { - throw new Error("seed must be between 0 and 4294967295"); - } - return next; -} - -export function scheduleCleanup( - tempDir: string, - delayMs: number = TEMP_FILE_CLEANUP_DELAY_MS, -): void { - const timer = setTimeout(() => { - try { - rmSync(tempDir, { recursive: true, force: true }); - } catch { - // ignore cleanup errors - } - }, delayMs); - timer.unref(); -} +export { + normalizeApplyTextNormalization, + normalizeLanguageCode, + normalizeSeed, + requireInRange, + scheduleCleanup, +} from "../tts/tts-provider-helpers.js"; diff --git a/src/tts/tts-core.ts b/src/tts/tts-core.ts index 7101bf3adbb..405cfd4e230 100644 --- a/src/tts/tts-core.ts +++ b/src/tts/tts-core.ts @@ -1,4 +1,3 @@ -import { rmSync } from "node:fs"; import { completeSimple, type TextContent } from "@mariozechner/pi-ai"; import { getApiKeyForModel, requireApiKey } from "../agents/model-auth.js"; import { @@ -10,13 +9,15 @@ import { import { resolveModelAsync } from "../agents/pi-embedded-runner/model.js"; import { prepareModelForSimpleCompletion } from "../agents/simple-completion-transport.js"; import type { OpenClawConfig } from "../config/types.js"; -import { - normalizeOptionalLowercaseString, - normalizeOptionalString, -} from "../shared/string-coerce.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import type { ResolvedTtsConfig } from "./tts-types.js"; - -const TEMP_FILE_CLEANUP_DELAY_MS = 5 * 60 * 1000; // 5 minutes +export { + normalizeApplyTextNormalization, + normalizeLanguageCode, + normalizeSeed, + requireInRange, + scheduleCleanup, +} from "./tts-provider-helpers.js"; type SummarizeTextDeps = { completeSimple: typeof completeSimple; @@ -36,45 +37,6 @@ function resolveDefaultSummarizeTextDeps(): SummarizeTextDeps { }; } -export function requireInRange(value: number, min: number, max: number, label: string): void { - if (!Number.isFinite(value) || value < min || value > max) { - throw new Error(`${label} must be between ${min} and ${max}`); - } -} - -export function normalizeLanguageCode(code?: string): string | undefined { - const normalized = normalizeOptionalLowercaseString(code); - if (!normalized) { - return undefined; - } - if (!/^[a-z]{2}$/.test(normalized)) { - throw new Error("languageCode must be a 2-letter ISO 639-1 code (e.g. en, de, fr)"); - } - return normalized; -} - -export function normalizeApplyTextNormalization(mode?: string): "auto" | "on" | "off" | undefined { - const normalized = normalizeOptionalLowercaseString(mode); - if (!normalized) { - return undefined; - } - if (normalized === "auto" || normalized === "on" || normalized === "off") { - return normalized; - } - throw new Error("applyTextNormalization must be one of: auto, on, off"); -} - -export function normalizeSeed(seed?: number): number | undefined { - if (seed == null) { - return undefined; - } - const next = Math.floor(seed); - if (!Number.isFinite(next) || next < 0 || next > 4_294_967_295) { - throw new Error("seed must be between 0 and 4294967295"); - } - return next; -} - type SummarizeResult = { summary: string; latencyMs: number; @@ -195,17 +157,3 @@ export async function summarizeText( throw err; } } - -export function scheduleCleanup( - tempDir: string, - delayMs: number = TEMP_FILE_CLEANUP_DELAY_MS, -): void { - const timer = setTimeout(() => { - try { - rmSync(tempDir, { recursive: true, force: true }); - } catch { - // ignore cleanup errors - } - }, delayMs); - timer.unref(); -} diff --git a/src/tts/tts-provider-helpers.ts b/src/tts/tts-provider-helpers.ts new file mode 100644 index 00000000000..3404e4b6b36 --- /dev/null +++ b/src/tts/tts-provider-helpers.ts @@ -0,0 +1,57 @@ +import { rmSync } from "node:fs"; +import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js"; + +const TEMP_FILE_CLEANUP_DELAY_MS = 5 * 60 * 1000; // 5 minutes + +export function requireInRange(value: number, min: number, max: number, label: string): void { + if (!Number.isFinite(value) || value < min || value > max) { + throw new Error(`${label} must be between ${min} and ${max}`); + } +} + +export function normalizeLanguageCode(code?: string): string | undefined { + const normalized = normalizeOptionalLowercaseString(code); + if (!normalized) { + return undefined; + } + if (!/^[a-z]{2}$/.test(normalized)) { + throw new Error("languageCode must be a 2-letter ISO 639-1 code (e.g. en, de, fr)"); + } + return normalized; +} + +export function normalizeApplyTextNormalization(mode?: string): "auto" | "on" | "off" | undefined { + const normalized = normalizeOptionalLowercaseString(mode); + if (!normalized) { + return undefined; + } + if (normalized === "auto" || normalized === "on" || normalized === "off") { + return normalized; + } + throw new Error("applyTextNormalization must be one of: auto, on, off"); +} + +export function normalizeSeed(seed?: number): number | undefined { + if (seed == null) { + return undefined; + } + const next = Math.floor(seed); + if (!Number.isFinite(next) || next < 0 || next > 4_294_967_295) { + throw new Error("seed must be between 0 and 4294967295"); + } + return next; +} + +export function scheduleCleanup( + tempDir: string, + delayMs: number = TEMP_FILE_CLEANUP_DELAY_MS, +): void { + const timer = setTimeout(() => { + try { + rmSync(tempDir, { recursive: true, force: true }); + } catch { + // ignore cleanup errors + } + }, delayMs); + timer.unref(); +}