Es la base del cobro por uso: hasta ahora no había ninguna medición de consumo en todo el repo. - UmindUso registra cada evento facturable con el costo YA calculado al precio vigente del plan. Congelarlo evita que subir un precio revalúe consumo pasado, que haría indefendible una factura ante un reclamo. - callAI devuelve los tokens que reportó el proveedor (campo usage, igual en todos los OpenAI-compatibles; input+output en Anthropic). Se mide cada ronda de tool-calling, no solo la última: todas gastan tokens. - ExtraerTextoOCR y TranscribirAudioSelfHosted reciben agenteID; 0 = no medir, que es lo que pasan los botones "Probar" del panel de staff. - Aviso al superar el tope del plan, una vez por mes y sin cortar el servicio. El flag de "ya avisé" es en memoria a propósito. - GET /app/umind/uso con filtros de fecha: resumen por tipo + detalle. - Test del cálculo de costo por tipo, incluida fracción de 1k tokens y tenant sin plan. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
75 lines
2.2 KiB
Go
75 lines
2.2 KiB
Go
package services
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"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))
|
|
}
|
|
|
|
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: %s", string(raw))
|
|
}
|
|
RegistrarUso(agenteID, models.UsoTipoWhisper, 1, "transcripcion")
|
|
return out.Text, nil
|
|
}
|