diff --git a/admin/DashboardController.php b/admin/DashboardController.php
index ed34bbf..f4b00b6 100644
--- a/admin/DashboardController.php
+++ b/admin/DashboardController.php
@@ -3170,6 +3170,9 @@ HTML;
$aiModel35 = $aiModelVal === 'gpt-3.5-turbo'? ' selected' : '';
$geminiApiKeyVal = self::h($config['gemini_api_key'] ?? '');
$geminiModelVal = $config['gemini_model'] ?? 'gemini-2.5-flash';
+ $whisperUrlVal = self::h($config['whisper_url'] ?? '');
+ $whisperUserVal = self::h($config['whisper_user'] ?? '');
+ $whisperPassVal = self::h($config['whisper_pass'] ?? '');
$geminiModelF25 = $geminiModelVal === 'gemini-2.5-flash' ? ' selected' : '';
$geminiModelF20 = $geminiModelVal === 'gemini-2.0-flash' ? ' selected' : '';
$geminiModelF15 = $geminiModelVal === 'gemini-1.5-flash' ? ' selected' : '';
@@ -3717,6 +3720,26 @@ HTML;
+
+
+
@@ -3862,6 +3885,10 @@ function saveAiConfig() {
if (oKey) fd.set('openai_api_key', oKey);
if (gKey) fd.set('gemini_api_key', gKey);
if (cKey) fd.set('claude_api_key', cKey);
+ fd.set('whisper_url', qs('input[name="whisper_url"]')?.value || '');
+ fd.set('whisper_user', qs('input[name="whisper_user"]')?.value || '');
+ const wPass = qs('input[name="whisper_pass"]')?.value || '';
+ if (wPass) fd.set('whisper_pass', wPass);
if (qs('input[name="ai_for_media"]')?.checked) fd.set('ai_for_media', '1');
if (qs('input[name="nlu"]')?.checked) fd.set('nlu', '1');
diff --git a/public/index.php b/public/index.php
index bbba291..3e05534 100644
--- a/public/index.php
+++ b/public/index.php
@@ -634,6 +634,13 @@ $routes = [
$claudeModel = trim($_POST['claude_model'] ?? '');
if ($claudeModel !== '') $config['claude_model'] = $claudeModel;
+ $whisperUrl = trim($_POST['whisper_url'] ?? '');
+ $config['whisper_url'] = $whisperUrl;
+ $whisperUser = trim($_POST['whisper_user'] ?? '');
+ $config['whisper_user'] = $whisperUser;
+ $whisperPass = trim($_POST['whisper_pass'] ?? '');
+ if ($whisperPass !== '') $config['whisper_pass'] = $whisperPass;
+
CompanyRepository::save(['id' => $companyId, 'config_json' => json_encode($config, JSON_UNESCAPED_UNICODE)]);
echo json_encode(['ok' => true]);
exit;
diff --git a/services/MediaTranscriber.php b/services/MediaTranscriber.php
index 9ab2506..3e862ef 100644
--- a/services/MediaTranscriber.php
+++ b/services/MediaTranscriber.php
@@ -69,13 +69,48 @@ class MediaTranscriber
private static function whisper(string $bytes, string $mime, array $cfg): ?string
{
- $apiKey = $cfg['openai_api_key'] ?? env('OPENAI_API_KEY', '');
- if ($apiKey === '') return null;
-
$ext = str_contains($mime, 'ogg') ? 'ogg' : (str_contains($mime, 'mp4') ? 'mp4' : 'ogg');
$tmpFile = tempnam(sys_get_temp_dir(), 'wa_audio_') . '.' . $ext;
file_put_contents($tmpFile, $bytes);
+ $whisperUrl = trim($cfg['whisper_url'] ?? '');
+ $whisperUser = trim($cfg['whisper_user'] ?? '');
+ $whisperPass = trim($cfg['whisper_pass'] ?? '');
+ $useOwn = $whisperUrl !== '';
+
+ if ($useOwn) {
+ $t0 = (int)(microtime(true) * 1000);
+ $ch = curl_init($whisperUrl);
+ $opts = [
+ CURLOPT_POST => true,
+ CURLOPT_POSTFIELDS => ['audio_file' => new CURLFile($tmpFile, $mime, 'audio.' . $ext)],
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_TIMEOUT => 120,
+ ];
+ if ($whisperUser !== '') {
+ $opts[CURLOPT_USERPWD] = "{$whisperUser}:{$whisperPass}";
+ }
+ curl_setopt_array($ch, $opts);
+ $resp = curl_exec($ch);
+ $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ curl_close($ch);
+ $ms = (int)(microtime(true) * 1000) - $t0;
+ @unlink($tmpFile);
+
+ if ($code !== 200 || !$resp) return null;
+
+ // Respuesta puede ser JSON {"text":"..."} o texto plano
+ $data = json_decode($resp, true);
+ $text = is_array($data) ? trim($data['text'] ?? '') : trim($resp);
+ if ($text === '') return null;
+ AiLogger::log($cfg['__company_id'] ?? null, 'whisper-own', 'whisper', 'transcribe', '[audio]', $text, $ms);
+ return $text;
+ }
+
+ // Fallback: OpenAI cloud Whisper
+ $apiKey = $cfg['openai_api_key'] ?? env('OPENAI_API_KEY', '');
+ if ($apiKey === '') { @unlink($tmpFile); return null; }
+
$t0 = (int)(microtime(true) * 1000);
$ch = curl_init('https://api.openai.com/v1/audio/transcriptions');
curl_setopt_array($ch, [