#!/usr/bin/php
<?php

ini_set("auto_detect_line_endings", true);

if ($argc < 6 || $argc > 7) {
    echo help();
    exit(1);
}

$url = $argv[1];
$language = $argv[2];
$voice = $argv[3];
$source = $argv[4];
$outputDirectory = rtrim($argv[5], '/');
$connector = strtolower($argc === 7 ? $argv[6] : (getenv('VXMLAUDIOS_CONNECTOR') ?: 'generic'));
$lineNumber = 0;

if (!is_dir($outputDirectory) && !mkdir($outputDirectory, 0775, true)) {
    fwrite(STDERR, "Unable to create output directory: $outputDirectory\n");
    exit(1);
}

$file = fopen($source, 'r');
if ($file === false) {
    fwrite(STDERR, "Unable to open input file: $source\n");
    exit(1);
}

while (($line = fgets($file)) !== false) {
    $text = trim($line);
    if ($text === '' || $text[0] === ';') {
        continue;
    }

    $pos = strpos($text, ':');
    if ($pos !== false) {
        $reference = trim(substr($text, 0, $pos));
        $text = trim(substr($text, $pos + 1));
        $fileParts = pathinfo($reference);
        $format = isset($fileParts['extension']) ? strtolower($fileParts['extension']) : 'raw';
        $destination = $outputDirectory . '/' . $reference;
        if (!isset($fileParts['extension'])) {
            $destination .= '.' . $format;
        }
    } else {
        $format = 'raw';
        $destination = $outputDirectory . '/' . (++$lineNumber) . '.' . $format;
    }

    $parentDirectory = dirname($destination);
    if (!is_dir($parentDirectory) && !mkdir($parentDirectory, 0775, true)) {
        fwrite(STDERR, "Unable to create directory: $parentDirectory\n");
        continue;
    }

    echo "Prompt: $text\nConnector: $connector\nDestination: $destination\n";

    $ch = curl_init();
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_TIMEOUT, 120);

    switch ($connector) {
        case 'google':
        case 'google_rest':
            $apiKey = getenv('GOOGLE_TTS_API_KEY');
            $accessToken = getenv('GOOGLE_TTS_ACCESS_TOKEN');
            if (!$apiKey && !$accessToken) {
                fwrite(STDERR, "Google TTS requires GOOGLE_TTS_API_KEY or GOOGLE_TTS_ACCESS_TOKEN.\n");
                continue 2;
            }
            $requestUrl = ($url !== '' && $url !== '-')
                ? $url : 'https://texttospeech.googleapis.com/v1/text:synthesize';
            $headers = array('Content-Type: application/json; charset=utf-8');
            if ($accessToken) {
                $headers[] = 'Authorization: Bearer ' . $accessToken;
            } else {
                $requestUrl .= (strpos($requestUrl, '?') === false ? '?' : '&') .
                    'key=' . rawurlencode($apiKey);
            }
            $payload = array(
                'input' => array('text' => $text),
                'voice' => array('languageCode' => $language, 'name' => $voice),
                'audioConfig' => array('audioEncoding' => googleAudioEncoding($format)),
            );
            curl_setopt($ch, CURLOPT_URL, $requestUrl);
            curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
            curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
            break;

        case 'elevenlabs':
            $apiKey = getenv('ELEVENLABS_API_KEY');
            if (!$apiKey) {
                fwrite(STDERR, "ElevenLabs requires ELEVENLABS_API_KEY.\n");
                continue 2;
            }
            $requestUrl = ($url !== '' && $url !== '-')
                ? rtrim($url, '/') . '/' . rawurlencode($voice)
                : 'https://api.elevenlabs.io/v1/text-to-speech/' . rawurlencode($voice);
            $requestUrl .= (strpos($requestUrl, '?') === false ? '?' : '&') .
                'output_format=' . rawurlencode(elevenLabsOutputFormat($format));
            $payload = array(
                'text' => $text,
                'model_id' => getenv('ELEVENLABS_MODEL_ID') ?: 'eleven_multilingual_v2',
            );
            curl_setopt($ch, CURLOPT_URL, $requestUrl);
            curl_setopt($ch, CURLOPT_HTTPHEADER, array(
                'Content-Type: application/json',
                'Accept: audio/mpeg',
                'xi-api-key: ' . $apiKey,
            ));
            curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
            break;

        case 'microsoft':
        case 'azure':
            $apiKey = getenv('AZURE_TTS_KEY');
            $region = getenv('AZURE_TTS_REGION');
            $accessToken = getenv('AZURE_TTS_ACCESS_TOKEN');
            if (!$apiKey && !$accessToken) {
                fwrite(STDERR, "Microsoft TTS requires AZURE_TTS_KEY or AZURE_TTS_ACCESS_TOKEN.\n");
                continue 2;
            }
            if (($url === '' || $url === '-') && !$region) {
                fwrite(STDERR, "Microsoft TTS requires AZURE_TTS_REGION when using the default URL.\n");
                continue 2;
            }
            $requestUrl = ($url !== '' && $url !== '-')
                ? $url
                : 'https://' . $region . '.tts.speech.microsoft.com/cognitiveservices/v1';
            $headers = array(
                'Content-Type: application/ssml+xml',
                'X-Microsoft-OutputFormat: ' . microsoftAudioFormat($format),
                'User-Agent: Voximal-vxmlaudios',
            );
            if ($accessToken) {
                $headers[] = 'Authorization: Bearer ' . $accessToken;
            } else {
                $headers[] = 'Ocp-Apim-Subscription-Key: ' . $apiKey;
            }
            $ssml = '<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis"' .
                ' xml:lang="' . xmlEscape($language) . '"><voice name="' .
                xmlEscape($voice) . '">' . xmlEscape($text) . '</voice></speak>';
            curl_setopt($ch, CURLOPT_URL, $requestUrl);
            curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
            curl_setopt($ch, CURLOPT_POSTFIELDS, $ssml);
            break;

        case 'cartesia':
            $apiKey = getenv('CARTESIA_API_KEY');
            if (!$apiKey) {
                fwrite(STDERR, "Cartesia requires CARTESIA_API_KEY.\n");
                continue 2;
            }
            $requestUrl = ($url !== '' && $url !== '-')
                ? $url : 'https://api.cartesia.ai/tts/bytes';
            $payload = array(
                'model_id' => getenv('CARTESIA_MODEL_ID') ?: 'sonic-3.6',
                'transcript' => $text,
                'voice' => $voice,
                'output_format' => cartesiaAudioFormat($format),
                'language' => $language,
            );
            curl_setopt($ch, CURLOPT_URL, $requestUrl);
            curl_setopt($ch, CURLOPT_HTTPHEADER, array(
                'Authorization: Bearer ' . $apiKey,
                'Cartesia-Version: ' . (getenv('CARTESIA_VERSION') ?: '2026-08-14'),
                'Content-Type: application/json',
            ));
            curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
            break;

        case 'soniox':
            $apiKey = getenv('SONIOX_API_KEY');
            if (!$apiKey) {
                fwrite(STDERR, "Soniox requires SONIOX_API_KEY.\n");
                continue 2;
            }
            $requestUrl = ($url !== '' && $url !== '-')
                ? $url : 'https://tts-rt.soniox.com/tts';
            $audio = sonioxAudioFormat($format);
            $payload = array(
                'text' => $text,
                'model' => getenv('SONIOX_MODEL_ID') ?: 'tts-rt-v2',
                'language' => $language,
                'voice' => $voice,
                'audio_format' => $audio['audio_format'],
                'sample_rate' => $audio['sample_rate'],
            );
            curl_setopt($ch, CURLOPT_URL, $requestUrl);
            curl_setopt($ch, CURLOPT_HTTPHEADER, array(
                'Authorization: Bearer ' . $apiKey,
                'Content-Type: application/json',
            ));
            curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
            break;

        case 'rime':
            $apiKey = getenv('RIME_API_KEY');
            if (!$apiKey) {
                fwrite(STDERR, "Rime requires RIME_API_KEY.\n");
                continue 2;
            }
            if (strtolower($format) === 'alaw') {
                fwrite(STDERR, "Rime Mist v3 does not provide native A-law output.\n");
                continue 2;
            }
            $requestUrl = ($url !== '' && $url !== '-')
                ? $url : 'https://mist.api.rime.ai/';
            $audio = rimeAudioFormat($format);
            $payload = array(
                'text' => $text,
                'speaker' => $voice,
                'lang' => $language,
                'samplingRate' => $audio['sample_rate'],
            );
            curl_setopt($ch, CURLOPT_URL, $requestUrl);
            curl_setopt($ch, CURLOPT_HTTPHEADER, array(
                'Authorization: Bearer ' . $apiKey,
                'Accept: ' . $audio['accept'],
                'Content-Type: application/json',
            ));
            curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
            break;

        case 'watson':
            $apiKey = getenv('IBM_WATSON_TTS_API_KEY');
            if (!$apiKey) {
                fwrite(STDERR, "Watson TTS requires IBM_WATSON_TTS_API_KEY.\n");
                continue 2;
            }
            if ($url === '' || $url === '-') {
                fwrite(STDERR, "Watson TTS requires its service URL as arg1.\n");
                continue 2;
            }
            curl_setopt($ch, CURLOPT_URL,
                rtrim($url, '?') . '?voice=' . rawurlencode($voice));
            curl_setopt($ch, CURLOPT_USERPWD, 'apikey:' . $apiKey);
            curl_setopt($ch, CURLOPT_HTTPHEADER, array(
                'Content-Type: application/json',
                'Accept: ' . watsonAudioFormat($format),
            ));
            curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(array('text' => $text)));
            break;

        case 'generic':
            curl_setopt($ch, CURLOPT_URL, $url);
            curl_setopt($ch, CURLOPT_HTTPHEADER,
                array('Content-Type: application/x-www-form-urlencoded'));
            curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query(array(
                'text' => $text,
                'language' => $language,
                'voice' => $voice,
                'format' => $format,
            )));
            break;

        default:
            fwrite(STDERR, "Unsupported connector: $connector\n");
            curl_close($ch);
            continue 2;
    }

    $result = curl_exec($ch);
    $curlError = curl_error($ch);
    $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($result === false || $curlError !== '') {
        fwrite(STDERR, "TTS request failed: $curlError\n");
        continue;
    }
    if ($httpCode < 200 || $httpCode >= 300) {
        fwrite(STDERR, "TTS request returned HTTP $httpCode: $result\n");
        continue;
    }

    if ($connector === 'google' || $connector === 'google_rest') {
        $response = json_decode($result, true);
        if (!is_array($response) || empty($response['audioContent'])) {
            fwrite(STDERR, "Google TTS response does not contain audioContent.\n");
            continue;
        }
        $result = base64_decode($response['audioContent'], true);
        if ($result === false) {
            fwrite(STDERR, "Unable to decode Google TTS audioContent.\n");
            continue;
        }
    }
    if ($connector === 'elevenlabs' && strtolower($format) === 'wav') {
        $result = pcmToWav($result, 16000, 1, 16);
    }

    if (file_put_contents($destination, $result) === false) {
        fwrite(STDERR, "Unable to write: $destination\n");
        continue;
    }
    echo "Created: $destination\n\n";
}

fclose($file);

function googleAudioEncoding($format)
{
    switch (strtolower($format)) {
        case 'mp3': return 'MP3';
        case 'ulaw': case 'mulaw': return 'MULAW';
        case 'alaw': return 'ALAW';
        case 'ogg': case 'opus': return 'OGG_OPUS';
        default: return 'LINEAR16';
    }
}

function elevenLabsOutputFormat($format)
{
    switch (strtolower($format)) {
        case 'ulaw': case 'mulaw': return 'ulaw_8000';
        case 'wav': return 'pcm_16000';
        case 'sln': case 'raw': return 'pcm_16000';
        case 'sln24': return 'pcm_24000';
        case 'sln44': return 'pcm_44100';
        default: return 'mp3_44100_128';
    }
}

function pcmToWav($pcm, $sampleRate, $channels, $bitsPerSample)
{
    $dataLength = strlen($pcm);
    $byteRate = $sampleRate * $channels * ($bitsPerSample / 8);
    $blockAlign = $channels * ($bitsPerSample / 8);
    return 'RIFF' . pack('V', 36 + $dataLength) . 'WAVE' .
        'fmt ' . pack('VvvVVvv', 16, 1, $channels, $sampleRate,
            $byteRate, $blockAlign, $bitsPerSample) .
        'data' . pack('V', $dataLength) . $pcm;
}

function watsonAudioFormat($format)
{
    switch (strtolower($format)) {
        case 'ulaw': case 'mulaw': return 'audio/mulaw;rate=8000';
        case 'mp3': return 'audio/mp3';
        case 'ogg': return 'audio/ogg;codecs=opus';
        default: return 'audio/wav;rate=8000';
    }
}

function microsoftAudioFormat($format)
{
    switch (strtolower($format)) {
        case 'mp3': return 'audio-16khz-128kbitrate-mono-mp3';
        case 'ulaw': case 'mulaw': return 'riff-8khz-8bit-mono-mulaw';
        case 'alaw': return 'riff-8khz-8bit-mono-alaw';
        case 'ogg': case 'opus': return 'ogg-24khz-16bit-mono-opus';
        case 'sln': case 'raw': return 'raw-16khz-16bit-mono-pcm';
        case 'sln24': return 'raw-24khz-16bit-mono-pcm';
        case 'sln48': return 'raw-48khz-16bit-mono-pcm';
        default: return 'riff-16khz-16bit-mono-pcm';
    }
}

function cartesiaAudioFormat($format)
{
    switch (strtolower($format)) {
        case 'ulaw':
        case 'mulaw':
            return array('container' => 'raw', 'encoding' => 'pcm_mulaw',
                'sample_rate' => 8000);
        case 'alaw':
            return array('container' => 'raw', 'encoding' => 'pcm_alaw',
                'sample_rate' => 8000);
        case 'sln24':
            return array('container' => 'raw', 'encoding' => 'pcm_s16le',
                'sample_rate' => 24000);
        case 'sln44':
            return array('container' => 'raw', 'encoding' => 'pcm_s16le',
                'sample_rate' => 44100);
        case 'sln48':
            return array('container' => 'raw', 'encoding' => 'pcm_s16le',
                'sample_rate' => 48000);
        case 'sln':
        case 'raw':
            return array('container' => 'raw', 'encoding' => 'pcm_s16le',
                'sample_rate' => 16000);
        default:
            return array('container' => 'wav', 'encoding' => 'pcm_s16le',
                'sample_rate' => 16000);
    }
}

function sonioxAudioFormat($format)
{
    switch (strtolower($format)) {
        case 'mp3':
            return array('audio_format' => 'mp3', 'sample_rate' => 16000);
        case 'ulaw':
        case 'mulaw':
            return array('audio_format' => 'pcm_mulaw', 'sample_rate' => 8000);
        case 'alaw':
            return array('audio_format' => 'pcm_alaw', 'sample_rate' => 8000);
        case 'ogg':
        case 'opus':
            return array('audio_format' => 'opus', 'sample_rate' => 16000);
        case 'flac':
            return array('audio_format' => 'flac', 'sample_rate' => 16000);
        case 'sln24':
            return array('audio_format' => 'pcm_s16le', 'sample_rate' => 24000);
        case 'sln44':
            return array('audio_format' => 'pcm_s16le', 'sample_rate' => 44100);
        case 'sln48':
            return array('audio_format' => 'pcm_s16le', 'sample_rate' => 48000);
        case 'sln':
        case 'raw':
            return array('audio_format' => 'pcm_s16le', 'sample_rate' => 16000);
        default:
            return array('audio_format' => 'wav', 'sample_rate' => 8000);
    }
}

function rimeAudioFormat($format)
{
    switch (strtolower($format)) {
        case 'mp3':
            return array('accept' => 'audio/mpeg', 'sample_rate' => 24000);
        case 'ulaw':
        case 'mulaw':
            return array('accept' => 'audio/PCMU', 'sample_rate' => 8000);
        case 'ogg':
        case 'opus':
            return array('accept' => 'audio/ogg;codecs=opus', 'sample_rate' => 24000);
        case 'webm':
            return array('accept' => 'audio/webm;codecs=opus', 'sample_rate' => 24000);
        case 'sln24':
            return array('accept' => 'audio/L16', 'sample_rate' => 24000);
        case 'sln44':
            return array('accept' => 'audio/L16', 'sample_rate' => 44100);
        case 'sln':
        case 'raw':
            return array('accept' => 'audio/L16', 'sample_rate' => 16000);
        default:
            return array('accept' => 'audio/wav', 'sample_rate' => 16000);
    }
}

function xmlEscape($value)
{
    return htmlspecialchars($value, ENT_XML1 | ENT_QUOTES, 'UTF-8');
}

function help()
{
    return "Usage:\n" .
        "  vxmlaudios URL LANGUAGE VOICE INPUT OUTPUT [CONNECTOR]\n\n" .
        "CONNECTOR: generic (default), watson, google, elevenlabs, microsoft, azure, cartesia, soniox, rime\n" .
        "Use '-' as URL for a default cloud endpoint.\n\n" .
        "Credentials:\n" .
        "  GOOGLE_TTS_API_KEY or GOOGLE_TTS_ACCESS_TOKEN\n" .
        "  ELEVENLABS_API_KEY [and optional ELEVENLABS_MODEL_ID]\n" .
        "  AZURE_TTS_KEY and AZURE_TTS_REGION, or AZURE_TTS_ACCESS_TOKEN\n" .
        "  CARTESIA_API_KEY [and optional CARTESIA_MODEL_ID, CARTESIA_VERSION]\n" .
        "  SONIOX_API_KEY [and optional SONIOX_MODEL_ID]\n" .
        "  RIME_API_KEY\n" .
        "  IBM_WATSON_TTS_API_KEY\n\n" .
        "Input format, one prompt per line:\n" .
        "  welcome.wav:Welcome to our service.\n" .
        "  prompts/error.mp3:An error occurred.\n\n" .
        "Examples:\n" .
        "  vxmlaudios - fr-FR fr-FR-Wavenet-A texts.txt sounds google\n" .
        "  vxmlaudios - fr voice-id texts.txt sounds elevenlabs\n" .
        "  vxmlaudios - fr-FR fr-FR-DeniseNeural texts.txt sounds microsoft\n" .
        "  vxmlaudios - fr voice-id texts.txt sounds cartesia\n" .
        "  vxmlaudios - fr Adrian texts.txt sounds soniox\n" .
        "  vxmlaudios - fr cove texts.txt sounds rime\n";
}

?>
