/** * SpeechProviderPlugin contract — the unified TTS provider interface for xopc v2. * * Ported from openclaw/src/tts/provider-types.ts (commit baseline 2026-05-08), with * the following INTENTIONAL OMISSIONS per docs/voice-rearchitecture.md §2.3 / §15.3: * - Telephony surface (synthesizeTelephony / SpeechTelephonySynthesisRequest) * - Persona system (ResolvedTtsPersona / persona resolution context) * - Talk-mode resolution (resolveTalkConfig / resolveTalkOverrides) * * DECISION: configType uses `Record` rather than a generic * because xopc's config schema is dynamic per-provider and providers handle * their own zod validation in `resolveConfig`. This matches openclaw's runtime * behavior (they also fall through to `Record` at the seam). */ import type { Config } from '../../config/schema.js'; /** Provider id, e.g. "openai", "alibaba", "edge", "minimax", "elevenlabs". */ export type SpeechProviderId = string; /** Where the synthesized audio is going. xopc currently only emits "voice-note" + "audio-file". */ export type SpeechSynthesisTarget = 'audio-file' | 'voice-note'; /** Opaque per-provider configuration after `resolveConfig` has normalized it. */ export type SpeechProviderConfig = Record; /** Per-call overrides parsed from `[[tts:xxx=yyy]]` directives. */ export type SpeechProviderOverrides = Record; /** Whether the model can override TTS parameters via directives, and at what scope. */ export interface SpeechModelOverridePolicy { enabled: boolean; allowText: boolean; allowProvider: boolean; allowVoice: boolean; allowModelId: boolean; allowVoiceSettings: boolean; allowNormalization: boolean; allowSeed: boolean; } /** TTS directive token parse context handed to `parseDirectiveToken`. */ export interface SpeechDirectiveTokenParseContext { /** Lowercased directive key (e.g. "voice", "speed", "elevenlabs_voice"). */ key: string; /** Raw value from `[[tts:key=value]]`. */ value: string; /** Active model-override policy (provider must respect this). */ policy: SpeechModelOverridePolicy; /** Provider id selected for this call (after directive `provider=` resolution). */ selectedProvider?: SpeechProviderId; /** Provider's resolved config for this call (read-only here). */ providerConfig?: SpeechProviderConfig; /** Overrides accumulated from previous tokens in the same directive block. */ currentOverrides?: SpeechProviderOverrides; } /** Result of `parseDirectiveToken` — provider declares whether it handled the token. */ export interface SpeechDirectiveTokenParseResult { /** True when this provider claimed the token (orchestrator stops trying others). */ handled: boolean; /** Patch to merge into accumulated overrides (e.g. `{ voice: 'alloy' }`). */ overrides?: SpeechProviderOverrides; /** Non-fatal warnings (e.g. "voice override ignored: not in allowlist"). */ warnings?: string[]; } /** Audio output of a one-shot `synthesize` call. */ export interface SpeechSynthesisResult { audioBuffer: Buffer; /** Container/codec id, e.g. "mp3", "opus", "wav". */ outputFormat: string; /** File extension WITHOUT the leading dot, e.g. "mp3". */ fileExtension: string; /** * True when the buffer is directly playable as a Telegram voice note (opus/ogg) * or comparable. False = downstream may need ffmpeg compression. */ voiceCompatible: boolean; } /** Streaming variant — chunks arrive as the provider produces them. */ export interface SpeechSynthesisStreamResult { audioStream: ReadableStream; outputFormat: string; fileExtension: string; voiceCompatible: boolean; /** Optional cleanup hook (close sockets, free temp files). Called once stream ends. */ release?: () => Promise; } export interface SpeechSynthesisRequest { text: string; cfg: Config; providerConfig: SpeechProviderConfig; target: SpeechSynthesisTarget; providerOverrides?: SpeechProviderOverrides; /** Hard timeout from the orchestrator (already merged with provider defaults). */ timeoutMs: number; } export type SpeechSynthesisStreamRequest = SpeechSynthesisRequest; /** Voice catalog entry returned by `listVoices`. */ export interface SpeechVoiceOption { id: string; name?: string; category?: string; description?: string; locale?: string; gender?: string; /** Provider-defined personality / style tags. */ personalities?: string[]; } export interface SpeechListVoicesRequest { cfg?: Config; providerConfig?: SpeechProviderConfig; apiKey?: string; baseUrl?: string; } export interface SpeechProviderResolveConfigContext { cfg: Config; /** Raw user-supplied config slice (e.g. `cfg.messages?.tts?.providers?.openai`). */ rawConfig: Record; timeoutMs: number; } export interface SpeechProviderConfiguredContext { cfg?: Config; providerConfig: SpeechProviderConfig; timeoutMs: number; } /** * The provider plugin interface. Every speech provider (whether bundled in xopc or * shipped from a user extension) implements this surface. * * DECISION: All methods except `id` and `resolveConfig` are optional. A provider * that only exposes `synthesize` is valid; orchestrator skips capabilities the * provider doesn't implement (no stub methods, no NotImplemented throws). */ export interface SpeechProviderPlugin { /** Canonical provider id. Must be unique within the registry. */ id: SpeechProviderId; /** * Optional aliases for provider lookup. Aliases participate in lookup but * `id` is the canonical persisted name. */ aliases?: readonly string[]; /** * Lower number = higher priority when auto-selecting providers (fallback disabled * or empty order). Matches OpenClaw `autoSelectOrder`. */ autoSelectOrder?: number; /** * Normalize raw config → SpeechProviderConfig. Throws on validation error. * The returned object is opaque to the orchestrator and re-passed back into * `synthesize`, `synthesizeStream`, etc. via `request.providerConfig`. */ resolveConfig(ctx: SpeechProviderResolveConfigContext): SpeechProviderConfig; /** Whether the provider can run (api key present, base url valid, etc.). */ isConfigured(ctx: SpeechProviderConfiguredContext): boolean; /** One-shot synthesis. Required for any provider that wants to be callable. */ synthesize?(req: SpeechSynthesisRequest): Promise; /** * Streaming synthesis. When omitted, the orchestrator falls back to `synthesize` * + `wrapBufferAsStream` (single-chunk stream). See speak-core §8.1. */ synthesizeStream?(req: SpeechSynthesisStreamRequest): Promise; /** Voice discovery for Web UI / CLI. Optional; orchestrator returns [] when missing. */ listVoices?(req: SpeechListVoicesRequest): Promise; /** * Per-token directive parsing. Receives one `key=value` at a time. Provider * declares ownership via `handled: true`. Unhandled tokens fall through to * the next provider in the directive resolution chain (or are dropped). */ parseDirectiveToken?(ctx: SpeechDirectiveTokenParseContext): SpeechDirectiveTokenParseResult; }