Funciones Avanzadas
Funciones avanzadas y capacidades de Qwen Image Edit
Procesamiento Asíncrono
Webhooks para Tareas Largas
Para imágenes grandes o procesamiento complejo, usa webhooks para recibir notificaciones cuando el trabajo esté completo.
// Iniciar procesamiento asíncrono
const trabajo = await editor.editTextAsync({
imagen: 'imagen-4k.jpg',
prompt: 'Edición compleja con múltiples elementos',
webhook: {
url: 'https://mi-app.com/webhook/qwen',
eventos: ['completado', 'error', 'progreso'],
secreto: process.env.WEBHOOK_SECRET,
headers: {
'Authorization': `Bearer ${process.env.API_TOKEN}`,
'X-App-ID': 'mi-aplicacion'
}
},
opciones: {
prioridad: 'alta',
notificarProgreso: true
}
});
console.log('Trabajo iniciado:', trabajo.id);
console.log('Estado:', trabajo.estado);
console.log('URL de seguimiento:', trabajo.urlSeguimiento);
Manejo de Webhooks
// Endpoint para recibir webhooks
app.post('/webhook/qwen', express.raw({type: 'application/json'}), (req, res) => {
const firma = req.headers['x-qwen-signature'];
const payload = req.body;
// Verificar firma del webhook
if (!verificarFirmaWebhook(payload, firma, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Firma inválida');
}
const evento = JSON.parse(payload);
switch (evento.tipo) {
case 'trabajo.completado':
manejarTrabajoCompletado(evento.datos);
break;
case 'trabajo.error':
manejarTrabajoError(evento.datos);
break;
case 'trabajo.progreso':
manejarProgresoTrabajo(evento.datos);
break;
}
res.status(200).send('OK');
});
function verificarFirmaWebhook(payload, firma, secreto) {
const crypto = require('crypto');
const firmaEsperada = crypto
.createHmac('sha256', secreto)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(firma),
Buffer.from(`sha256=${firmaEsperada}`)
);
}
Seguimiento de Trabajos
// Consultar estado de trabajo
async function consultarEstadoTrabajo(trabajoId: string) {
const estado = await editor.getJobStatus(trabajoId);
console.log('Estado del trabajo:', {
id: estado.id,
estado: estado.estado, // 'pendiente', 'procesando', 'completado', 'error'
progreso: estado.progreso, // 0-100
tiempoEstimado: estado.tiempoEstimadoMs,
resultado: estado.resultado,
error: estado.error
});
return estado;
}
// Polling para trabajos sin webhooks
async function esperarCompletado(trabajoId: string, intervalo = 5000) {
return new Promise((resolve, reject) => {
const verificar = async () => {
try {
const estado = await consultarEstadoTrabajo(trabajoId);
if (estado.estado === 'completado') {
resolve(estado.resultado);
} else if (estado.estado === 'error') {
reject(new Error(estado.error));
} else {
setTimeout(verificar, intervalo);
}
} catch (error) {
reject(error);
}
};
verificar();
});
}
Procesamiento en Lotes
Lotes Optimizados
interface LoteConfig {
tamañoLote: number;
concurrencia: number;
delayEntreLotes: number;
reintentos: number;
onProgreso?: (completados: number, total: number) => void;
onError?: (error: Error, imagen: string) => void;
}
async function procesarLote(
imagenes: string[],
operacion: (imagen: string) => Promise<any>,
config: LoteConfig
) {
const resultados = [];
const errores = [];
for (let i = 0; i < imagenes.length; i += config.tamañoLote) {
const lote = imagenes.slice(i, i + config.tamañoLote);
// Procesar lote con concurrencia limitada
const promesasLote = lote.map(async (imagen) => {
for (let intento = 1; intento <= config.reintentos; intento++) {
try {
return await operacion(imagen);
} catch (error) {
if (intento === config.reintentos) {
config.onError?.(error, imagen);
errores.push({ imagen, error });
return null;
}
// Esperar antes del siguiente intento
await new Promise(resolve =>
setTimeout(resolve, intento * 1000)
);
}
}
});
const resultadosLote = await Promise.all(promesasLote);
resultados.push(...resultadosLote.filter(r => r !== null));
// Reportar progreso
config.onProgreso?.(resultados.length, imagenes.length);
// Delay entre lotes para evitar rate limiting
if (i + config.tamañoLote < imagenes.length) {
await new Promise(resolve =>
setTimeout(resolve, config.delayEntreLotes)
);
}
}
return { resultados, errores };
}
// Ejemplo de uso
const imagenes = ['img1.jpg', 'img2.jpg', /* ... */];
const { resultados, errores } = await procesarLote(
imagenes,
(imagen) => editor.editText({
imagen,
prompt: 'Mejorar calidad'
}),
{
tamañoLote: 10,
concurrencia: 5,
delayEntreLotes: 2000,
reintentos: 3,
onProgreso: (completados, total) => {
console.log(`Progreso: ${completados}/${total} (${Math.round(completados/total*100)}%)`);
},
onError: (error, imagen) => {
console.error(`Error procesando ${imagen}:`, error.message);
}
}
);
Procesamiento Paralelo Inteligente
import pLimit from 'p-limit';
import pRetry from 'p-retry';
class ProcesadorInteligente {
private limiteConcurrencia: ReturnType<typeof pLimit>;
private estadisticas = {
procesados: 0,
errores: 0,
tiempoPromedio: 0
};
constructor(private editor: QwenImageEdit, concurrencia = 5) {
this.limiteConcurrencia = pLimit(concurrencia);
}
async procesarImagenes(imagenes: string[], operacion: any) {
const inicioTiempo = Date.now();
const promesas = imagenes.map((imagen, indice) =>
this.limiteConcurrencia(async () => {
const inicioImagen = Date.now();
try {
const resultado = await pRetry(
() => this.ejecutarOperacion(imagen, operacion),
{
retries: 3,
factor: 2,
minTimeout: 1000,
maxTimeout: 10000,
onFailedAttempt: (error) => {
console.warn(`Intento ${error.attemptNumber} falló para ${imagen}:`, error.message);
}
}
);
this.estadisticas.procesados++;
const tiempoImagen = Date.now() - inicioImagen;
this.actualizarTiempoPromedio(tiempoImagen);
console.log(`✅ ${imagen} procesada en ${tiempoImagen}ms`);
return { imagen, resultado, indice };
} catch (error) {
this.estadisticas.errores++;
console.error(`❌ Error procesando ${imagen}:`, error.message);
return { imagen, error, indice };
}
})
);
const resultados = await Promise.all(promesas);
const tiempoTotal = Date.now() - inicioTiempo;
console.log('📊 Estadísticas de procesamiento:', {
...this.estadisticas,
tiempoTotal: `${tiempoTotal}ms`,
velocidadPromedio: `${Math.round(imagenes.length / (tiempoTotal / 1000))} img/s`
});
return resultados;
}
private async ejecutarOperacion(imagen: string, operacion: any) {
// Lógica específica de la operación
return await this.editor.editText({
imagen,
...operacion
});
}
private actualizarTiempoPromedio(nuevoTiempo: number) {
const total = this.estadisticas.procesados;
this.estadisticas.tiempoPromedio =
(this.estadisticas.tiempoPromedio * (total - 1) + nuevoTiempo) / total;
}
}
Análisis Avanzado de Imágenes
Análisis Profundo
async function analisisCompleto(imagen: string) {
const analisis = await editor.analyzeImage({
imagen,
detectar: ['texto', 'objetos', 'rostros', 'colores', 'estilo', 'calidad'],
opciones: {
profundidad: 'alta',
incluirMetadatos: true,
detectarEmociones: true,
analizarComposicion: true
}
});
return {
// Análisis de texto
texto: analisis.texto?.map(t => ({
contenido: t.contenido,
idioma: t.idioma,
confianza: t.confianza,
posicion: t.posicion,
estilo: {
fuente: t.fuente,
tamaño: t.tamaño,
color: t.color,
negrita: t.negrita,
cursiva: t.cursiva
}
})),
// Análisis de objetos
objetos: analisis.objetos?.map(o => ({
nombre: o.nombre,
categoria: o.categoria,
confianza: o.confianza,
posicion: o.posicion,
atributos: o.atributos
})),
// Análisis de rostros
rostros: analisis.rostros?.map(r => ({
posicion: r.posicion,
edad: r.edadEstimada,
genero: r.generoEstimado,
emociones: r.emociones,
puntosClave: r.puntosClave
})),
// Análisis de colores
colores: {
dominantes: analisis.colores?.dominantes,
paleta: analisis.colores?.paleta,
temperatura: analisis.colores?.temperatura,
saturacion: analisis.colores?.saturacion
},
// Análisis de estilo
estilo: {
categoria: analisis.estilo?.categoria,
epoca: analisis.estilo?.epoca,
tecnica: analisis.estilo?.tecnica,
mood: analisis.estilo?.mood
},
// Análisis de calidad
calidad: {
puntuacion: analisis.calidad?.puntuacion,
nitidez: analisis.calidad?.nitidez,
exposicion: analisis.calidad?.exposicion,
ruido: analisis.calidad?.ruido,
compresion: analisis.calidad?.compresion
}
};
}
Detección de Contenido Sensible
async function verificarContenido(imagen: string) {
const verificacion = await editor.moderateContent({
imagen,
categorias: [
'adulto',
'violencia',
'drogas',
'odio',
'spam'
],
umbral: 0.7
});
const esSeguro = verificacion.categorias.every(c => c.puntuacion < 0.7);
if (!esSeguro) {
console.warn('⚠️ Contenido potencialmente sensible detectado:',
verificacion.categorias.filter(c => c.puntuacion >= 0.7)
);
}
return {
esSeguro,
categorias: verificacion.categorias,
recomendaciones: verificacion.recomendaciones
};
}
Edición Condicional
Edición Basada en Condiciones
interface CondicionEdicion {
tipo: 'texto' | 'objeto' | 'color' | 'region';
condicion: any;
accion: any;
}
async function editarCondicional(
imagen: string,
condiciones: CondicionEdicion[]
) {
// Primero analizar la imagen
const analisis = await editor.analyzeImage({
imagen,
detectar: ['texto', 'objetos', 'colores']
});
const edicionesAplicar = [];
for (const condicion of condiciones) {
switch (condicion.tipo) {
case 'texto':
const textoEncontrado = analisis.texto?.find(t =>
t.contenido.includes(condicion.condicion.buscar)
);
if (textoEncontrado) {
edicionesAplicar.push({
tipo: 'editText',
opciones: {
...condicion.accion,
posicion: textoEncontrado.posicion
}
});
}
break;
case 'objeto':
const objetoEncontrado = analisis.objetos?.find(o =>
o.nombre === condicion.condicion.nombre
);
if (objetoEncontrado) {
edicionesAplicar.push({
tipo: 'editElement',
opciones: {
...condicion.accion,
posicion: objetoEncontrado.posicion
}
});
}
break;
case 'color':
const colorDominante = analisis.colores?.dominantes[0];
if (colorDominante &&
this.colorSimilar(colorDominante, condicion.condicion.color)) {
edicionesAplicar.push({
tipo: 'adjustColors',
opciones: condicion.accion
});
}
break;
}
}
// Aplicar ediciones en secuencia
let imagenActual = imagen;
const resultados = [];
for (const edicion of edicionesAplicar) {
const resultado = await this.aplicarEdicion(imagenActual, edicion);
resultados.push(resultado);
imagenActual = resultado.imagenUrl;
}
return {
imagenFinal: imagenActual,
edicionesAplicadas: resultados.length,
detalles: resultados
};
}
// Ejemplo de uso
const resultado = await editarCondicional('imagen.jpg', [
{
tipo: 'texto',
condicion: { buscar: 'SALE' },
accion: {
prompt: 'Cambiar "SALE" por "OFERTA"',
color: '#ff0000'
}
},
{
tipo: 'objeto',
condicion: { nombre: 'persona' },
accion: {
prompt: 'Difuminar rostro para privacidad',
intensidad: 0.8
}
}
]);
Pipelines de Procesamiento
Pipeline Configurable
interface PasosPipeline {
nombre: string;
operacion: string;
parametros: any;
condicion?: (resultado: any) => boolean;
onError?: 'continuar' | 'detener' | 'reintentar';
}
class PipelineProcesamiento {
constructor(private editor: QwenImageEdit) {}
async ejecutar(imagen: string, pasos: PasosPipeline[]) {
let imagenActual = imagen;
const resultados = [];
for (const [indice, paso] of pasos.entries()) {
try {
console.log(`🔄 Ejecutando paso ${indice + 1}: ${paso.nombre}`);
const resultado = await this.ejecutarPaso(imagenActual, paso);
// Verificar condición si existe
if (paso.condicion && !paso.condicion(resultado)) {
console.log(`⏭️ Saltando paso ${paso.nombre} - condición no cumplida`);
continue;
}
resultados.push({
paso: paso.nombre,
resultado,
timestamp: new Date().toISOString()
});
imagenActual = resultado.imagenUrl || imagenActual;
} catch (error) {
console.error(`❌ Error en paso ${paso.nombre}:`, error.message);
switch (paso.onError) {
case 'continuar':
console.log('⏭️ Continuando con siguiente paso');
break;
case 'detener':
throw error;
case 'reintentar':
console.log('🔄 Reintentando paso');
// Implementar lógica de reintento
break;
}
}
}
return {
imagenFinal: imagenActual,
pasos: resultados,
duracionTotal: this.calcularDuracion(resultados)
};
}
private async ejecutarPaso(imagen: string, paso: PasosPipeline) {
switch (paso.operacion) {
case 'editText':
return await this.editor.editText({ imagen, ...paso.parametros });
case 'editElement':
return await this.editor.editElement({ imagen, ...paso.parametros });
case 'transferStyle':
return await this.editor.transferStyle({ imagen, ...paso.parametros });
case 'enhance':
return await this.editor.enhanceImage({ imagen, ...paso.parametros });
case 'resize':
return await this.editor.resizeImage({ imagen, ...paso.parametros });
default:
throw new Error(`Operación no soportada: ${paso.operacion}`);
}
}
private calcularDuracion(resultados: any[]) {
if (resultados.length < 2) return 0;
const inicio = new Date(resultados[0].timestamp).getTime();
const fin = new Date(resultados[resultados.length - 1].timestamp).getTime();
return fin - inicio;
}
}
// Ejemplo de pipeline
const pipeline = new PipelineProcesamiento(editor);
const resultado = await pipeline.ejecutar('imagen.jpg', [
{
nombre: 'Análisis inicial',
operacion: 'analyze',
parametros: { detectar: ['calidad'] },
condicion: (r) => r.calidad.puntuacion > 0.5
},
{
nombre: 'Mejora de calidad',
operacion: 'enhance',
parametros: { mejoras: ['nitidez', 'contraste'] },
onError: 'continuar'
},
{
nombre: 'Edición de texto',
operacion: 'editText',
parametros: { prompt: 'Mejorar legibilidad del texto' },
onError: 'reintentar'
},
{
nombre: 'Optimización final',
operacion: 'resize',
parametros: {
dimensiones: { ancho: 1920 },
calidad: 90
}
}
]);
Integración con IA Externa
Combinación con GPT para Prompts Inteligentes
import OpenAI from 'openai';
class GeneradorPromptsIA {
constructor(
private openai: OpenAI,
private editor: QwenImageEdit
) {}
async generarPromptOptimo(imagen: string, objetivo: string) {
// Analizar imagen primero
const analisis = await this.editor.analyzeImage({
imagen,
detectar: ['texto', 'objetos', 'estilo']
});
// Generar prompt con GPT
const contexto = `
Imagen analizada:
- Texto detectado: ${analisis.texto?.map(t => t.contenido).join(', ')}
- Objetos: ${analisis.objetos?.map(o => o.nombre).join(', ')}
- Estilo: ${analisis.estilo?.categoria}
Objetivo: ${objetivo}
Genera un prompt específico y detallado para editar esta imagen.
`;
const respuesta = await this.openai.chat.completions.create({
model: 'gpt-4',
messages: [{
role: 'user',
content: contexto
}],
max_tokens: 200
});
return respuesta.choices[0].message.content;
}
async editarConIA(imagen: string, objetivo: string) {
const promptOptimo = await this.generarPromptOptimo(imagen, objetivo);
console.log('🤖 Prompt generado por IA:', promptOptimo);
return await this.editor.editText({
imagen,
prompt: promptOptimo
});
}
}
Optimización de Performance
Caché Inteligente
import Redis from 'ioredis';
import crypto from 'crypto';
class CacheInteligente {
private redis: Redis;
constructor(redisConfig: any) {
this.redis = new Redis(redisConfig);
}
private generarClave(operacion: string, parametros: any): string {
const hash = crypto.createHash('sha256');
hash.update(operacion + JSON.stringify(parametros));
return `qwen:${hash.digest('hex')}`;
}
async obtener(operacion: string, parametros: any) {
const clave = this.generarClave(operacion, parametros);
const resultado = await this.redis.get(clave);
if (resultado) {
console.log('📦 Resultado desde caché');
return JSON.parse(resultado);
}
return null;
}
async guardar(
operacion: string,
parametros: any,
resultado: any,
ttl = 3600
) {
const clave = this.generarClave(operacion, parametros);
await this.redis.setex(clave, ttl, JSON.stringify(resultado));
}
async invalidar(patron: string) {
const claves = await this.redis.keys(`qwen:${patron}*`);
if (claves.length > 0) {
await this.redis.del(...claves);
}
}
}
// Wrapper con caché
class QwenConCache {
constructor(
private editor: QwenImageEdit,
private cache: CacheInteligente
) {}
async editText(opciones: any) {
const cacheado = await this.cache.obtener('editText', opciones);
if (cacheado) return cacheado;
const resultado = await this.editor.editText(opciones);
await this.cache.guardar('editText', opciones, resultado);
return resultado;
}
}
Monitoreo y Métricas
Sistema de Métricas Completo
interface Metrica {
nombre: string;
valor: number;
timestamp: number;
etiquetas?: Record<string, string>;
}
class MonitorQwen {
private metricas: Metrica[] = [];
constructor(private editor: QwenImageEdit) {
this.configurarInterceptores();
}
private configurarInterceptores() {
const originalEditText = this.editor.editText.bind(this.editor);
this.editor.editText = async (opciones: any) => {
const inicio = Date.now();
try {
const resultado = await originalEditText(opciones);
this.registrarMetrica('qwen.edit_text.duracion', Date.now() - inicio, {
estado: 'exito',
tamaño_imagen: this.obtenerTamañoImagen(opciones.imagen)
});
this.registrarMetrica('qwen.edit_text.total', 1, {
estado: 'exito'
});
return resultado;
} catch (error) {
this.registrarMetrica('qwen.edit_text.duracion', Date.now() - inicio, {
estado: 'error',
tipo_error: error.codigo
});
this.registrarMetrica('qwen.edit_text.total', 1, {
estado: 'error'
});
throw error;
}
};
}
private registrarMetrica(
nombre: string,
valor: number,
etiquetas?: Record<string, string>
) {
this.metricas.push({
nombre,
valor,
timestamp: Date.now(),
etiquetas
});
// Limpiar métricas antiguas
const hace24h = Date.now() - (24 * 60 * 60 * 1000);
this.metricas = this.metricas.filter(m => m.timestamp > hace24h);
}
obtenerEstadisticas() {
const ahora = Date.now();
const hace1h = ahora - (60 * 60 * 1000);
const metricasRecientes = this.metricas.filter(m => m.timestamp > hace1h);
return {
solicitudesUltimaHora: metricasRecientes.filter(m =>
m.nombre === 'qwen.edit_text.total'
).length,
tasaExito: this.calcularTasaExito(metricasRecientes),
tiempoPromedioMs: this.calcularTiempoPromedio(metricasRecientes),
erroresPorTipo: this.agruparErrores(metricasRecientes)
};
}
private calcularTasaExito(metricas: Metrica[]): number {
const solicitudes = metricas.filter(m => m.nombre === 'qwen.edit_text.total');
const exitosas = solicitudes.filter(m => m.etiquetas?.estado === 'exito');
return solicitudes.length > 0 ? exitosas.length / solicitudes.length : 0;
}
private calcularTiempoPromedio(metricas: Metrica[]): number {
const duraciones = metricas.filter(m => m.nombre === 'qwen.edit_text.duracion');
if (duraciones.length === 0) return 0;
const suma = duraciones.reduce((acc, m) => acc + m.valor, 0);
return suma / duraciones.length;
}
private agruparErrores(metricas: Metrica[]): Record<string, number> {
const errores = metricas.filter(m =>
m.nombre === 'qwen.edit_text.total' &&
m.etiquetas?.estado === 'error'
);
return errores.reduce((acc, m) => {
const tipo = m.etiquetas?.tipo_error || 'desconocido';
acc[tipo] = (acc[tipo] || 0) + 1;
return acc;
}, {} as Record<string, number>);
}
}
Estas funciones avanzadas te permiten aprovechar al máximo Qwen Image Edit para casos de uso complejos y aplicaciones de producción robustas.