Agrega transcripción de audio (Whisper) en Telegram y corrige adjuntos en tareas
- 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.
This commit is contained in:
@@ -174,3 +174,23 @@ func GetAiConfigForService(service string) (*AiConfig, error) {
|
||||
log.Printf("[AI_CONFIG] No se encontró config para servicio '%s', usando cualquier activa", service)
|
||||
return &items[0], nil
|
||||
}
|
||||
|
||||
// GetWhisperConfig retorna la config de IA activa etiquetada específicamente con
|
||||
// el módulo "whisper" (transcripción de audio). A diferencia de
|
||||
// GetAiConfigForService, NO cae a una config global: si el admin no configuró
|
||||
// una explícitamente para whisper, es mejor avisar con claridad que intentar
|
||||
// transcribir contra un proveedor que no soporta ese endpoint (ej. Anthropic).
|
||||
func GetWhisperConfig() (*AiConfig, error) {
|
||||
var items []AiConfig
|
||||
if err := app.Http.Database.DB.Where("is_active = ?", true).Find(&items).Error; err != nil {
|
||||
return nil, fmt.Errorf("error leyendo ai_configs: %w", err)
|
||||
}
|
||||
for i := range items {
|
||||
for _, m := range SplitModulos(items[i].Modulo) {
|
||||
if m == "whisper" {
|
||||
return &items[i], nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no hay ninguna configuración activa con el módulo 'whisper' en /app/ai-config")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user