El servicio devolvía la transcripción correcta y la reportábamos como
error: "respuesta inesperada del servicio de transcripción: <la
transcripción completa>". La causa es que mandábamos response_format=json,
que es la convención de la API de OpenAI — whisper-asr-webservice usa el
query param `output`, así que ignoró el campo y respondió en su formato
por defecto (txt), y el parser exigía JSON.
- textoDeRespuestaWhisper acepta las dos formas (JSON {"text":...} o texto
pelado) en vez de adivinar qué variante corre del otro lado. Con test:
8 casos, incluidos JSON sin campo text y un texto que empieza con "{".
- Whisper y OCR: la caja de resultado ahora muestra el conteo de
caracteres, scrollea si es largo, y tiene Copiar y Descargar .txt (Blob
+ <a download>, el texto ya está en el navegador).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
98 lines
3.0 KiB
Go
98 lines
3.0 KiB
Go
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
|
|
}
|