Implementación inicial de Nucleo Whisper
Some checks failed
build-and-deploy / build (push) Failing after 5s
build-and-deploy / deploy (push) Has been skipped

- Configurado proyecto Nuxt 4 con PWA
- Integrado OpenAI Whisper API para transcripción de audio
- Implementada captura de audio desde navegador
- Creada UI con grabación y visualización de transcripciones
- Configurado Authentik Proxy para autenticación
- Setup de Docker y Gitea Actions para despliegue
This commit is contained in:
2025-10-13 14:33:04 -06:00
commit 6439ff8f60
49 changed files with 24236 additions and 0 deletions

View File

@@ -0,0 +1,40 @@
/**
* Endpoint para verificar membresía de grupo desde el backend
* Valida contra los headers de Authentik en el servidor
*/
export default defineEventHandler(async (event) => {
// Leer el body de la petición
const body = await readBody(event)
const { groupName } = body
if (!groupName || typeof groupName !== 'string') {
throw createError({
statusCode: 400,
statusMessage: 'Group name is required'
})
}
// Leer headers de Authentik
const headers = getHeaders(event)
const authentikGroups = headers['x-authentik-groups']
// Si no hay header de grupos, el usuario no está autenticado o no tiene grupos
if (!authentikGroups) {
return {
hasGroup: false,
groups: []
}
}
// Parsear los grupos (separados por |)
const userGroups = authentikGroups.split('|').filter(g => g.trim())
// Verificar si el usuario tiene el grupo solicitado
const hasGroup = userGroups.includes(groupName)
return {
hasGroup,
groups: userGroups,
checkedGroup: groupName
}
})

View File

@@ -0,0 +1,43 @@
/**
* API endpoint para verificar el estado de autenticación en tiempo real
* Consulta los headers inyectados por Authentik Proxy Outpost
*/
export default defineEventHandler((event) => {
// Establecer headers para prevenir caching
setResponseHeaders(event, {
'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
})
// Leer headers de Authentik en tiempo real
const headers = getHeaders(event)
const username = headers['x-authentik-username']
const email = headers['x-authentik-email']
const name = headers['x-authentik-name']
const groups = headers['x-authentik-groups']
const uid = headers['x-authentik-uid']
// Si no hay username, no hay sesión activa en Authentik
if (!username) {
return {
authenticated: false,
user: null,
timestamp: new Date().toISOString()
}
}
// Sesión activa
return {
authenticated: true,
user: {
username,
email,
name,
groups: groups ? groups.split('|') : [],
uid
},
timestamp: new Date().toISOString()
}
})

View File

@@ -0,0 +1,106 @@
import { FormData } from 'formdata-node'
export default defineEventHandler(async (event) => {
try {
// Verificar autenticación mediante headers de Authentik
const headers = getRequestHeaders(event)
const username = headers['x-authentik-username']
if (!username) {
throw createError({
statusCode: 401,
message: 'No autenticado'
})
}
// Obtener la API key de OpenAI desde las variables de entorno
const apiKey = process.env.OPENAI_API_KEY
if (!apiKey) {
throw createError({
statusCode: 500,
message: 'API Key de OpenAI no configurada'
})
}
// Leer el archivo de audio del request
const form = await readMultipartFormData(event)
if (!form || form.length === 0) {
throw createError({
statusCode: 400,
message: 'No se recibió ningún archivo de audio'
})
}
// Encontrar el archivo de audio
const audioFile = form.find(part => part.name === 'file')
if (!audioFile) {
throw createError({
statusCode: 400,
message: 'No se encontró el archivo de audio en el formulario'
})
}
// Obtener parámetros opcionales
const languageParam = form.find(part => part.name === 'language')
const promptParam = form.find(part => part.name === 'prompt')
const language = languageParam?.data.toString() || 'es'
const prompt = promptParam?.data.toString()
// Crear FormData para enviar a OpenAI
const formData = new FormData()
// Crear un Blob desde el buffer
const blob = new Blob([audioFile.data], {
type: audioFile.type || 'audio/webm'
})
formData.append('file', blob, audioFile.filename || 'audio.webm')
formData.append('model', 'whisper-1')
formData.append('language', language)
if (prompt) {
formData.append('prompt', prompt)
}
// Enviar a OpenAI Whisper API
const response = await fetch('https://api.openai.com/v1/audio/transcriptions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`
},
body: formData as any
})
if (!response.ok) {
const errorData = await response.text()
console.error('Error de OpenAI:', errorData)
throw createError({
statusCode: response.status,
message: `Error de OpenAI Whisper: ${response.statusText}`
})
}
const result = await response.json()
// Log para auditoría
console.log(`[Whisper] Transcripción exitosa para usuario: ${username}`)
return {
success: true,
transcription: result.text,
user: username
}
} catch (error: any) {
console.error('[Whisper] Error:', error)
if (error.statusCode) {
throw error
}
throw createError({
statusCode: 500,
message: error.message || 'Error al procesar la transcripción'
})
}
})