import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { PerfilCompletoDto } from '../webhook/webhook.dto'; import { PromptBuilderService } from './prompt-builder.service'; import { ImageGeneratorService } from './image-generator.service'; import { SupervisorService } from './supervisor.service'; import { ReformixService } from '../reformix/reformix.service'; interface ZonaRender { zona: string; imagen: string; score: number; aprobada: boolean; } @Injectable() export class PipelineService { private readonly logger = new Logger(PipelineService.name); private readonly maxRetries: number; private readonly minScore: number; constructor( private readonly config: ConfigService, private readonly promptBuilder: PromptBuilderService, private readonly imageGenerator: ImageGeneratorService, private readonly supervisor: SupervisorService, private readonly reformix: ReformixService, ) { this.maxRetries = this.config.get('MAX_RETRIES', 2); this.minScore = this.config.get('SUPERVISOR_MIN_SCORE', 70); } async procesarLead(dto: PerfilCompletoDto): Promise { const { leadId, reforma, zonas, preferencias } = dto; const zonasConFotos = zonas.filter((z) => z.fotos.antes.length > 0); const zonasSaltadas = zonas.filter((z) => z.fotos.antes.length === 0); this.logger.log(`[${leadId}] Iniciando pipeline para ${zonasConFotos.length} zonas`); for (const z of zonasSaltadas) { this.logger.log(`[${leadId}] Zona ${z.zona}: sin fotos "antes", saltando`); } const renders: ZonaRender[] = []; for (const zona of zonasConFotos) { try { const render = await this.procesarZona(leadId, zona.zona, reforma, zona.notas, zona.fotos.antes[0], preferencias); renders.push(render); } catch (err: any) { this.logger.error(`[${leadId}] Zona ${zona.zona}: error fatal: ${err.message}`); } } if (renders.length === 0) { this.logger.warn(`[${leadId}] No se generaron renders para ninguna zona`); return; } const items = renders.map((r) => ({ zona: r.zona, imagen: r.imagen, })); const ok = await this.reformix.entregarRenders(leadId, items); if (ok) { this.logger.log(`[${leadId}] Renders entregados correctamente (${renders.length} zonas)`); } else { this.logger.error(`[${leadId}] Error entregando renders a la app principal`); } } private async procesarZona( leadId: string, zona: string, reforma: PerfilCompletoDto['reforma'], notas: string[], fotoAntes: string, preferencias?: PerfilCompletoDto['preferencias'], ): Promise { const prompt = await this.promptBuilder.generarPrompt(reforma.tipo, reforma.m2Suelo, reforma.calidad, notas, preferencias); this.logger.log(`[${leadId}] Zona ${zona}: prompt generado`); let ultimaImagen: string | null = null; for (let intento = 0; intento <= this.maxRetries; intento++) { if (intento > 0) { this.logger.log(`[${leadId}] Zona ${zona}: reintento ${intento} de ${this.maxRetries}`); } const imagen = await this.imageGenerator.generarRender(prompt, fotoAntes); ultimaImagen = imagen; this.logger.log(`[${leadId}] Zona ${zona}: imagen generada`); const resultado = await this.supervisor.supervisar( reforma.tipo, reforma.m2Suelo, reforma.calidad, notas, fotoAntes, imagen, ); const aprobada = resultado.aprobado && resultado.score >= this.minScore; this.logger.log(`[${leadId}] Zona ${zona}: ${aprobada ? 'aprobada' : 'rechazada'} (score: ${resultado.score}) - ${resultado.motivo}`); if (aprobada) { return { zona, imagen, score: resultado.score, aprobada: true }; } } this.logger.warn(`[${leadId}] Zona ${zona}: usando ultimo render pese a no superar validacion`); return { zona, imagen: ultimaImagen!, score: 0, aprobada: false }; } }