feat: agrega integraciones OCR y Whisper ASR (servicios propios)

Nuevos submódulos en Integraciones para conectar el servicio propio de
OCR (extracción de texto de imágenes) y el de transcripción de audio
self-hosted (whisper-asr-webservice, Basic Auth), con panel de
configuración y prueba en vivo, siguiendo el patrón ya usado por
WebSMS/Hostinger.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-08-13 10:09:40 -05:00
co-authored by Claude Sonnet 5
parent 997e3cc790
commit d493d6dee6
11 changed files with 621 additions and 0 deletions
+67
View File
@@ -0,0 +1,67 @@
package services
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
)
var ocrHTTPClient = &http.Client{Timeout: 30 * time.Second}
// ExtraerTextoOCR manda una imagen al servicio propio de OCR y devuelve el
// texto extraído. mimeType ej: "image/png", "image/jpeg".
func ExtraerTextoOCR(imagenBytes []byte, mimeType string) (string, error) {
cfg, err := models.GetOcrConfig()
if err != nil {
return "", fmt.Errorf("el servicio de OCR no está configurado (Integraciones → OCR)")
}
body, err := json.Marshal(map[string]string{
"image_base64": base64.StdEncoding.EncodeToString(imagenBytes),
"mime_type": mimeType,
})
if err != nil {
return "", err
}
req, err := http.NewRequest(http.MethodPost, cfg.BaseURL, bytes.NewReader(body))
if err != nil {
return "", err
}
req.Header.Set("Authorization", "Bearer "+cfg.Token)
req.Header.Set("Content-Type", "application/json")
resp, err := ocrHTTPClient.Do(req)
if err != nil {
return "", fmt.Errorf("no se pudo contactar el servicio de OCR: %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 OCR respondió %d: %s", resp.StatusCode, string(raw))
}
var out struct {
Success bool `json:"success"`
Text string `json:"text"`
Error string `json:"error"`
}
if err := json.Unmarshal(raw, &out); err != nil {
return "", fmt.Errorf("respuesta inesperada del servicio de OCR: %s", string(raw))
}
if !out.Success {
msg := out.Error
if msg == "" {
msg = "el servicio de OCR no pudo procesar la imagen"
}
return "", fmt.Errorf("%s", msg)
}
return out.Text, nil
}
+67
View File
@@ -0,0 +1,67 @@
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.
func TranscribirAudioSelfHosted(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))
}
return out.Text, nil
}