Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc54ec992b | ||
|
|
9509c3b4fc |
@@ -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;
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Whisper server (auto-hosted) -->
|
||||
<div class="form-group" style="margin-top:12px;padding:12px;background:#f0f7f0;border:1px solid #c6e0c6;border-radius:8px">
|
||||
<label style="font-weight:600">🎙️ Servidor Whisper propio (transcripción de audio)</label>
|
||||
<div style="font-size:11px;color:#4a5568;margin-bottom:8px">Si se configura, el audio se transcribe aquí en vez del proveedor seleccionado arriba.</div>
|
||||
<div class="form-group">
|
||||
<label>URL del servidor</label>
|
||||
<input type="text" name="whisper_url" value="{$whisperUrlVal}" placeholder="https://whisper.u-s.app/asr">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Usuario</label>
|
||||
<input type="text" name="whisper_user" value="{$whisperUserVal}" placeholder="whisper">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Contraseña</label>
|
||||
<input type="password" name="whisper_pass" value="{$whisperPassVal}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row" style="margin-top:4px">
|
||||
<div class="form-group">
|
||||
<label>Temperatura ({$aiTempVal})</label>
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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, [
|
||||
|
||||
@@ -509,6 +509,7 @@ class NormalBot
|
||||
$cap['collected_labels'][$field['key']] = $value;
|
||||
unset($cap['__awaiting_other']);
|
||||
$cap['index'] = $idx + 1;
|
||||
while ($cap['index'] < count($fields) && self::capShouldSkip($fields[$cap['index']], $cap['collected'])) { $cap['index']++; }
|
||||
$meta['__cap'] = $cap;
|
||||
ConversationContext::updateMetadata($ctxId, $meta);
|
||||
if ($cap['index'] < count($fields)) {
|
||||
@@ -566,6 +567,7 @@ class NormalBot
|
||||
}
|
||||
|
||||
$cap['index'] = $idx + 1;
|
||||
while ($cap['index'] < count($fields) && self::capShouldSkip($fields[$cap['index']], $cap['collected'])) { $cap['index']++; }
|
||||
$meta['__cap'] = $cap;
|
||||
ConversationContext::updateMetadata($ctxId, $meta);
|
||||
|
||||
@@ -713,6 +715,17 @@ class NormalBot
|
||||
return self::sendText($text, $to, $company);
|
||||
}
|
||||
|
||||
// skip_if: {"field":"novedad_id","not_in":["35","41"]} → skip this field if collected[field] not in list
|
||||
private static function capShouldSkip(array $field, array $collected): bool
|
||||
{
|
||||
$si = $field['skip_if'] ?? null;
|
||||
if (!$si) return false;
|
||||
$val = (string)($collected[$si['field'] ?? ''] ?? '');
|
||||
if (isset($si['not_in'])) return !in_array($val, array_map('strval', $si['not_in']));
|
||||
if (isset($si['equals'])) return $val !== (string)$si['equals'];
|
||||
return false;
|
||||
}
|
||||
|
||||
private static function capFieldLabel(array $fields, string $key): string
|
||||
{
|
||||
foreach ($fields as $f) {
|
||||
@@ -1128,6 +1141,7 @@ class NormalBot
|
||||
$cap['collected_labels'][$field['key']] = $pending['label'];
|
||||
unset($cap['__lookup_pending']);
|
||||
$cap['index'] = $idx + 1;
|
||||
while ($cap['index'] < count($fields) && self::capShouldSkip($fields[$cap['index']], $cap['collected'])) { $cap['index']++; }
|
||||
$meta['__cap'] = $cap;
|
||||
ConversationContext::updateMetadata($ctxId, $meta);
|
||||
if ($cap['index'] < count($fields)) {
|
||||
|
||||
Reference in New Issue
Block a user