Files
reformix-hackaton/mvp/image-worker/src/pipeline/pipeline.service.ts
Carlos Narro ad87e45892 Lleva las preferencias del cliente (estilo, color, material) al render
Hasta ahora el render solo se condicionaba con tipo/m²/calidad + notas de texto
libre por zona; lo que el cliente decía hablando con Luisa o en la llamada
(estilo, colores, materiales) se guardaba en estilo/tasteText pero NO viajaba al
generador de imagen, así que el render no lo representaba.

- b2c (perfil.ts): el payload de PERFIL_WEBHOOK_URL incluye ahora
  preferencias:{estilo, gustos} (gustos = tasteText). Claves vacías se omiten.
- worker (webhook.dto): nuevo PreferenciasDto opcional.
- worker (prompt-builder): construirUserContent (función pura) inyecta el estilo
  y los gustos del cliente como bloque dedicado y omite el "modern" por defecto
  cuando hay preferencias; el system prompt prioriza colores/materiales del
  cliente sobre un estilo genérico.
- worker (pipeline): enhebra preferencias hasta generarPrompt.
- worker (sandbox): acepta estilo/gustos para poder probarlos.
- docs/arquitectura-integracion: documenta el campo preferencias.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 18:17:45 +02:00

116 lines
4.0 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, 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<ZonaRender> {
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 };
}
}