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:
co-authored by
Claude Sonnet 5
parent
997e3cc790
commit
d493d6dee6
@@ -0,0 +1,41 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/sujit-baniya/fiber-boilerplate/app"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// OcrConfig almacena la conexión al servicio propio de OCR (extracción de
|
||||
// texto de imágenes, ej. comprobantes de pago). Solo un registro activo a
|
||||
// la vez, mismo patrón que HostingerConfig/WebSmsConfig.
|
||||
type OcrConfig struct {
|
||||
gorm.Model
|
||||
BaseURL string `json:"base_url" gorm:"column:base_url;type:text;not null"` // ej: https://ocr.u-s.app/extract
|
||||
Token string `json:"token" gorm:"column:token;type:text;not null"` // Bearer token
|
||||
Notas string `json:"notas" gorm:"column:notas;type:text"`
|
||||
Activo bool `json:"activo" gorm:"column:activo;default:true"`
|
||||
}
|
||||
|
||||
func (OcrConfig) TableName() string { return "ocr_config" }
|
||||
|
||||
func GetOcrConfig() (*OcrConfig, error) {
|
||||
var item OcrConfig
|
||||
if err := app.Http.Database.DB.Where("activo = ?", true).Order("id DESC").First(&item).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func SaveOcrConfig(s OcrConfig) error {
|
||||
app.Http.Database.DB.Model(&OcrConfig{}).Where("activo = ?", true).Update("activo", false)
|
||||
s.Activo = true
|
||||
if s.ID > 0 {
|
||||
return app.Http.Database.DB.Model(&s).Updates(map[string]interface{}{
|
||||
"base_url": s.BaseURL,
|
||||
"token": s.Token,
|
||||
"notas": s.Notas,
|
||||
"activo": true,
|
||||
}).Error
|
||||
}
|
||||
return app.Http.Database.DB.Create(&s).Error
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/sujit-baniya/fiber-boilerplate/app"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// WhisperAsrConfig almacena la conexión al servicio propio de transcripción
|
||||
// de audio (whisper-asr-webservice self-hosted, autenticado con Basic Auth)
|
||||
// — distinto del Whisper de OpenAI que ya se configura vía AiConfig con
|
||||
// modulo "whisper" para el bot de Telegram. Solo un registro activo a la vez.
|
||||
type WhisperAsrConfig struct {
|
||||
gorm.Model
|
||||
BaseURL string `json:"base_url" gorm:"column:base_url;type:text;not null"` // ej: https://whisper.u-s.app/asr
|
||||
Username string `json:"username" gorm:"column:username;size:255;not null"`
|
||||
Password string `json:"password" gorm:"column:password;type:text;not null"`
|
||||
Notas string `json:"notas" gorm:"column:notas;type:text"`
|
||||
Activo bool `json:"activo" gorm:"column:activo;default:true"`
|
||||
}
|
||||
|
||||
func (WhisperAsrConfig) TableName() string { return "whisper_asr_config" }
|
||||
|
||||
func GetWhisperAsrConfig() (*WhisperAsrConfig, error) {
|
||||
var item WhisperAsrConfig
|
||||
if err := app.Http.Database.DB.Where("activo = ?", true).Order("id DESC").First(&item).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func SaveWhisperAsrConfig(s WhisperAsrConfig) error {
|
||||
app.Http.Database.DB.Model(&WhisperAsrConfig{}).Where("activo = ?", true).Update("activo", false)
|
||||
s.Activo = true
|
||||
if s.ID > 0 {
|
||||
return app.Http.Database.DB.Model(&s).Updates(map[string]interface{}{
|
||||
"base_url": s.BaseURL,
|
||||
"username": s.Username,
|
||||
"password": s.Password,
|
||||
"notas": s.Notas,
|
||||
"activo": true,
|
||||
}).Error
|
||||
}
|
||||
return app.Http.Database.DB.Create(&s).Error
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user