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,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})
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user