Files
soft_usite/pkg/services/whisper_service.go
T
Lizandro GuarnizoandClaude Sonnet 5 9dc44decd7 fix: unifica el mapeo proveedor→URL, el botón "Probar conexión" tenía su propia copia sin Gemini
El fix anterior de Gemini (22efb9c) solo tocó providerDefaultURL, que usan
el chat/embeddings/whisper reales. TestAiConfigHandler (el botón "Probar
conexión" del panel de AI Config) tenía una tercera copia independiente del
mismo mapeo, sin caso para Gemini ni Deepseek — por eso el chat ya
funcionaba con Gemini pero la prueba de conexión seguía fallando.

Se exporta providerDefaultURL a services.ProviderDefaultURL y el controller
la reusa, en vez de mantener una copia más que se desactualiza cada vez que
se agrega un proveedor.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 21:38:16 -05:00

99 lines
2.8 KiB
Go

package services
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
)
var whisperHTTPClient = &http.Client{Timeout: 90 * time.Second}
// TranscribirAudio envía un archivo de audio a un endpoint de transcripción
// compatible con la API de OpenAI (POST /audio/transcriptions, multipart) y
// devuelve el texto. Funciona tanto con OpenAI (modelo whisper-1) como con Groq
// (modelo whisper-large-v3), reutilizando la config que ya se administra en
// /app/ai-config con el módulo "whisper".
func TranscribirAudio(ai *models.AiConfig, audioPath string) (string, error) {
if ai == nil {
return "", fmt.Errorf("no hay una configuración de Whisper activa: ve a /app/ai-config, crea o edita una y márcale el módulo 'Transcripción de audio (Whisper)'")
}
if ai.ApiKey == "" {
return "", fmt.Errorf("la configuración de Whisper '%s' no tiene API key", ai.Nombre)
}
baseURL := ai.BaseURL
if baseURL == "" {
baseURL = ProviderDefaultURL(ai.Provider)
}
baseURL = strings.TrimRight(baseURL, "/")
model := ai.ModelName
if model == "" {
model = "whisper-1"
}
f, err := os.Open(audioPath)
if err != nil {
return "", fmt.Errorf("no se pudo abrir el audio descargado: %w", err)
}
defer f.Close()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, err := writer.CreateFormFile("file", filepath.Base(audioPath))
if err != nil {
return "", err
}
if _, err := io.Copy(part, f); err != nil {
return "", fmt.Errorf("error leyendo el audio: %w", err)
}
_ = writer.WriteField("model", model)
_ = writer.WriteField("response_format", "json")
if err := writer.Close(); err != nil {
return "", err
}
req, err := http.NewRequest("POST", baseURL+"/audio/transcriptions", &body)
if err != nil {
return "", err
}
req.Header.Set("Authorization", "Bearer "+ai.ApiKey)
req.Header.Set("Content-Type", writer.FormDataContentType())
resp, err := whisperHTTPClient.Do(req)
if err != nil {
return "", fmt.Errorf("no se pudo conectar con el servicio de transcripción: %w", err)
}
defer resp.Body.Close()
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 1024*1024))
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
detalle := strings.TrimSpace(string(raw))
if len(detalle) > 300 {
detalle = detalle[:300]
}
return "", fmt.Errorf("el servicio de transcripción respondió %d: %s", resp.StatusCode, detalle)
}
var out struct {
Text string `json:"text"`
}
if err := json.Unmarshal(raw, &out); err != nil {
return "", fmt.Errorf("respuesta inesperada del servicio de transcripción")
}
texto := strings.TrimSpace(out.Text)
if texto == "" {
return "", fmt.Errorf("no se detectó texto en el audio")
}
return texto, nil
}