package services import ( "bytes" "encoding/json" "fmt" "io" "mime/multipart" "net/http" "strings" "time" "github.com/sujit-baniya/fiber-boilerplate/pkg/models" ) var whisperAsrHTTPClient = &http.Client{Timeout: 120 * time.Second} // audio largo puede tardar // TranscribirAudioSelfHosted manda un archivo de audio al servicio propio de // transcripción (whisper-asr-webservice, Basic Auth) y devuelve el texto. // // agenteID identifica a quién cobrarle la transcripción; 0 = no medir. // // ponytail: se cobra por transcripción, no por minuto de audio — // whisper-asr-webservice con response_format=json no devuelve la duración. Si // hace falta cobrar por minuto, pedirle verbose_json y sumar los segments. func TranscribirAudioSelfHosted(agenteID uint, audioBytes []byte, filename string) (string, error) { cfg, err := models.GetWhisperAsrConfig() if err != nil { return "", fmt.Errorf("el servicio de transcripción no está configurado (Integraciones → Whisper ASR)") } if filename == "" { filename = "audio.wav" } var buf bytes.Buffer w := multipart.NewWriter(&buf) fw, err := w.CreateFormFile("audio_file", filename) if err != nil { return "", err } if _, err := fw.Write(audioBytes); err != nil { return "", err } _ = w.WriteField("response_format", "json") if err := w.Close(); err != nil { return "", err } req, err := http.NewRequest(http.MethodPost, cfg.BaseURL, &buf) if err != nil { return "", err } req.Header.Set("Content-Type", w.FormDataContentType()) req.SetBasicAuth(cfg.Username, cfg.Password) resp, err := whisperAsrHTTPClient.Do(req) if err != nil { return "", fmt.Errorf("no se pudo contactar el servicio de transcripción: %w", err) } defer resp.Body.Close() raw, _ := io.ReadAll(io.LimitReader(resp.Body, 2*1024*1024)) if resp.StatusCode < 200 || resp.StatusCode >= 300 { return "", fmt.Errorf("el servicio de transcripción respondió %d: %s", resp.StatusCode, string(raw)) } texto, err := textoDeRespuestaWhisper(raw) if err != nil { return "", err } RegistrarUso(agenteID, models.UsoTipoWhisper, 1, "transcripcion") return texto, nil } // textoDeRespuestaWhisper acepta las dos formas en que puede volver una // transcripción, en vez de adivinar cuál variante corre del otro lado: // // - JSON {"text": "..."} — las APIs compatibles con OpenAI. // - El texto pelado — whisper-asr-webservice devuelve txt por defecto: su // parámetro es `output` (query), no `response_format` (form), así que // ignora el que mandamos. // // Exigir JSON hacía que una transcripción buena se reportara como "respuesta // inesperada", con el texto correcto adentro del mensaje de error. func textoDeRespuestaWhisper(raw []byte) (string, error) { var out struct { Text string `json:"text"` } if err := json.Unmarshal(raw, &out); err == nil && strings.TrimSpace(out.Text) != "" { return strings.TrimSpace(out.Text), nil } texto := strings.TrimSpace(string(raw)) if texto == "" { return "", fmt.Errorf("el servicio de transcripción devolvió una respuesta vacía") } return texto, nil }