/**
 * src/media/voice.ts
 * خط أنابيب الصوت — Voice note transcription (Whisper) + Speech synthesis (Azure/ElevenLabs)
 */
import { readFile, writeFile, mkdir, stat } from 'node:fs/promises';
import { join, extname } from 'node:path';
import { config } from '../config.js';
import { createChildLogger } from '../utils/logger.js';
const log = createChildLogger('media:voice');
const TMP_DIR = '/tmp/muassis-tts';
// ─── Helpers ─────────────────────────────────────────────────────────
async function ensureTmpDir() {
    await mkdir(TMP_DIR, { recursive: true });
}
function getContentType(filePath) {
    const ext = extname(filePath).toLowerCase();
    const mimeMap = {
        '.ogg': 'audio/ogg',
        '.mp3': 'audio/mpeg',
        '.wav': 'audio/wav',
        '.m4a': 'audio/m4a',
        '.webm': 'audio/webm',
        '.flac': 'audio/flac',
    };
    return mimeMap[ext] || 'audio/ogg';
}
// ─── Transcription (OpenAI Whisper) ──────────────────────────────────
/**
 * نسخ الصوت — Transcribe voice note to text using OpenAI Whisper API
 */
export async function transcribeAudio(audioPath, language) {
    if (!config.OPENAI_API_KEY) {
        throw new Error('OPENAI_API_KEY is required for audio transcription');
    }
    const startTime = Date.now();
    log.info({ audioPath, language }, 'Starting audio transcription');
    // Get file info for duration estimate
    let fileSizeBytes = 0;
    try {
        const fileStat = await stat(audioPath);
        fileSizeBytes = fileStat.size;
    }
    catch {
        log.warn({ audioPath }, 'Could not stat audio file');
    }
    // Build multipart form data
    const fileBuffer = await readFile(audioPath);
    const fileName = `audio${extname(audioPath) || '.ogg'}`;
    const formData = new FormData();
    formData.append('file', new Blob([fileBuffer], { type: getContentType(audioPath) }), fileName);
    formData.append('model', 'whisper-1');
    formData.append('response_format', 'verbose_json');
    if (language) {
        formData.append('language', language);
    }
    const response = await fetch('https://api.openai.com/v1/audio/transcriptions', {
        method: 'POST',
        headers: {
            'Authorization': `Bearer ${config.OPENAI_API_KEY}`,
        },
        body: formData,
        signal: AbortSignal.timeout(120_000),
    });
    if (!response.ok) {
        const errorText = await response.text().catch(() => 'Unknown error');
        log.error({ status: response.status, errorText }, 'Whisper API call failed');
        throw new Error(`Whisper API error: ${response.status} — ${errorText}`);
    }
    const data = await response.json();
    const latencyMs = Date.now() - startTime;
    const detectedLanguage = data.language || language || 'unknown';
    const duration = data.duration ?? 0;
    log.info({ language: detectedLanguage, duration, latencyMs, textLength: data.text.length }, 'Transcription complete');
    return {
        text: data.text,
        language: detectedLanguage,
        duration,
    };
}
// ─── Speech Synthesis ────────────────────────────────────────────────
/**
 * تحويل النص إلى كلام — Convert text to Arabic speech
 * Primary: Azure TTS → Fallback: ElevenLabs
 */
export async function synthesizeSpeech(text, options = {}) {
    await ensureTmpDir();
    const { voice = 'male', speed = 1.0 } = options;
    // Try Azure first
    if (config.AZURE_TTS_KEY) {
        try {
            return await synthesizeWithAzure(text, voice, speed);
        }
        catch (error) {
            log.warn({ error }, 'Azure TTS failed, trying ElevenLabs fallback');
        }
    }
    // Fallback to ElevenLabs
    if (config.ELEVENLABS_API_KEY) {
        try {
            return await synthesizeWithElevenLabs(text, voice, speed);
        }
        catch (error) {
            log.error({ error }, 'ElevenLabs TTS also failed');
            throw error;
        }
    }
    throw new Error('No TTS provider available. Set AZURE_TTS_KEY or ELEVENLABS_API_KEY.');
}
/**
 * Azure Cognitive Services TTS
 */
async function synthesizeWithAzure(text, voice, speed) {
    const region = config.AZURE_TTS_REGION;
    const voiceName = voice === 'female' ? 'ar-SA-ZariyahNeural' : 'ar-SA-HamedNeural';
    // Rate: 0.5 = slow, 1.0 = normal, 2.0 = fast
    const ratePercent = Math.round((speed - 1) * 100);
    const rateStr = ratePercent >= 0 ? `+${ratePercent}%` : `${ratePercent}%`;
    const ssml = `<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xml:lang='ar-SA'>
  <voice name='${voiceName}'>
    <prosody rate='${rateStr}'>
      ${escapeXml(text)}
    </prosody>
  </voice>
</speak>`;
    log.debug({ voiceName, region, textLength: text.length }, 'Calling Azure TTS');
    const response = await fetch(`https://${region}.tts.speech.microsoft.com/cognitiveservices/v1`, {
        method: 'POST',
        headers: {
            'Ocp-Apim-Subscription-Key': config.AZURE_TTS_KEY,
            'Content-Type': 'application/ssml+xml',
            'X-Microsoft-OutputFormat': 'audio-16khz-128kbitrate-mono-mp3',
        },
        body: ssml,
        signal: AbortSignal.timeout(60_000),
    });
    if (!response.ok) {
        const errorText = await response.text().catch(() => 'Unknown error');
        throw new Error(`Azure TTS error: ${response.status} — ${errorText}`);
    }
    const arrayBuffer = await response.arrayBuffer();
    const audioBuffer = Buffer.from(arrayBuffer);
    const fileName = `tts-${Date.now()}.mp3`;
    const filePath = join(TMP_DIR, fileName);
    await writeFile(filePath, audioBuffer);
    // Rough estimate: ~150 words per minute for Arabic
    const wordCount = text.split(/\s+/).length;
    const durationEstimate = Math.round((wordCount / 150) * 60);
    log.info({ filePath, sizeBytes: audioBuffer.length, voice: voiceName }, 'Azure TTS synthesis complete');
    return {
        audioBuffer,
        filePath,
        format: 'mp3',
        durationEstimate,
    };
}
/**
 * ElevenLabs TTS (fallback)
 */
async function synthesizeWithElevenLabs(text, voice, speed) {
    // Default Arabic voices on ElevenLabs
    // Male: "Adam" (general purpose) — Female: "Nadia" (Arabic-friendly)
    const voiceId = voice === 'female'
        ? 'jBpfAFnaylXS5xM6wOFm' // Nadia – Arabic
        : 'pNInz6obpgDQGcFmaJgB'; // Adam – multilingual
    log.debug({ voiceId, textLength: text.length }, 'Calling ElevenLabs TTS');
    const response = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`, {
        method: 'POST',
        headers: {
            'xi-api-key': config.ELEVENLABS_API_KEY,
            'Content-Type': 'application/json',
            'Accept': 'audio/mpeg',
        },
        body: JSON.stringify({
            text,
            model_id: 'eleven_multilingual_v2',
            voice_settings: {
                stability: 0.5,
                similarity_boost: 0.75,
                speed,
            },
        }),
        signal: AbortSignal.timeout(60_000),
    });
    if (!response.ok) {
        const errorText = await response.text().catch(() => 'Unknown error');
        throw new Error(`ElevenLabs TTS error: ${response.status} — ${errorText}`);
    }
    const arrayBuffer = await response.arrayBuffer();
    const audioBuffer = Buffer.from(arrayBuffer);
    const fileName = `tts-${Date.now()}.mp3`;
    const filePath = join(TMP_DIR, fileName);
    await writeFile(filePath, audioBuffer);
    const wordCount = text.split(/\s+/).length;
    const durationEstimate = Math.round((wordCount / 150) * 60);
    log.info({ filePath, sizeBytes: audioBuffer.length, voiceId }, 'ElevenLabs TTS synthesis complete');
    return {
        audioBuffer,
        filePath,
        format: 'mp3',
        durationEstimate,
    };
}
// ─── Full Pipeline ───────────────────────────────────────────────────
/**
 * معالجة الرسالة الصوتية — Full pipeline: transcribe → return text + metadata
 */
export async function processVoiceNote(audioPath) {
    log.info({ audioPath }, 'Processing voice note');
    const transcription = await transcribeAudio(audioPath);
    return {
        text: transcription.text,
        language: transcription.language,
        duration: transcription.duration,
        audioPath,
    };
}
// ─── Utilities ───────────────────────────────────────────────────────
function escapeXml(text) {
    return text
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&apos;');
}
//# sourceMappingURL=voice.js.map