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
+84
View File
@@ -0,0 +1,84 @@
package controllers
import (
"io"
"github.com/gofiber/fiber/v2"
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
)
func OcrConfigPage(c *fiber.Ctx) error {
cfg, _ := models.GetOcrConfig()
return c.Render("ocr_config", fiber.Map{
"user": c.Locals("user"),
"modules": c.Locals("userModules"),
"cfg": cfg,
}, "layouts/main")
}
func GetOcrConfigHandler(c *fiber.Ctx) error {
cfg, err := models.GetOcrConfig()
if err != nil {
return c.JSON(fiber.Map{"data": nil})
}
return c.JSON(fiber.Map{"data": cfg})
}
func SaveOcrConfigHandler(c *fiber.Ctx) error {
type body struct {
ID uint `json:"id"`
BaseURL string `json:"base_url"`
Token string `json:"token"`
Notas string `json:"notas"`
}
var b body
if err := c.BodyParser(&b); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
}
if b.BaseURL == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "base_url es requerida"})
}
if b.Token == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "token es requerido"})
}
cfg := models.OcrConfig{BaseURL: b.BaseURL, Token: b.Token, Notas: b.Notas}
cfg.ID = b.ID
if err := models.SaveOcrConfig(cfg); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"message": "Configuración guardada"})
}
// TestOcrConfigHandler recibe una imagen de prueba y devuelve el texto que
// extrae el servicio configurado — confirma que la URL/token funcionan de
// verdad, no solo que se guardaron.
func TestOcrConfigHandler(c *fiber.Ctx) error {
fh, err := c.FormFile("imagen")
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "imagen requerida"})
}
if fh.Size > 8*1024*1024 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "máximo 8MB"})
}
f, err := fh.Open()
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "no se pudo leer el archivo"})
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "no se pudo leer el archivo"})
}
mimeType := fh.Header.Get("Content-Type")
if mimeType == "" {
mimeType = "image/png"
}
texto, err := services.ExtraerTextoOCR(data, mimeType)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"text": texto})
}
@@ -0,0 +1,80 @@
package controllers
import (
"io"
"github.com/gofiber/fiber/v2"
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
)
func WhisperAsrConfigPage(c *fiber.Ctx) error {
cfg, _ := models.GetWhisperAsrConfig()
return c.Render("whisper_asr_config", fiber.Map{
"user": c.Locals("user"),
"modules": c.Locals("userModules"),
"cfg": cfg,
}, "layouts/main")
}
func GetWhisperAsrConfigHandler(c *fiber.Ctx) error {
cfg, err := models.GetWhisperAsrConfig()
if err != nil {
return c.JSON(fiber.Map{"data": nil})
}
return c.JSON(fiber.Map{"data": cfg})
}
func SaveWhisperAsrConfigHandler(c *fiber.Ctx) error {
type body struct {
ID uint `json:"id"`
BaseURL string `json:"base_url"`
Username string `json:"username"`
Password string `json:"password"`
Notas string `json:"notas"`
}
var b body
if err := c.BodyParser(&b); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
}
if b.BaseURL == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "base_url es requerida"})
}
if b.Username == "" || b.Password == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "usuario y contraseña son requeridos"})
}
cfg := models.WhisperAsrConfig{BaseURL: b.BaseURL, Username: b.Username, Password: b.Password, Notas: b.Notas}
cfg.ID = b.ID
if err := models.SaveWhisperAsrConfig(cfg); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"message": "Configuración guardada"})
}
// TestWhisperAsrConfigHandler recibe un audio de prueba y devuelve la
// transcripción — confirma que la URL/credenciales funcionan de verdad.
func TestWhisperAsrConfigHandler(c *fiber.Ctx) error {
fh, err := c.FormFile("audio")
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "audio requerido"})
}
if fh.Size > 20*1024*1024 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "máximo 20MB"})
}
f, err := fh.Open()
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "no se pudo leer el archivo"})
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "no se pudo leer el archivo"})
}
texto, err := services.TranscribirAudioSelfHosted(data, fh.Filename)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"text": texto})
}
+12
View File
@@ -619,6 +619,18 @@ func UserRoutes(app fiber.Router) {
protected.Get("/websms/logs", controllers.GetWebSmsLogs)
protected.Get("/websms/webhook-logs", controllers.GetWebSmsWebhookLogs)
// ─── OCR (servicio propio) ─────────────────────────────────────────────────
protected.Get("/ocr", middlewares.MenuMiddleware, controllers.OcrConfigPage)
protected.Get("/ocr/config", controllers.GetOcrConfigHandler)
protected.Post("/ocr/save", controllers.SaveOcrConfigHandler)
protected.Post("/ocr/test", controllers.TestOcrConfigHandler)
// ─── Whisper ASR (transcripción, servicio propio) ─────────────────────────
protected.Get("/whisper-asr", middlewares.MenuMiddleware, controllers.WhisperAsrConfigPage)
protected.Get("/whisper-asr/config", controllers.GetWhisperAsrConfigHandler)
protected.Post("/whisper-asr/save", controllers.SaveWhisperAsrConfigHandler)
protected.Post("/whisper-asr/test", controllers.TestWhisperAsrConfigHandler)
// ─── Planes dLocal (gestión desde panel protegido) ────────────────────────
protected.Get("/dlocal/planes", apiControllers.SeePlanes)
protected.Post("/dlocal/planes", apiControllers.CreatePlan)