115 lines
3.9 KiB
TypeScript
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 };
|
|
}
|
|
}
|