import { createLogger } from "../../utils/logger/index.js";
import { init_logger } from "../../utils/logger.js";
import { ProviderHttpError } from "../../media-shared/http/provider-http-errors.js";
import { fetchWithTimeoutGuarded, normalizeBaseUrl, postJsonRequest } from "../../media-shared/http/provider-http.js";
import "../../media-shared/http/index.js";
//#region src/voice/tts/openai-compatible-speech.ts
/**
* Factory: createOpenAiCompatibleSpeechProvider
*
* Most TTS providers (OpenAI, MiniMax, ElevenLabs, OpenRouter, ...) speak the
* OpenAI `/audio/speech` POST shape. This factory eliminates the boilerplate so
* a new compatible vendor is ~30 lines of declaration instead of a full provider
* implementation.
*
* Ported from openclaw/src/tts/openai-compatible-speech-provider.ts (commit
* baseline 2026-05-08), with the following INTENTIONAL OMISSIONS per
* docs/voice-rearchitecture.md §15.3:
*   - resolveTalkConfig / resolveTalkOverrides (talk-mode is excluded)
*   - Persona resolution context (persona system is excluded)
*   - Custom dispatcherPolicy (xopc uses Node 22 native fetch — see ssrf-guard.ts)
*
* DECISION: We use `Record<string, unknown>` for the extra-config slot rather
* than openclaw's generic `<ExtraConfig extends Record<string, unknown>>`. The
* generic adds significant TypeScript complexity but no runtime safety; we
* declare per-provider config types in each provider file's own normalizer.
*
* DECISION: `voiceCompatibleResponseFormats` defaults to `['opus']` if omitted,
* because Telegram voice notes require opus/ogg. Providers that natively support
* opus output (OpenAI, ElevenLabs) can rely on this; others (MiniMax mp3-only)
* pass an empty array and downstream ffmpeg-compresses to opus.
*/
init_logger();
const log = createLogger("SpeechProvider:OpenAiCompat");
function asObject(value) {
	return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
}
function trimToUndefined(value) {
	if (typeof value !== "string") return;
	const trimmed = value.trim();
	return trimmed.length > 0 ? trimmed : void 0;
}
function asFiniteNumber(value) {
	if (typeof value === "number" && Number.isFinite(value)) return value;
	if (typeof value === "string") {
		const parsed = Number.parseFloat(value);
		if (Number.isFinite(parsed)) return parsed;
	}
}
function compactKey(value) {
	return value.replace(/[^a-z0-9]+/giu, "").toLowerCase();
}
function normalizeResponseFormat(label, formats, value) {
	const next = trimToUndefined(value)?.toLowerCase();
	if (!next) return;
	if (formats.includes(next)) return next;
	throw new Error(`Invalid ${label} speech responseFormat: ${next}`);
}
function buildExtraJsonBodyFields(config, fields) {
	const body = {};
	for (const field of fields ?? []) {
		const value = config[field.configKey];
		if (value !== void 0 && value !== null && value !== "") body[field.requestKey ?? field.configKey] = value;
	}
	return body;
}
function readSpeechOverrides(overrides) {
	if (!overrides) return {};
	return {
		model: trimToUndefined(overrides.model ?? overrides.modelId),
		voice: trimToUndefined(overrides.voice ?? overrides.voiceId),
		speed: asFiniteNumber(overrides.speed)
	};
}
function parseDirectiveTokenInternal(ctx, providerConfigKey) {
	const compactProviderKey = compactKey(providerConfigKey);
	switch (ctx.key) {
		case "voice":
		case "voice_id":
		case "voiceid":
		case `${providerConfigKey}_voice`:
		case `${compactProviderKey}voice`:
			if (!ctx.policy.allowVoice) return { handled: true };
			return {
				handled: true,
				overrides: { voice: ctx.value }
			};
		case "model":
		case "model_id":
		case "modelid":
		case `${providerConfigKey}_model`:
		case `${compactProviderKey}model`:
			if (!ctx.policy.allowModelId) return { handled: true };
			return {
				handled: true,
				overrides: { model: ctx.value }
			};
		case "speed": {
			const speed = asFiniteNumber(ctx.value);
			if (speed === void 0) return {
				handled: true,
				warnings: [`Invalid speed value: ${ctx.value}`]
			};
			return {
				handled: true,
				overrides: { speed }
			};
		}
		default: return { handled: false };
	}
}
function createOpenAiCompatibleSpeechProvider(options) {
	const providerConfigKey = options.id;
	const voiceCompatibleResponseFormats = options.voiceCompatibleResponseFormats ?? ["opus"];
	function readExtraConfig(raw) {
		return options.readExtraConfig?.(raw) ?? {};
	}
	function normalizeConfig(rawConfig) {
		const raw = asObject(rawConfig[providerConfigKey]) ?? rawConfig;
		return {
			apiKey: trimToUndefined(raw.apiKey),
			baseUrl: trimToUndefined(raw.baseUrl) ? normalizeBaseUrl(String(raw.baseUrl)) : void 0,
			model: trimToUndefined(raw.model ?? raw.modelId) ?? options.defaultModel,
			voice: trimToUndefined(raw.voice ?? raw.voiceId) ?? options.defaultVoice,
			speed: asFiniteNumber(raw.speed),
			responseFormat: normalizeResponseFormat(options.label, options.responseFormats, raw.responseFormat),
			...readExtraConfig(raw)
		};
	}
	function readProviderConfig(config) {
		return {
			apiKey: trimToUndefined(config.apiKey),
			baseUrl: trimToUndefined(config.baseUrl) ? normalizeBaseUrl(String(config.baseUrl)) : void 0,
			model: trimToUndefined(config.model ?? config.modelId) ?? options.defaultModel,
			voice: trimToUndefined(config.voice ?? config.voiceId) ?? options.defaultVoice,
			speed: asFiniteNumber(config.speed),
			responseFormat: normalizeResponseFormat(options.label, options.responseFormats, config.responseFormat) ?? options.defaultResponseFormat,
			...readExtraConfig(config)
		};
	}
	function resolveApiKey(providerConfig) {
		return providerConfig.apiKey ?? trimToUndefined(process.env[options.envKey]);
	}
	function resolveBaseUrl(providerConfig) {
		return providerConfig.baseUrl ?? options.defaultBaseUrl.replace(/\/+$/u, "");
	}
	return {
		id: options.id,
		aliases: options.aliases,
		autoSelectOrder: options.autoSelectOrder,
		resolveConfig: (ctx) => normalizeConfig(ctx.rawConfig),
		isConfigured: (ctx) => Boolean(resolveApiKey(readProviderConfig(ctx.providerConfig))),
		parseDirectiveToken: (ctx) => parseDirectiveTokenInternal(ctx, providerConfigKey),
		listVoices: async () => options.voices.map((voice) => ({
			id: voice,
			name: voice
		})),
		synthesize: async (req) => {
			const config = readProviderConfig(req.providerConfig);
			const overrides = readSpeechOverrides(req.providerOverrides);
			const apiKey = resolveApiKey(config);
			if (!apiKey) throw new Error(options.missingApiKeyError ?? `${options.label} API key missing`);
			const baseUrl = resolveBaseUrl(config);
			const responseFormat = config.responseFormat ?? options.defaultResponseFormat;
			const speed = overrides.speed ?? config.speed;
			const model = overrides.model ?? config.model;
			const voice = overrides.voice ?? config.voice;
			const url = `${baseUrl}/audio/speech`;
			log.debug({
				provider: options.id,
				model,
				voice,
				responseFormat,
				baseUrl,
				target: req.target
			}, `Calling ${options.label} TTS`);
			const response = await postJsonRequest(url, {
				timeoutMs: req.timeoutMs,
				label: options.apiErrorLabel ?? `${options.label} TTS API error`,
				headers: {
					Authorization: `Bearer ${apiKey}`,
					...options.extraHeaders
				},
				body: {
					model,
					input: req.text,
					voice,
					response_format: responseFormat,
					...speed === void 0 ? {} : { speed },
					...buildExtraJsonBodyFields(config, options.extraJsonBodyFields)
				}
			});
			return {
				audioBuffer: Buffer.from(await response.arrayBuffer()),
				outputFormat: responseFormat,
				fileExtension: responseFormat,
				voiceCompatible: voiceCompatibleResponseFormats.includes(responseFormat)
			};
		},
		/**
		* Streaming via OpenAI's POST /audio/speech (the response body itself is
		* the audio stream — no SSE / WebSocket needed). We do NOT call assertOk
		* via postJsonRequest because we want to surface the body as-is for
		* streaming consumers; instead we manually check status and convert to
		* ProviderHttpError on failure (matches openclaw behavior).
		*/
		synthesizeStream: async (req) => {
			const config = readProviderConfig(req.providerConfig);
			const overrides = readSpeechOverrides(req.providerOverrides);
			const apiKey = resolveApiKey(config);
			if (!apiKey) throw new Error(options.missingApiKeyError ?? `${options.label} API key missing`);
			const baseUrl = resolveBaseUrl(config);
			const responseFormat = config.responseFormat ?? options.defaultResponseFormat;
			const speed = overrides.speed ?? config.speed;
			const model = overrides.model ?? config.model;
			const voice = overrides.voice ?? config.voice;
			const response = await fetchWithTimeoutGuarded(`${baseUrl}/audio/speech`, {
				timeoutMs: req.timeoutMs,
				label: options.apiErrorLabel ?? `${options.label} TTS streaming`,
				init: {
					method: "POST",
					headers: {
						Authorization: `Bearer ${apiKey}`,
						"content-type": "application/json",
						accept: "application/octet-stream",
						...options.extraHeaders
					},
					body: JSON.stringify({
						model,
						input: req.text,
						voice,
						response_format: responseFormat,
						stream: true,
						...speed === void 0 ? {} : { speed },
						...buildExtraJsonBodyFields(config, options.extraJsonBodyFields)
					})
				}
			});
			if (!response.ok) {
				const detail = await response.text().catch(() => void 0);
				throw new ProviderHttpError({
					label: options.apiErrorLabel ?? `${options.label} TTS streaming`,
					status: response.status,
					detail: detail?.slice(0, 220)
				});
			}
			if (!response.body) throw new Error(`${options.label} TTS streaming: empty response body`);
			return {
				audioStream: response.body,
				outputFormat: responseFormat,
				fileExtension: responseFormat,
				voiceCompatible: voiceCompatibleResponseFormats.includes(responseFormat)
			};
		}
	};
}
//#endregion
export { createOpenAiCompatibleSpeechProvider };

//# sourceMappingURL=openai-compatible-speech.js.map