- Nueva config "whisper" reutilizando /app/ai-config (credenciales/endpoint
validables) y servicio de transcripción compatible con OpenAI/Groq.
- El webhook de Telegram ahora detecta notas de voz/audio, las transcribe
y procesa el texto resultante como si el usuario lo hubiera escrito.
- Fix: subir un archivo adjunto en un comentario de tarea sin escribir
texto era rechazado por el backend ("contenido requerido") aunque el
frontend sí lo permitía.
99 lines
2.8 KiB
Go
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
|
|
}
|