import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.js";
import { a as asOptionalRecord, s as filterStringRecord } from "./record-coerce-DItp3I4t.js";
import { i as writeExternalFileWithinRoot } from "./fs-safe-gVoExnQ2.js";
import { i as readRegularFileSync } from "./regular-file-Dwz6p59y.js";
import { n as resolvePreferredOpenClawTmpDir } from "./tmp-openclaw-dir-DnyL0lW9.js";
import { t as createSubsystemLogger } from "./subsystem-CT-8ly4J.js";
import { r as withTempWorkspace } from "./private-temp-workspace-CFXeleDH.js";
import { t as runCommandBuffered } from "./exec-B6wf9wmM.js";
import { o as runFfmpeg } from "./media-probe-glecfkRa.js";
import "./temp-path-CmnkjO5z.js";
import "./runtime-env-C1O5oVvq.js";
import "./string-coerce-runtime-C8jKEm3h.js";
import "./media-runtime-ANMyxQCF.js";
import "./security-runtime--v54nkpJ.js";
import "./process-runtime-ITj9s6JA.js";
import "./text-utility-runtime-Cyi5sIYn.js";
import { readdirSync } from "node:fs";
import path from "node:path";
//#region extensions/tts-local-cli/speech-provider.ts
const log = createSubsystemLogger("tts-local-cli");
const VALID_OUTPUT_FORMATS = [
	"mp3",
	"opus",
	"wav"
];
const AUDIO_EXTENSIONS = /* @__PURE__ */ new Set([
	".wav",
	".mp3",
	".opus",
	".ogg",
	".m4a"
]);
const DEFAULT_TIMEOUT_MS = 12e4;
const MAX_AUDIO_OUTPUT_BYTES = 50 * 1024 * 1024;
const MAX_CLI_STDERR_BYTES = 1024 * 1024;
function asStringArray(value) {
	return Array.isArray(value) && value.every((v) => typeof v === "string") ? value : void 0;
}
function normalizeOutputFormat(value) {
	if (typeof value !== "string") return "mp3";
	const lower = value.toLowerCase().trim();
	if (VALID_OUTPUT_FORMATS.includes(lower)) return lower;
	return "mp3";
}
function resolveCliProviderConfig(rawConfig) {
	const providers = asOptionalRecord(rawConfig.providers);
	return asOptionalRecord(providers?.["tts-local-cli"]) ?? asOptionalRecord(providers?.cli) ?? {};
}
function getConfig(cfg) {
	const command = typeof cfg.command === "string" ? cfg.command.trim() : "";
	if (!command) return null;
	return {
		command,
		args: asStringArray(cfg.args) ?? [],
		outputFormat: normalizeOutputFormat(cfg.outputFormat),
		timeoutMs: typeof cfg.timeoutMs === "number" ? cfg.timeoutMs : DEFAULT_TIMEOUT_MS,
		cwd: typeof cfg.cwd === "string" ? cfg.cwd : void 0,
		env: filterStringRecord(cfg.env)
	};
}
function stripEmojis(text) {
	return text.replace(/[\p{Emoji_Presentation}\p{Extended_Pictographic}]/gu, " ").replace(/\s+/g, " ").trim();
}
function applyTemplate(str, ctx) {
	return str.replace(/{{\s*(\w+)\s*}}/gi, (_, key) => {
		return ctx[key.charAt(0).toUpperCase() + key.slice(1).toLowerCase()] ?? ctx[key] ?? "";
	});
}
function parseCommand(cmdStr) {
	const parts = [];
	let current = "";
	let inQuote = false;
	let quoteChar = "";
	for (const char of cmdStr.trim()) if (inQuote) if (char === quoteChar) inQuote = false;
	else current += char;
	else if (char === "\"" || char === "'") {
		inQuote = true;
		quoteChar = char;
	} else if (char === " " || char === "	") {
		if (current) {
			parts.push(current);
			current = "";
		}
	} else current += char;
	if (current) parts.push(current);
	return {
		cmd: parts[0] || "",
		initialArgs: parts.slice(1)
	};
}
function findAudioFile(dir, baseName) {
	const files = readdirSync(dir);
	for (const file of files) {
		const ext = path.extname(file).toLowerCase();
		if (AUDIO_EXTENSIONS.has(ext) && (file.startsWith(baseName) || file.includes(baseName))) return path.join(dir, file);
	}
	for (const file of files) {
		const ext = path.extname(file).toLowerCase();
		if (AUDIO_EXTENSIONS.has(ext)) return path.join(dir, file);
	}
	return null;
}
function detectFormatFromExtension(filePath) {
	return path.extname(filePath).toLowerCase() === ".m4a" ? "m4a" : null;
}
function hasMpegFrameHeader(buffer, offset) {
	const mpegHeader = buffer[offset + 1] ?? 0;
	const mpegFormat = buffer[offset + 2] ?? 0;
	return buffer.length >= offset + 4 && buffer[offset] === 255 && (mpegHeader & 224) === 224 && (mpegHeader & 24) !== 8 && (mpegHeader & 6) !== 0 && (mpegFormat & 240) !== 240 && (mpegFormat & 12) !== 12;
}
function hasId3v2MpegFrame(buffer) {
	if (buffer.length < 10) return false;
	const majorVersion = buffer[3] ?? 0;
	const revision = buffer[4] ?? 0;
	const flags = buffer[5] ?? 0;
	if (majorVersion < 2 || majorVersion > 4 || revision === 255) return false;
	if ((flags & (255 ^ (majorVersion === 2 ? 192 : majorVersion === 3 ? 224 : 240))) !== 0) return false;
	const size0 = buffer[6] ?? 0;
	const size1 = buffer[7] ?? 0;
	const size2 = buffer[8] ?? 0;
	const size3 = buffer[9] ?? 0;
	if ((size0 | size1 | size2 | size3) & 128) return false;
	const tagSize = size0 << 21 | size1 << 14 | size2 << 7 | size3;
	const footerSize = majorVersion === 4 && (flags & 16) !== 0 ? 10 : 0;
	const audioOffset = 10 + tagSize + footerSize;
	return audioOffset < buffer.length && hasMpegFrameHeader(buffer, audioOffset);
}
function detectAudioFormat(buffer) {
	const prefix = buffer.toString("ascii", 0, 12);
	if (prefix.startsWith("RIFF") && prefix.slice(8, 12) === "WAVE") return "wav";
	if (hasMpegFrameHeader(buffer, 0) || prefix.startsWith("ID3") && hasId3v2MpegFrame(buffer)) return "mp3";
	return prefix.startsWith("OggS") ? "ogg" : null;
}
function getFileExt(format) {
	if (format === "opus") return ".opus";
	if (format === "ogg") return ".ogg";
	if (format === "m4a") return ".m4a";
	if (format === "wav") return ".wav";
	return ".mp3";
}
function readAudioFile(filePath) {
	return readRegularFileSync({
		filePath,
		maxBytes: MAX_AUDIO_OUTPUT_BYTES
	}).buffer;
}
async function runCli(params) {
	const cleanText = stripEmojis(params.text);
	if (!cleanText) throw new Error("CLI TTS: text is empty after removing emojis");
	const outputExt = getFileExt(params.config.outputFormat);
	const ctx = {
		Text: cleanText,
		OutputPath: path.join(params.outputDir, `${params.filePrefix}${outputExt}`),
		OutputDir: params.outputDir,
		OutputBase: params.filePrefix
	};
	const { cmd, initialArgs } = parseCommand(params.config.command);
	if (!cmd) throw new Error("CLI TTS: invalid command");
	const baseArgs = [...initialArgs, ...params.config.args];
	const args = baseArgs.map((a) => applyTemplate(a, ctx));
	const input = baseArgs.some((a) => /{{\s*text\s*}}/i.test(a)) ? "" : cleanText;
	const result = await runCommandBuffered([cmd, ...args], {
		cwd: params.config.cwd,
		env: params.config.env,
		input,
		maxOutputBytes: {
			stdout: MAX_AUDIO_OUTPUT_BYTES,
			stderr: MAX_CLI_STDERR_BYTES
		},
		timeoutMs: params.config.timeoutMs
	});
	if (result.termination === "timeout") throw new Error(`CLI TTS timed out after ${params.config.timeoutMs}ms`);
	if (result.termination === "output-limit") {
		const stream = result.outputLimitStream ?? "stdout";
		throw new Error(`CLI TTS ${stream} exceeded ${stream === "stderr" ? MAX_CLI_STDERR_BYTES : MAX_AUDIO_OUTPUT_BYTES} bytes`);
	}
	if (result.code !== null && result.code !== 0) throw new Error(`CLI TTS exit ${result.code}: ${result.stderr.toString("utf8")}`);
	if (result.termination !== "exit" && result.termination !== "error") throw new Error(`CLI TTS failed: ${result.error?.message ?? result.termination}`);
	if (result.termination === "error" && result.code !== 0) throw new Error(`CLI TTS failed: ${result.error?.message ?? result.termination}`);
	const audioFile = findAudioFile(params.outputDir, params.filePrefix);
	if (audioFile) {
		const buffer = readAudioFile(audioFile);
		const format = detectAudioFormat(buffer) ?? detectFormatFromExtension(audioFile);
		if (!format) throw new Error(`CLI TTS: unknown format for ${audioFile}`);
		return {
			buffer,
			actualFormat: format,
			audioPath: audioFile
		};
	}
	if (result.termination === "error" && result.errorStream !== "stderr") throw new Error(`CLI TTS failed: ${result.error?.message ?? result.termination}`);
	const stdout = result.stdout;
	if (stdout.length > 0) {
		const actualFormat = detectAudioFormat(stdout);
		if (!actualFormat) throw new Error("CLI TTS stdout audio format is not recognized; emit WAV, MP3, or Ogg Opus bytes, or write a supported audio file");
		return {
			buffer: stdout,
			actualFormat
		};
	}
	if (result.termination === "error") throw new Error(`CLI TTS failed: ${result.error?.message ?? result.termination}`);
	throw new Error("CLI TTS produced no output");
}
async function runFfmpegToBuffer(params) {
	const outputPath = path.join(params.outputDir, params.outputFileName);
	await writeExternalFileWithinRoot({
		rootDir: params.outputDir,
		path: params.outputFileName,
		write: async (tempPath) => {
			await runFfmpeg([...params.args, tempPath]);
		}
	});
	return readAudioFile(outputPath);
}
async function convertAudio(inputPath, outputDir, target) {
	const outputFileName = `converted${getFileExt(target)}`;
	const args = [
		"-y",
		"-i",
		inputPath
	];
	if (target === "opus") args.push("-c:a", "libopus", "-b:a", "64k", "-f", "opus");
	else if (target === "wav") args.push("-c:a", "pcm_s16le", "-f", "wav");
	else args.push("-c:a", "libmp3lame", "-b:a", "128k", "-f", "mp3");
	return await runFfmpegToBuffer({
		args,
		outputDir,
		outputFileName
	});
}
async function convertToRawPcm(inputPath, outputDir) {
	return await runFfmpegToBuffer({
		args: [
			"-y",
			"-i",
			inputPath,
			"-c:a",
			"pcm_s16le",
			"-ar",
			"16000",
			"-ac",
			"1",
			"-f",
			"s16le"
		],
		outputDir,
		outputFileName: "telephony.pcm"
	});
}
function buildCliSpeechProvider() {
	return {
		id: "tts-local-cli",
		aliases: ["cli"],
		label: "Local CLI",
		autoSelectOrder: 1e3,
		resolveConfig(ctx) {
			return resolveCliProviderConfig(ctx.rawConfig);
		},
		isConfigured(ctx) {
			return getConfig(ctx.providerConfig) !== null;
		},
		async synthesize(req) {
			const config = getConfig(req.providerConfig);
			if (!config) throw new Error("CLI TTS not configured");
			log.debug(`synthesize: text=${truncateUtf16Safe(req.text, 50)}...`);
			return await withTempWorkspace({
				rootDir: resolvePreferredOpenClawTmpDir(),
				prefix: "openclaw-cli-tts-"
			}, async (temp) => {
				const tempDir = temp.dir;
				const result = await runCli({
					config,
					text: req.text,
					outputDir: tempDir,
					filePrefix: "speech"
				});
				log.debug(`synthesize: format=${result.actualFormat}, size=${result.buffer.length}`);
				const format = req.target === "voice-note" ? "opus" : config.outputFormat;
				let buffer = result.buffer;
				if (result.actualFormat !== format) {
					const inputName = `input${getFileExt(result.actualFormat)}`;
					const inputFile = result.audioPath ?? path.join(tempDir, inputName);
					if (!result.audioPath) await temp.write(inputName, result.buffer);
					buffer = await convertAudio(inputFile, tempDir, format);
				}
				const fileExtension = format === "opus" ? ".ogg" : `.${format}`;
				return {
					audioBuffer: buffer,
					outputFormat: format,
					fileExtension,
					voiceCompatible: req.target === "voice-note" && format === "opus"
				};
			});
		},
		async synthesizeTelephony(req) {
			const config = getConfig(req.providerConfig);
			if (!config) throw new Error("CLI TTS not configured");
			log.debug(`synthesizeTelephony: text=${truncateUtf16Safe(req.text, 50)}...`);
			return await withTempWorkspace({
				rootDir: resolvePreferredOpenClawTmpDir(),
				prefix: "openclaw-cli-tts-"
			}, async (temp) => {
				const tempDir = temp.dir;
				const result = await runCli({
					config,
					text: req.text,
					outputDir: tempDir,
					filePrefix: "telephony"
				});
				const inputFile = result.audioPath ?? path.join(tempDir, `input${getFileExt(result.actualFormat)}`);
				if (!result.audioPath) await temp.write(`input${getFileExt(result.actualFormat)}`, result.buffer);
				return {
					audioBuffer: await convertToRawPcm(inputFile, tempDir),
					outputFormat: "pcm",
					sampleRate: 16e3
				};
			});
		}
	};
}
//#endregion
export { buildCliSpeechProvider as t };
