Files
reformix-hackaton/mvp/image-worker/src/pipeline/pipeline.service.ts
2026-06-07 18:11:44 -04:00

115 lines
3.9 KiB
TypeScript

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<number>('MAX_RETRIES', 2);
this.minScore = this.config.get<number>('SUPERVISOR_MIN_SCORE', 70);
}
async procesarLead(dto: PerfilCompletoDto): Promise<void> {
const { leadId, reforma, zonas } = 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]);
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,
): Promise<ZonaRender> {
const prompt = await this.promptBuilder.generarPrompt(reforma.tipo, reforma.m2Suelo, reforma.calidad, notas);
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 };
}
}