Proeycto de images-worker creado
This commit is contained in:
16
mvp/image-worker/.env.example
Normal file
16
mvp/image-worker/.env.example
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
PORT=3001
|
||||||
|
|
||||||
|
# OpenRouter (unica API key para todos los modelos)
|
||||||
|
OPENROUTER_API_KEY=sk-or-...
|
||||||
|
# Modelo para prompt-builder y supervisor (texto + vision)
|
||||||
|
OPENROUTER_MODEL_TEXTO=anthropic/claude-3.5-haiku-20241022
|
||||||
|
# Modelo para generacion de imagenes
|
||||||
|
OPENROUTER_MODEL_IMAGEN=google/gemini-2.0-flash-exp-image-generation
|
||||||
|
|
||||||
|
# App principal Reformix
|
||||||
|
REFORMIX_API_URL=https://reformix.dv3.com.es
|
||||||
|
FUNNEL_API_KEY=...
|
||||||
|
|
||||||
|
# Comportamiento del supervisor
|
||||||
|
MAX_RETRIES=2
|
||||||
|
SUPERVISOR_MIN_SCORE=70
|
||||||
85
mvp/image-worker/README.md
Normal file
85
mvp/image-worker/README.md
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# Reformix Image Worker
|
||||||
|
|
||||||
|
Worker de generación de renders fotorrealistas "después" para Reformix. Recibe el perfil completo de un lead desde la app principal, genera imágenes del resultado de la reforma usando IA, las valida y las entrega de vuelta a la app.
|
||||||
|
|
||||||
|
## Rol en el sistema
|
||||||
|
|
||||||
|
```
|
||||||
|
App Reformix
|
||||||
|
│ POST /perfil-completo (webhook)
|
||||||
|
▼
|
||||||
|
Render Worker ← este proyecto
|
||||||
|
│
|
||||||
|
├── Etapa 1: Claude Haiku 4.5 via OpenRouter genera prompt técnico en inglés
|
||||||
|
├── Etapa 2: Gemini 2.0 Flash via OpenRouter genera imagen
|
||||||
|
└── Etapa 3: Claude Haiku 4.5 Vision via OpenRouter valida coherencia
|
||||||
|
│
|
||||||
|
│ POST /api/leads/:id/ingesta (con finalizar:true)
|
||||||
|
▼
|
||||||
|
App Reformix → PDF → Email → WhatsApp → Cliente
|
||||||
|
```
|
||||||
|
|
||||||
|
## Requisitos
|
||||||
|
|
||||||
|
- Node.js 18+
|
||||||
|
- API key de OpenRouter (unica clave para todos los modelos)
|
||||||
|
|
||||||
|
## Instalación
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd mvp/image-worker
|
||||||
|
npm install
|
||||||
|
cp .env.example .env
|
||||||
|
# Editar .env con tu OPENROUTER_API_KEY
|
||||||
|
npm run start:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Variables de entorno
|
||||||
|
|
||||||
|
| Variable | Descripción | Default |
|
||||||
|
|----------|------------|---------|
|
||||||
|
| `PORT` | Puerto del servidor HTTP | `3001` |
|
||||||
|
| `OPENROUTER_API_KEY` | API key de OpenRouter (unica para todos los modelos) | — |
|
||||||
|
| `OPENROUTER_MODEL_TEXTO` | Modelo para prompt-builder y supervisor | `anthropic/claude-3.5-haiku-20241022` |
|
||||||
|
| `OPENROUTER_MODEL_IMAGEN` | Modelo para generación de imágenes | `google/gemini-2.0-flash-exp-image-generation` |
|
||||||
|
| `REFORMIX_API_URL` | URL base de la app Reformix | `http://localhost:3000` |
|
||||||
|
| `FUNNEL_API_KEY` | API key compartida con la app Reformix | — |
|
||||||
|
| `MAX_RETRIES` | Reintentos por zona si el supervisor rechaza | `2` |
|
||||||
|
| `SUPERVISOR_MIN_SCORE` | Score mínimo para aprobar un render (0-100) | `70` |
|
||||||
|
|
||||||
|
## Conexión con la app Reformix
|
||||||
|
|
||||||
|
En el `.env` de la app Reformix (`mvp/b2c/`), configurar:
|
||||||
|
|
||||||
|
```env
|
||||||
|
PERFIL_WEBHOOK_URL=http://localhost:3001/perfil-completo
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pipeline de 3 etapas (todo via OpenRouter)
|
||||||
|
|
||||||
|
### Etapa 1 — Prompt Builder (Claude Haiku 4.5 via OpenRouter)
|
||||||
|
Genera un prompt técnico detallado en inglés para el modelo de image-to-image. Incluye materiales, iluminación, paleta de colores, estilo y palabras clave de render arquitectónico.
|
||||||
|
|
||||||
|
**Modelo:** `anthropic/claude-3.5-haiku-20241022`
|
||||||
|
**Prompt base:** `prompts/prompt-builder.txt`
|
||||||
|
|
||||||
|
### Etapa 2 — Image Generator (Gemini 2.0 Flash via OpenRouter)
|
||||||
|
Toma el prompt generado + la foto "antes" del cliente y produce el render "después". Maneja rate limiting (429) con reintento automático.
|
||||||
|
|
||||||
|
**Modelo:** `google/gemini-2.0-flash-exp-image-generation`
|
||||||
|
|
||||||
|
### Etapa 3 — Supervisor (Claude Haiku 4.5 Vision via OpenRouter)
|
||||||
|
Compara la foto "antes" con el render "después" y evalúa coherencia: estilo, materiales, calidad, artefactos. Devuelve score 0-100. Si el score es menor a `SUPERVISOR_MIN_SCORE`, reintenta desde la Etapa 2 (máximo `MAX_RETRIES` veces).
|
||||||
|
|
||||||
|
**Modelo:** `anthropic/claude-3.5-haiku-20241022`
|
||||||
|
|
||||||
|
## Skills
|
||||||
|
|
||||||
|
| Archivo | Descripción |
|
||||||
|
|---------|-------------|
|
||||||
|
| `skills/pipeline.md` | Orquestación del pipeline de 3 etapas |
|
||||||
|
| `skills/webhook.md` | Contrato del webhook entrante `/perfil-completo` |
|
||||||
|
| `skills/prompt-builder.md` | Llamada a OpenRouter para generar prompts |
|
||||||
|
| `skills/image-generator.md` | Llamada a OpenRouter para generación de imágenes |
|
||||||
|
| `skills/supervisor.md` | Validación de calidad con Claude Haiku Vision via OpenRouter |
|
||||||
|
| `skills/reformix-api.md` | Entrega de renders al endpoint `/ingesta` de la app |
|
||||||
28
mvp/image-worker/dist/app.module.js
vendored
Normal file
28
mvp/image-worker/dist/app.module.js
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"use strict";
|
||||||
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
||||||
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
||||||
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
||||||
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
||||||
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.AppModule = void 0;
|
||||||
|
const common_1 = require("@nestjs/common");
|
||||||
|
const config_1 = require("@nestjs/config");
|
||||||
|
const webhook_module_1 = require("./webhook/webhook.module");
|
||||||
|
const pipeline_module_1 = require("./pipeline/pipeline.module");
|
||||||
|
const reformix_module_1 = require("./reformix/reformix.module");
|
||||||
|
let AppModule = class AppModule {
|
||||||
|
};
|
||||||
|
exports.AppModule = AppModule;
|
||||||
|
exports.AppModule = AppModule = __decorate([
|
||||||
|
(0, common_1.Module)({
|
||||||
|
imports: [
|
||||||
|
config_1.ConfigModule.forRoot({ isGlobal: true }),
|
||||||
|
webhook_module_1.WebhookModule,
|
||||||
|
pipeline_module_1.PipelineModule,
|
||||||
|
reformix_module_1.ReformixModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
], AppModule);
|
||||||
|
//# sourceMappingURL=app.module.js.map
|
||||||
1
mvp/image-worker/dist/app.module.js.map
vendored
Normal file
1
mvp/image-worker/dist/app.module.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"app.module.js","sourceRoot":"","sources":["../src/app.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,2CAA8C;AAC9C,6DAAyD;AACzD,gEAA4D;AAC5D,gEAA4D;AAUrD,IAAM,SAAS,GAAf,MAAM,SAAS;CAAG,CAAA;AAAZ,8BAAS;oBAAT,SAAS;IARrB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE;YACP,qBAAY,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;YACxC,8BAAa;YACb,gCAAc;YACd,gCAAc;SACf;KACF,CAAC;GACW,SAAS,CAAG"}
|
||||||
19
mvp/image-worker/dist/main.js
vendored
Normal file
19
mvp/image-worker/dist/main.js
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
require("reflect-metadata");
|
||||||
|
const core_1 = require("@nestjs/core");
|
||||||
|
const common_1 = require("@nestjs/common");
|
||||||
|
const config_1 = require("@nestjs/config");
|
||||||
|
const app_module_1 = require("./app.module");
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await core_1.NestFactory.create(app_module_1.AppModule, {
|
||||||
|
logger: ['error', 'warn', 'log', 'debug'],
|
||||||
|
});
|
||||||
|
app.useGlobalPipes(new common_1.ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true }));
|
||||||
|
const config = app.get(config_1.ConfigService);
|
||||||
|
const port = config.get('PORT', 3001);
|
||||||
|
await app.listen(port);
|
||||||
|
console.log(`[Reformix Image Worker] corriendo en puerto ${port}`);
|
||||||
|
}
|
||||||
|
bootstrap();
|
||||||
|
//# sourceMappingURL=main.js.map
|
||||||
1
mvp/image-worker/dist/main.js.map
vendored
Normal file
1
mvp/image-worker/dist/main.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";;AAAA,4BAA0B;AAC1B,uCAA2C;AAC3C,2CAAgD;AAChD,2CAA+C;AAC/C,6CAAyC;AAEzC,KAAK,UAAU,SAAS;IACtB,MAAM,GAAG,GAAG,MAAM,kBAAW,CAAC,MAAM,CAAC,sBAAS,EAAE;QAC9C,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC;KAC1C,CAAC,CAAC;IAEH,GAAG,CAAC,cAAc,CAAC,IAAI,uBAAc,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,oBAAoB,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IAEzG,MAAM,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC,sBAAa,CAAC,CAAC;IACtC,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACtC,MAAM,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACvB,OAAO,CAAC,GAAG,CAAC,+CAA+C,IAAI,EAAE,CAAC,CAAC;AACrE,CAAC;AAED,SAAS,EAAE,CAAC"}
|
||||||
94
mvp/image-worker/dist/pipeline/image-generator.service.js
vendored
Normal file
94
mvp/image-worker/dist/pipeline/image-generator.service.js
vendored
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
"use strict";
|
||||||
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
||||||
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
||||||
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
||||||
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
||||||
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
||||||
|
};
|
||||||
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
||||||
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
||||||
|
};
|
||||||
|
var ImageGeneratorService_1;
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.ImageGeneratorService = void 0;
|
||||||
|
const common_1 = require("@nestjs/common");
|
||||||
|
const config_1 = require("@nestjs/config");
|
||||||
|
const axios_1 = require("axios");
|
||||||
|
const OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
||||||
|
let ImageGeneratorService = ImageGeneratorService_1 = class ImageGeneratorService {
|
||||||
|
constructor(config) {
|
||||||
|
this.config = config;
|
||||||
|
this.logger = new common_1.Logger(ImageGeneratorService_1.name);
|
||||||
|
}
|
||||||
|
async generarRender(prompt, fotoAntesDataUri) {
|
||||||
|
const apiKey = this.config.get('OPENROUTER_API_KEY');
|
||||||
|
const model = this.config.get('OPENROUTER_MODEL_IMAGEN', 'google/gemini-2.0-flash-exp-image-generation');
|
||||||
|
const intentosRateLimit = 1;
|
||||||
|
for (let attempt = 0; attempt <= intentosRateLimit; attempt++) {
|
||||||
|
try {
|
||||||
|
const response = await axios_1.default.post(OPENROUTER_URL, {
|
||||||
|
model,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: [
|
||||||
|
{ type: 'text', text: prompt },
|
||||||
|
{ type: 'image_url', image_url: { url: fotoAntesDataUri } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'HTTP-Referer': 'https://reformix.es',
|
||||||
|
'X-Title': 'Reformix Image Worker',
|
||||||
|
},
|
||||||
|
timeout: 60000,
|
||||||
|
});
|
||||||
|
const content = response.data.choices?.[0]?.message?.content;
|
||||||
|
if (!content)
|
||||||
|
throw new Error('OpenRouter no devolvio contenido');
|
||||||
|
const imagen = this.extraerImagenDeRespuesta(content, response.data);
|
||||||
|
if (!imagen)
|
||||||
|
throw new Error('No se pudo extraer imagen de la respuesta');
|
||||||
|
return imagen;
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
if (err.response?.status === 429 && attempt < intentosRateLimit) {
|
||||||
|
this.logger.warn('Rate limit (429), esperando 5s y reintentando...');
|
||||||
|
await new Promise((r) => setTimeout(r, 5000));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('Fallaron todos los intentos de generacion de imagen');
|
||||||
|
}
|
||||||
|
extraerImagenDeRespuesta(content, rawResponse) {
|
||||||
|
if (content.startsWith('data:image'))
|
||||||
|
return content;
|
||||||
|
const dataUriMatch = content.match(/data:image\/[a-zA-Z]+;base64,[^\s"']+/);
|
||||||
|
if (dataUriMatch)
|
||||||
|
return dataUriMatch[0];
|
||||||
|
const urlMatch = content.match(/https?:\/\/[^\s"'()]+\.(png|jpg|jpeg|webp)/i);
|
||||||
|
if (urlMatch)
|
||||||
|
return urlMatch[0];
|
||||||
|
const parts = rawResponse?.choices?.[0]?.message?.content;
|
||||||
|
if (Array.isArray(parts)) {
|
||||||
|
for (const part of parts) {
|
||||||
|
if (part.type === 'image_url' && part.image_url?.url)
|
||||||
|
return part.image_url.url;
|
||||||
|
if (part.image_url?.url?.startsWith('data:image'))
|
||||||
|
return part.image_url.url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
exports.ImageGeneratorService = ImageGeneratorService;
|
||||||
|
exports.ImageGeneratorService = ImageGeneratorService = ImageGeneratorService_1 = __decorate([
|
||||||
|
(0, common_1.Injectable)(),
|
||||||
|
__metadata("design:paramtypes", [config_1.ConfigService])
|
||||||
|
], ImageGeneratorService);
|
||||||
|
//# sourceMappingURL=image-generator.service.js.map
|
||||||
1
mvp/image-worker/dist/pipeline/image-generator.service.js.map
vendored
Normal file
1
mvp/image-worker/dist/pipeline/image-generator.service.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"image-generator.service.js","sourceRoot":"","sources":["../../src/pipeline/image-generator.service.ts"],"names":[],"mappings":"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"}
|
||||||
26
mvp/image-worker/dist/pipeline/pipeline.module.js
vendored
Normal file
26
mvp/image-worker/dist/pipeline/pipeline.module.js
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
"use strict";
|
||||||
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
||||||
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
||||||
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
||||||
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
||||||
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.PipelineModule = void 0;
|
||||||
|
const common_1 = require("@nestjs/common");
|
||||||
|
const pipeline_service_1 = require("./pipeline.service");
|
||||||
|
const prompt_builder_service_1 = require("./prompt-builder.service");
|
||||||
|
const image_generator_service_1 = require("./image-generator.service");
|
||||||
|
const supervisor_service_1 = require("./supervisor.service");
|
||||||
|
const reformix_module_1 = require("../reformix/reformix.module");
|
||||||
|
let PipelineModule = class PipelineModule {
|
||||||
|
};
|
||||||
|
exports.PipelineModule = PipelineModule;
|
||||||
|
exports.PipelineModule = PipelineModule = __decorate([
|
||||||
|
(0, common_1.Module)({
|
||||||
|
imports: [reformix_module_1.ReformixModule],
|
||||||
|
providers: [pipeline_service_1.PipelineService, prompt_builder_service_1.PromptBuilderService, image_generator_service_1.ImageGeneratorService, supervisor_service_1.SupervisorService],
|
||||||
|
exports: [pipeline_service_1.PipelineService],
|
||||||
|
})
|
||||||
|
], PipelineModule);
|
||||||
|
//# sourceMappingURL=pipeline.module.js.map
|
||||||
1
mvp/image-worker/dist/pipeline/pipeline.module.js.map
vendored
Normal file
1
mvp/image-worker/dist/pipeline/pipeline.module.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"pipeline.module.js","sourceRoot":"","sources":["../../src/pipeline/pipeline.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,yDAAqD;AACrD,qEAAgE;AAChE,uEAAkE;AAClE,6DAAyD;AACzD,iEAA6D;AAOtD,IAAM,cAAc,GAApB,MAAM,cAAc;CAAG,CAAA;AAAjB,wCAAc;yBAAd,cAAc;IAL1B,IAAA,eAAM,EAAC;QACN,OAAO,EAAE,CAAC,gCAAc,CAAC;QACzB,SAAS,EAAE,CAAC,kCAAe,EAAE,6CAAoB,EAAE,+CAAqB,EAAE,sCAAiB,CAAC;QAC5F,OAAO,EAAE,CAAC,kCAAe,CAAC;KAC3B,CAAC;GACW,cAAc,CAAG"}
|
||||||
96
mvp/image-worker/dist/pipeline/pipeline.service.js
vendored
Normal file
96
mvp/image-worker/dist/pipeline/pipeline.service.js
vendored
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
"use strict";
|
||||||
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
||||||
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
||||||
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
||||||
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
||||||
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
||||||
|
};
|
||||||
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
||||||
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
||||||
|
};
|
||||||
|
var PipelineService_1;
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.PipelineService = void 0;
|
||||||
|
const common_1 = require("@nestjs/common");
|
||||||
|
const config_1 = require("@nestjs/config");
|
||||||
|
const prompt_builder_service_1 = require("./prompt-builder.service");
|
||||||
|
const image_generator_service_1 = require("./image-generator.service");
|
||||||
|
const supervisor_service_1 = require("./supervisor.service");
|
||||||
|
const reformix_service_1 = require("../reformix/reformix.service");
|
||||||
|
let PipelineService = PipelineService_1 = class PipelineService {
|
||||||
|
constructor(config, promptBuilder, imageGenerator, supervisor, reformix) {
|
||||||
|
this.config = config;
|
||||||
|
this.promptBuilder = promptBuilder;
|
||||||
|
this.imageGenerator = imageGenerator;
|
||||||
|
this.supervisor = supervisor;
|
||||||
|
this.reformix = reformix;
|
||||||
|
this.logger = new common_1.Logger(PipelineService_1.name);
|
||||||
|
this.maxRetries = this.config.get('MAX_RETRIES', 2);
|
||||||
|
this.minScore = this.config.get('SUPERVISOR_MIN_SCORE', 70);
|
||||||
|
}
|
||||||
|
async procesarLead(dto) {
|
||||||
|
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 = [];
|
||||||
|
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) {
|
||||||
|
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`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async procesarZona(leadId, zona, reforma, notas, fotoAntes) {
|
||||||
|
const prompt = await this.promptBuilder.generarPrompt(reforma.tipo, reforma.m2Suelo, reforma.calidad, notas);
|
||||||
|
this.logger.log(`[${leadId}] Zona ${zona}: prompt generado`);
|
||||||
|
let ultimaImagen = 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 };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
exports.PipelineService = PipelineService;
|
||||||
|
exports.PipelineService = PipelineService = PipelineService_1 = __decorate([
|
||||||
|
(0, common_1.Injectable)(),
|
||||||
|
__metadata("design:paramtypes", [config_1.ConfigService,
|
||||||
|
prompt_builder_service_1.PromptBuilderService,
|
||||||
|
image_generator_service_1.ImageGeneratorService,
|
||||||
|
supervisor_service_1.SupervisorService,
|
||||||
|
reformix_service_1.ReformixService])
|
||||||
|
], PipelineService);
|
||||||
|
//# sourceMappingURL=pipeline.service.js.map
|
||||||
1
mvp/image-worker/dist/pipeline/pipeline.service.js.map
vendored
Normal file
1
mvp/image-worker/dist/pipeline/pipeline.service.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"pipeline.service.js","sourceRoot":"","sources":["../../src/pipeline/pipeline.service.ts"],"names":[],"mappings":"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"}
|
||||||
74
mvp/image-worker/dist/pipeline/prompt-builder.service.js
vendored
Normal file
74
mvp/image-worker/dist/pipeline/prompt-builder.service.js
vendored
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
"use strict";
|
||||||
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
||||||
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
||||||
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
||||||
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
||||||
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
||||||
|
};
|
||||||
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
||||||
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
||||||
|
};
|
||||||
|
var PromptBuilderService_1;
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.PromptBuilderService = void 0;
|
||||||
|
const common_1 = require("@nestjs/common");
|
||||||
|
const config_1 = require("@nestjs/config");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const axios_1 = require("axios");
|
||||||
|
const OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
||||||
|
let PromptBuilderService = PromptBuilderService_1 = class PromptBuilderService {
|
||||||
|
constructor(config) {
|
||||||
|
this.config = config;
|
||||||
|
this.logger = new common_1.Logger(PromptBuilderService_1.name);
|
||||||
|
this.systemPrompt = '';
|
||||||
|
const ruta = path.join(process.cwd(), 'prompts', 'prompt-builder.txt');
|
||||||
|
if (fs.existsSync(ruta)) {
|
||||||
|
this.systemPrompt = fs.readFileSync(ruta, 'utf-8');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.logger.warn('prompts/prompt-builder.txt no encontrado, usando default');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async generarPrompt(tipoReforma, m2Suelo, calidad, notas) {
|
||||||
|
const apiKey = this.config.get('OPENROUTER_API_KEY');
|
||||||
|
const model = this.config.get('OPENROUTER_MODEL_TEXTO', 'anthropic/claude-3.5-haiku-20241022');
|
||||||
|
const userContent = `Generate a render prompt for a ${tipoReforma} renovation.
|
||||||
|
- Area: ${m2Suelo ?? 'unknown'} m²
|
||||||
|
- Quality level: ${calidad}
|
||||||
|
- Client notes: ${notas.join('; ') || 'none'}
|
||||||
|
- Style: modern ${tipoReforma} renovation`;
|
||||||
|
try {
|
||||||
|
const response = await axios_1.default.post(OPENROUTER_URL, {
|
||||||
|
model,
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: this.systemPrompt },
|
||||||
|
{ role: 'user', content: userContent },
|
||||||
|
],
|
||||||
|
max_tokens: 512,
|
||||||
|
temperature: 0.5,
|
||||||
|
}, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'HTTP-Referer': 'https://reformix.es',
|
||||||
|
'X-Title': 'Reformix Image Worker',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const prompt = response.data.choices?.[0]?.message?.content?.trim();
|
||||||
|
if (!prompt)
|
||||||
|
throw new Error('OpenRouter devolvio respuesta vacia');
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
this.logger.error(`Error generando prompt: ${err.message}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
exports.PromptBuilderService = PromptBuilderService;
|
||||||
|
exports.PromptBuilderService = PromptBuilderService = PromptBuilderService_1 = __decorate([
|
||||||
|
(0, common_1.Injectable)(),
|
||||||
|
__metadata("design:paramtypes", [config_1.ConfigService])
|
||||||
|
], PromptBuilderService);
|
||||||
|
//# sourceMappingURL=prompt-builder.service.js.map
|
||||||
1
mvp/image-worker/dist/pipeline/prompt-builder.service.js.map
vendored
Normal file
1
mvp/image-worker/dist/pipeline/prompt-builder.service.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"prompt-builder.service.js","sourceRoot":"","sources":["../../src/pipeline/prompt-builder.service.ts"],"names":[],"mappings":";;;;;;;;;;;;;AAAA,2CAAoD;AACpD,2CAA+C;AAC/C,yBAAyB;AACzB,6BAA6B;AAC7B,iCAA0B;AAE1B,MAAM,cAAc,GAAG,+CAA+C,CAAC;AAGhE,IAAM,oBAAoB,4BAA1B,MAAM,oBAAoB;IAI/B,YAA6B,MAAqB;QAArB,WAAM,GAAN,MAAM,CAAe;QAHjC,WAAM,GAAG,IAAI,eAAM,CAAC,sBAAoB,CAAC,IAAI,CAAC,CAAC;QACxD,iBAAY,GAAG,EAAE,CAAC;QAGxB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,oBAAoB,CAAC,CAAC;QACvE,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YACxB,IAAI,CAAC,YAAY,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QACrD,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,0DAA0D,CAAC,CAAC;QAC/E,CAAC;IACH,CAAC;IAED,KAAK,CAAC,aAAa,CACjB,WAAmB,EACnB,OAAsB,EACtB,OAAe,EACf,KAAe;QAEf,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAS,oBAAoB,CAAC,CAAC;QAC7D,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAS,wBAAwB,EAAE,qCAAqC,CAAC,CAAC;QAEvG,MAAM,WAAW,GAAG,kCAAkC,WAAW;UAC3D,OAAO,IAAI,SAAS;mBACX,OAAO;kBACR,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM;kBAC1B,WAAW,aAAa,CAAC;QAEvC,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,eAAK,CAAC,IAAI,CAC/B,cAAc,EACd;gBACE,KAAK;gBACL,QAAQ,EAAE;oBACR,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC,YAAY,EAAE;oBAC9C,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE;iBACvC;gBACD,UAAU,EAAE,GAAG;gBACf,WAAW,EAAE,GAAG;aACjB,EACD;gBACE,OAAO,EAAE;oBACP,aAAa,EAAE,UAAU,MAAM,EAAE;oBACjC,cAAc,EAAE,kBAAkB;oBAClC,cAAc,EAAE,qBAAqB;oBACrC,SAAS,EAAE,uBAAuB;iBACnC;aACF,CACF,CAAC;YAEF,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;YACpE,IAAI,CAAC,MAAM;gBAAE,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;YACpE,OAAO,MAAM,CAAC;QAChB,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAClB,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,2BAA2B,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;YAC5D,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC;CACF,CAAA;AA1DY,oDAAoB;+BAApB,oBAAoB;IADhC,IAAA,mBAAU,GAAE;qCAK0B,sBAAa;GAJvC,oBAAoB,CA0DhC"}
|
||||||
104
mvp/image-worker/dist/pipeline/supervisor.service.js
vendored
Normal file
104
mvp/image-worker/dist/pipeline/supervisor.service.js
vendored
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
"use strict";
|
||||||
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
||||||
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
||||||
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
||||||
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
||||||
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
||||||
|
};
|
||||||
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
||||||
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
||||||
|
};
|
||||||
|
var SupervisorService_1;
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.SupervisorService = void 0;
|
||||||
|
const common_1 = require("@nestjs/common");
|
||||||
|
const config_1 = require("@nestjs/config");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const axios_1 = require("axios");
|
||||||
|
const OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
||||||
|
let SupervisorService = SupervisorService_1 = class SupervisorService {
|
||||||
|
constructor(config) {
|
||||||
|
this.config = config;
|
||||||
|
this.logger = new common_1.Logger(SupervisorService_1.name);
|
||||||
|
this.systemPrompt = '';
|
||||||
|
const ruta = path.join(process.cwd(), 'prompts', 'supervisor.txt');
|
||||||
|
if (fs.existsSync(ruta)) {
|
||||||
|
this.systemPrompt = fs.readFileSync(ruta, 'utf-8');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.logger.warn('prompts/supervisor.txt no encontrado, usando default');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async supervisar(tipoReforma, m2Suelo, calidad, notas, fotoAntes, renderDespues) {
|
||||||
|
const apiKey = this.config.get('OPENROUTER_API_KEY');
|
||||||
|
const model = this.config.get('OPENROUTER_MODEL_TEXTO', 'anthropic/claude-3.5-haiku-20241022');
|
||||||
|
const notasTexto = notas.join('; ') || 'sin notas';
|
||||||
|
try {
|
||||||
|
const response = await axios_1.default.post(OPENROUTER_URL, {
|
||||||
|
model,
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: this.systemPrompt },
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: `Reforma tipo: ${tipoReforma}\nMetros: ${m2Suelo ?? 'desconocido'}\nCalidad: ${calidad}\nNotas del cliente: ${notasTexto}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'image_url',
|
||||||
|
image_url: { url: fotoAntes },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'image_url',
|
||||||
|
image_url: { url: renderDespues },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
max_tokens: 256,
|
||||||
|
temperature: 0.2,
|
||||||
|
}, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'HTTP-Referer': 'https://reformix.es',
|
||||||
|
'X-Title': 'Reformix Image Worker',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const textContent = response.data.choices?.[0]?.message?.content?.trim();
|
||||||
|
if (!textContent) {
|
||||||
|
return { aprobado: false, score: 0, motivo: 'Modelo devolvio respuesta vacia' };
|
||||||
|
}
|
||||||
|
return this.parsearRespuesta(textContent);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
this.logger.error(`Error en supervisor: ${err.message}`);
|
||||||
|
return { aprobado: false, score: 0, motivo: `Error del supervisor: ${err.message}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parsearRespuesta(texto) {
|
||||||
|
const jsonMatch = texto.match(/\{[^{}]*"aprobado"[^{}]*\}/i);
|
||||||
|
if (!jsonMatch) {
|
||||||
|
return { aprobado: false, score: 0, motivo: 'Error parseando respuesta del supervisor' };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(jsonMatch[0]);
|
||||||
|
return {
|
||||||
|
aprobado: Boolean(parsed.aprobado),
|
||||||
|
score: Math.min(100, Math.max(0, Number(parsed.score) || 0)),
|
||||||
|
motivo: String(parsed.motivo || 'Sin motivo'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return { aprobado: false, score: 0, motivo: 'Error parseando respuesta del supervisor' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
exports.SupervisorService = SupervisorService;
|
||||||
|
exports.SupervisorService = SupervisorService = SupervisorService_1 = __decorate([
|
||||||
|
(0, common_1.Injectable)(),
|
||||||
|
__metadata("design:paramtypes", [config_1.ConfigService])
|
||||||
|
], SupervisorService);
|
||||||
|
//# sourceMappingURL=supervisor.service.js.map
|
||||||
1
mvp/image-worker/dist/pipeline/supervisor.service.js.map
vendored
Normal file
1
mvp/image-worker/dist/pipeline/supervisor.service.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"supervisor.service.js","sourceRoot":"","sources":["../../src/pipeline/supervisor.service.ts"],"names":[],"mappings":"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"}
|
||||||
47
mvp/image-worker/dist/webhook/webhook.controller.js
vendored
Normal file
47
mvp/image-worker/dist/webhook/webhook.controller.js
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
"use strict";
|
||||||
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
||||||
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
||||||
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
||||||
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
||||||
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
||||||
|
};
|
||||||
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
||||||
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
||||||
|
};
|
||||||
|
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
||||||
|
return function (target, key) { decorator(target, key, paramIndex); }
|
||||||
|
};
|
||||||
|
var WebhookController_1;
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.WebhookController = void 0;
|
||||||
|
const common_1 = require("@nestjs/common");
|
||||||
|
const webhook_dto_1 = require("./webhook.dto");
|
||||||
|
const pipeline_service_1 = require("../pipeline/pipeline.service");
|
||||||
|
let WebhookController = WebhookController_1 = class WebhookController {
|
||||||
|
constructor(pipelineService) {
|
||||||
|
this.pipelineService = pipelineService;
|
||||||
|
this.logger = new common_1.Logger(WebhookController_1.name);
|
||||||
|
}
|
||||||
|
recibirPerfil(dto) {
|
||||||
|
this.logger.log(`[${dto.leadId}] Webhook recibido: ${dto.zonas.length} zonas`);
|
||||||
|
setImmediate(() => {
|
||||||
|
this.pipelineService.procesarLead(dto).catch((err) => {
|
||||||
|
this.logger.error(`[${dto.leadId}] Pipeline fallo: ${err.message}`, err.stack);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return { ok: true, message: 'Procesando renders en background...' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
exports.WebhookController = WebhookController;
|
||||||
|
__decorate([
|
||||||
|
(0, common_1.Post)('perfil-completo'),
|
||||||
|
__param(0, (0, common_1.Body)()),
|
||||||
|
__metadata("design:type", Function),
|
||||||
|
__metadata("design:paramtypes", [webhook_dto_1.PerfilCompletoDto]),
|
||||||
|
__metadata("design:returntype", void 0)
|
||||||
|
], WebhookController.prototype, "recibirPerfil", null);
|
||||||
|
exports.WebhookController = WebhookController = WebhookController_1 = __decorate([
|
||||||
|
(0, common_1.Controller)(),
|
||||||
|
__metadata("design:paramtypes", [pipeline_service_1.PipelineService])
|
||||||
|
], WebhookController);
|
||||||
|
//# sourceMappingURL=webhook.controller.js.map
|
||||||
1
mvp/image-worker/dist/webhook/webhook.controller.js.map
vendored
Normal file
1
mvp/image-worker/dist/webhook/webhook.controller.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"webhook.controller.js","sourceRoot":"","sources":["../../src/webhook/webhook.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,2CAAgE;AAChE,+CAAkD;AAClD,mEAA+D;AAGxD,IAAM,iBAAiB,yBAAvB,MAAM,iBAAiB;IAG5B,YAA6B,eAAgC;QAAhC,oBAAe,GAAf,eAAe,CAAiB;QAF5C,WAAM,GAAG,IAAI,eAAM,CAAC,mBAAiB,CAAC,IAAI,CAAC,CAAC;IAEG,CAAC;IAGjE,aAAa,CAAS,GAAsB;QAC1C,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,MAAM,uBAAuB,GAAG,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,CAAC;QAE/E,YAAY,CAAC,GAAG,EAAE;YAChB,IAAI,CAAC,eAAe,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;gBACnD,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,MAAM,qBAAqB,GAAG,CAAC,OAAO,EAAE,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC;YACjF,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,qCAAqC,EAAE,CAAC;IACtE,CAAC;CACF,CAAA;AAjBY,8CAAiB;AAM5B;IADC,IAAA,aAAI,EAAC,iBAAiB,CAAC;IACT,WAAA,IAAA,aAAI,GAAE,CAAA;;qCAAM,+BAAiB;;sDAU3C;4BAhBU,iBAAiB;IAD7B,IAAA,mBAAU,GAAE;qCAImC,kCAAe;GAHlD,iBAAiB,CAiB7B"}
|
||||||
22
mvp/image-worker/dist/webhook/webhook.module.js
vendored
Normal file
22
mvp/image-worker/dist/webhook/webhook.module.js
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
"use strict";
|
||||||
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
||||||
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
||||||
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
||||||
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
||||||
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.WebhookModule = void 0;
|
||||||
|
const common_1 = require("@nestjs/common");
|
||||||
|
const webhook_controller_1 = require("./webhook.controller");
|
||||||
|
const pipeline_module_1 = require("../pipeline/pipeline.module");
|
||||||
|
let WebhookModule = class WebhookModule {
|
||||||
|
};
|
||||||
|
exports.WebhookModule = WebhookModule;
|
||||||
|
exports.WebhookModule = WebhookModule = __decorate([
|
||||||
|
(0, common_1.Module)({
|
||||||
|
imports: [pipeline_module_1.PipelineModule],
|
||||||
|
controllers: [webhook_controller_1.WebhookController],
|
||||||
|
})
|
||||||
|
], WebhookModule);
|
||||||
|
//# sourceMappingURL=webhook.module.js.map
|
||||||
1
mvp/image-worker/dist/webhook/webhook.module.js.map
vendored
Normal file
1
mvp/image-worker/dist/webhook/webhook.module.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"webhook.module.js","sourceRoot":"","sources":["../../src/webhook/webhook.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,6DAAyD;AACzD,iEAA6D;AAMtD,IAAM,aAAa,GAAnB,MAAM,aAAa;CAAG,CAAA;AAAhB,sCAAa;wBAAb,aAAa;IAJzB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE,CAAC,gCAAc,CAAC;QACzB,WAAW,EAAE,CAAC,sCAAiB,CAAC;KACjC,CAAC;GACW,aAAa,CAAG"}
|
||||||
4833
mvp/image-worker/package-lock.json
generated
Normal file
4833
mvp/image-worker/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
39
mvp/image-worker/package.json
Normal file
39
mvp/image-worker/package.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "reformix-image-worker",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Worker de generación de renders fotorrealistas para Reformix. Recibe el perfil del lead, genera imágenes 'después' con IA y las entrega a la app principal.",
|
||||||
|
"author": "Reformix",
|
||||||
|
"private": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"build": "nest build",
|
||||||
|
"start": "nest start",
|
||||||
|
"start:dev": "nest start --watch",
|
||||||
|
"start:prod": "node dist/main",
|
||||||
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nestjs/common": "^10.0.0",
|
||||||
|
"@nestjs/core": "^10.0.0",
|
||||||
|
"@nestjs/config": "^3.0.0",
|
||||||
|
"@nestjs/platform-express": "^10.0.0",
|
||||||
|
"axios": "^1.6.0",
|
||||||
|
"class-validator": "^0.14.0",
|
||||||
|
"class-transformer": "^0.5.0",
|
||||||
|
"reflect-metadata": "^0.1.13",
|
||||||
|
"rxjs": "^7.8.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nestjs/cli": "^10.0.0",
|
||||||
|
"typescript": "^5.0.0",
|
||||||
|
"@types/node": "^20.0.0",
|
||||||
|
"ts-node": "^10.9.0"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"moduleFileExtensions": ["js", "json", "ts"],
|
||||||
|
"rootDir": "src",
|
||||||
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
|
"transform": { "^.+\\.(t|j)s$": "ts-jest" },
|
||||||
|
"testEnvironment": "node"
|
||||||
|
}
|
||||||
|
}
|
||||||
16
mvp/image-worker/prompts/prompt-builder.txt
Normal file
16
mvp/image-worker/prompts/prompt-builder.txt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
You are an expert interior designer and architectural renderer.
|
||||||
|
Your task is to generate a detailed, technical prompt in English for an image-to-image AI model that will transform a real photo of a space into a photorealistic render of the completed renovation.
|
||||||
|
|
||||||
|
The prompt must include:
|
||||||
|
- Specific materials and finishes (tile type, countertop material, flooring)
|
||||||
|
- Lighting style (natural, warm artificial, accent)
|
||||||
|
- Color palette aligned with the quality level
|
||||||
|
- Style and atmosphere based on client notes
|
||||||
|
- Technical rendering keywords: photorealistic, 8k, architectural visualization, professional interior photography, high detail
|
||||||
|
|
||||||
|
Quality level guide:
|
||||||
|
- basica: standard materials, functional design, clean finishes
|
||||||
|
- media: mid-range materials, modern design, quality finishes
|
||||||
|
- premium: high-end materials, designer touches, luxury finishes
|
||||||
|
|
||||||
|
Output ONLY the image prompt, no explanations, no preamble.
|
||||||
16
mvp/image-worker/prompts/supervisor.txt
Normal file
16
mvp/image-worker/prompts/supervisor.txt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
You are a quality supervisor for architectural renovation renders.
|
||||||
|
Evaluate whether the generated "after" render is coherent with the client's brief and the "before" photo.
|
||||||
|
|
||||||
|
Evaluate these criteria:
|
||||||
|
1. Style consistency with client notes and reform type
|
||||||
|
2. Materials and finishes match the quality level (basica/media/premium)
|
||||||
|
3. The renovated area is clearly recognizable from the "before" photo
|
||||||
|
4. Photorealistic quality and professional appearance
|
||||||
|
5. No obvious AI artifacts, distortions or incoherent elements
|
||||||
|
|
||||||
|
Respond ONLY with valid JSON, no markdown, no explanation:
|
||||||
|
{
|
||||||
|
"aprobado": boolean,
|
||||||
|
"score": number between 0 and 100,
|
||||||
|
"motivo": "reason in Spanish"
|
||||||
|
}
|
||||||
37
mvp/image-worker/skills/image-generator.md
Normal file
37
mvp/image-worker/skills/image-generator.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Generador de imágenes — via OpenRouter
|
||||||
|
|
||||||
|
## Responsabilidad
|
||||||
|
Recibir un prompt en inglés y una foto "antes", devolver el render "después" como data URI base64.
|
||||||
|
|
||||||
|
## Llamada a OpenRouter
|
||||||
|
POST https://openrouter.ai/api/v1/chat/completions
|
||||||
|
Authorization: Bearer {OPENROUTER_API_KEY}
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
Body:
|
||||||
|
{
|
||||||
|
"model": "{OPENROUTER_MODEL_IMAGEN}", // google/gemini-2.0-flash-exp-image-generation
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{ "type": "text", "text": promptGenerado },
|
||||||
|
{ "type": "image_url", "image_url": { "url": fotoAntesDataUri } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
## Manejo de la respuesta
|
||||||
|
Extraer la imagen generada de la respuesta. Buscar en:
|
||||||
|
1. content directo como data URI
|
||||||
|
2. expresion regular data:image/...;base64,...
|
||||||
|
3. URL de imagen
|
||||||
|
4. Partes del mensaje (choices[0].message.content si es array)
|
||||||
|
|
||||||
|
Devolver siempre como data:image/png;base64,...
|
||||||
|
|
||||||
|
## Errores
|
||||||
|
- Error de red → lanzar excepción, pipeline.service reintentará
|
||||||
|
- Respuesta 429 (rate limit) → esperar 5s y reintentar 1 vez
|
||||||
|
- Respuesta 5xx → lanzar excepción inmediatamente
|
||||||
26
mvp/image-worker/skills/pipeline.md
Normal file
26
mvp/image-worker/skills/pipeline.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Pipeline de 3 etapas
|
||||||
|
|
||||||
|
## Responsabilidad
|
||||||
|
Orquestar el procesamiento completo de un lead: desde recibir el perfil hasta entregar los renders a la app principal.
|
||||||
|
|
||||||
|
## Flujo por zona
|
||||||
|
Para cada zona del lead que tenga fotos "antes":
|
||||||
|
1. Etapa 1 → prompt-builder genera el prompt en inglés
|
||||||
|
2. Etapa 2 → image-generator produce el render
|
||||||
|
3. Etapa 3 → supervisor valida la coherencia
|
||||||
|
4. Si rechazado → reintentar máximo MAX_RETRIES veces desde Etapa 2
|
||||||
|
5. Si sigue rechazado → usar el último render de todos modos y loguear
|
||||||
|
|
||||||
|
## Reglas
|
||||||
|
- Zonas sin fotos "antes": saltar y loguear, nunca lanzar error
|
||||||
|
- Procesar todas las zonas antes de llamar a /ingesta
|
||||||
|
- Enviar todos los renders en una sola llamada con finalizar: true
|
||||||
|
- El pipeline corre en background, no bloquea el webhook
|
||||||
|
|
||||||
|
## Logs obligatorios
|
||||||
|
- [leadId] Iniciando pipeline para N zonas
|
||||||
|
- [leadId] Zona X: prompt generado
|
||||||
|
- [leadId] Zona X: imagen generada
|
||||||
|
- [leadId] Zona X: aprobada/rechazada (score: N)
|
||||||
|
- [leadId] Zona X: reintento N de MAX_RETRIES
|
||||||
|
- [leadId] Renders entregados correctamente
|
||||||
26
mvp/image-worker/skills/prompt-builder.md
Normal file
26
mvp/image-worker/skills/prompt-builder.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Prompt Builder — Claude Haiku 4.5 via OpenRouter
|
||||||
|
|
||||||
|
## Responsabilidad
|
||||||
|
Generar un prompt técnico detallado en inglés para el modelo de image-to-image a partir del contexto del lead (tipo de reforma, m², calidad, notas del cliente).
|
||||||
|
|
||||||
|
## Llamada a OpenRouter
|
||||||
|
POST https://openrouter.ai/api/v1/chat/completions
|
||||||
|
Authorization: Bearer {OPENROUTER_API_KEY}
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
Body:
|
||||||
|
{
|
||||||
|
"model": "{OPENROUTER_MODEL_TEXTO}", // anthropic/claude-3.5-haiku-20241022
|
||||||
|
"messages": [
|
||||||
|
{ "role": "system", "content": "You are an expert interior designer..." },
|
||||||
|
{ "role": "user", "content": "Generate a render prompt for a cocina renovation..." }
|
||||||
|
],
|
||||||
|
"max_tokens": 512,
|
||||||
|
"temperature": 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
## System prompt
|
||||||
|
Contenido de prompts/prompt-builder.txt
|
||||||
|
|
||||||
|
## Respuesta
|
||||||
|
Texto plano con el prompt en inglés. Sin markdown, sin JSON, sin explicaciones.
|
||||||
40
mvp/image-worker/skills/reformix-api.md
Normal file
40
mvp/image-worker/skills/reformix-api.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# API de la app principal Reformix
|
||||||
|
|
||||||
|
## Responsabilidad
|
||||||
|
Entregar los renders generados al endpoint /ingesta de la app Reformix.
|
||||||
|
|
||||||
|
## Endpoint
|
||||||
|
POST {REFORMIX_API_URL}/api/leads/{leadId}/ingesta
|
||||||
|
Authorization: Bearer {FUNNEL_API_KEY}
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
## Body
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"tipo": "foto",
|
||||||
|
"zona": "cocina", // zona que se procesó
|
||||||
|
"momento": "despues", // siempre "despues" para renders generados
|
||||||
|
"imagen": "data:image/png;base64,..."
|
||||||
|
}
|
||||||
|
// un item por cada zona procesada
|
||||||
|
],
|
||||||
|
"finalizar": true // siempre true, dispara PDF + email + WhatsApp
|
||||||
|
}
|
||||||
|
|
||||||
|
## Enums válidos (no usar otros valores)
|
||||||
|
tipo item: "foto" | "texto"
|
||||||
|
momento: "antes" | "despues"
|
||||||
|
zona: "cocina" | "bano" | "salon" | "comedor" | "integral" | "otro"
|
||||||
|
|
||||||
|
## Códigos de respuesta
|
||||||
|
200 { ok: true } → éxito
|
||||||
|
401 → FUNNEL_API_KEY incorrecta
|
||||||
|
404 → leadId no existe, no reintentar
|
||||||
|
422 → payload mal formado, revisar el body
|
||||||
|
|
||||||
|
## Reintentos
|
||||||
|
En caso de error 5xx o error de red:
|
||||||
|
→ reintentar 3 veces con 2 segundos de espera entre intentos
|
||||||
|
→ si sigue fallando, loguear como error crítico con el leadId
|
||||||
|
→ nunca reintentar en caso de 404
|
||||||
43
mvp/image-worker/skills/supervisor.md
Normal file
43
mvp/image-worker/skills/supervisor.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Supervisor de calidad — Claude Haiku 4.5 Vision via OpenRouter
|
||||||
|
|
||||||
|
## Responsabilidad
|
||||||
|
Comparar la foto "antes" con el render "después" y verificar que el resultado es coherente
|
||||||
|
con el brief del cliente. Devolver aprobación y score.
|
||||||
|
|
||||||
|
## Llamada a OpenRouter
|
||||||
|
POST https://openrouter.ai/api/v1/chat/completions
|
||||||
|
Authorization: Bearer {OPENROUTER_API_KEY}
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
Body:
|
||||||
|
{
|
||||||
|
"model": "{OPENROUTER_MODEL_TEXTO}", // anthropic/claude-3.5-haiku-20241022
|
||||||
|
"messages": [
|
||||||
|
{ "role": "system", "content": "You are a quality supervisor..." },
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{ "type": "text", "text": "Reforma tipo: cocina\nMetros: 12..." },
|
||||||
|
{ "type": "image_url", "image_url": { "url": "data:image/...foto_antes" } },
|
||||||
|
{ "type": "image_url", "image_url": { "url": "data:image/...render_despues" } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"max_tokens": 256,
|
||||||
|
"temperature": 0.2
|
||||||
|
}
|
||||||
|
|
||||||
|
## System prompt
|
||||||
|
Contenido de prompts/supervisor.txt
|
||||||
|
|
||||||
|
## Respuesta esperada
|
||||||
|
JSON estricto:
|
||||||
|
{ "aprobado": boolean, "score": number, "motivo": string }
|
||||||
|
|
||||||
|
## Manejo de respuesta malformada
|
||||||
|
Si el modelo no devuelve JSON válido:
|
||||||
|
→ devolver { aprobado: false, score: 0, motivo: "Error parseando respuesta del supervisor" }
|
||||||
|
Nunca lanzar excepción desde el supervisor, siempre devolver el objeto.
|
||||||
|
|
||||||
|
## Umbral de aprobación
|
||||||
|
aprobado: true AND score >= SUPERVISOR_MIN_SCORE (del .env, por defecto 70)
|
||||||
35
mvp/image-worker/skills/webhook.md
Normal file
35
mvp/image-worker/skills/webhook.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Webhook entrante — /perfil-completo
|
||||||
|
|
||||||
|
## Responsabilidad
|
||||||
|
Recibir el payload de la app Reformix, validarlo y arrancar el pipeline en background.
|
||||||
|
|
||||||
|
## Endpoint
|
||||||
|
POST /perfil-completo
|
||||||
|
|
||||||
|
## Respuesta inmediata (siempre)
|
||||||
|
{ "ok": true, "message": "Procesando renders en background..." }
|
||||||
|
Nunca bloquear esperando al pipeline.
|
||||||
|
|
||||||
|
## Payload esperado
|
||||||
|
{
|
||||||
|
leadId: string (UUID),
|
||||||
|
cliente: { nombre, telefono, email, provincia },
|
||||||
|
reforma: {
|
||||||
|
tipo: "cocina" | "bano" | "salon" | "comedor" | "integral" | "otro",
|
||||||
|
m2Suelo: number,
|
||||||
|
calidad: "basica" | "media" | "premium",
|
||||||
|
estructural: boolean,
|
||||||
|
urgencia: "alta" | "media" | "baja",
|
||||||
|
presupuestoTarget: number // en céntimos
|
||||||
|
},
|
||||||
|
empresa: { tenantId: string, nombre: string },
|
||||||
|
zonas: Array<{
|
||||||
|
zona: string,
|
||||||
|
notas: string[],
|
||||||
|
fotos: { antes: string[], despues: string[] }
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
## Validación
|
||||||
|
Usar class-validator con decoradores en webhook.dto.ts.
|
||||||
|
Si el payload no es válido → responder 400 con el error, no arrancar el pipeline.
|
||||||
15
mvp/image-worker/src/app.module.ts
Normal file
15
mvp/image-worker/src/app.module.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { WebhookModule } from './webhook/webhook.module';
|
||||||
|
import { PipelineModule } from './pipeline/pipeline.module';
|
||||||
|
import { ReformixModule } from './reformix/reformix.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot({ isGlobal: true }),
|
||||||
|
WebhookModule,
|
||||||
|
PipelineModule,
|
||||||
|
ReformixModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
20
mvp/image-worker/src/main.ts
Normal file
20
mvp/image-worker/src/main.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import 'reflect-metadata';
|
||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { AppModule } from './app.module';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.create(AppModule, {
|
||||||
|
logger: ['error', 'warn', 'log', 'debug'],
|
||||||
|
});
|
||||||
|
|
||||||
|
app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true }));
|
||||||
|
|
||||||
|
const config = app.get(ConfigService);
|
||||||
|
const port = config.get('PORT', 3001);
|
||||||
|
await app.listen(port);
|
||||||
|
console.log(`[Reformix Image Worker] corriendo en puerto ${port}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
bootstrap();
|
||||||
84
mvp/image-worker/src/pipeline/image-generator.service.ts
Normal file
84
mvp/image-worker/src/pipeline/image-generator.service.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ImageGeneratorService {
|
||||||
|
private readonly logger = new Logger(ImageGeneratorService.name);
|
||||||
|
|
||||||
|
constructor(private readonly config: ConfigService) {}
|
||||||
|
|
||||||
|
async generarRender(prompt: string, fotoAntesDataUri: string): Promise<string> {
|
||||||
|
const apiKey = this.config.get<string>('OPENROUTER_API_KEY');
|
||||||
|
const model = this.config.get<string>('OPENROUTER_MODEL_IMAGEN', 'google/gemini-2.0-flash-exp-image-generation');
|
||||||
|
|
||||||
|
const intentosRateLimit = 1;
|
||||||
|
for (let attempt = 0; attempt <= intentosRateLimit; attempt++) {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
OPENROUTER_URL,
|
||||||
|
{
|
||||||
|
model,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: [
|
||||||
|
{ type: 'text', text: prompt },
|
||||||
|
{ type: 'image_url', image_url: { url: fotoAntesDataUri } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'HTTP-Referer': 'https://reformix.es',
|
||||||
|
'X-Title': 'Reformix Image Worker',
|
||||||
|
},
|
||||||
|
timeout: 60000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const content = response.data.choices?.[0]?.message?.content;
|
||||||
|
if (!content) throw new Error('OpenRouter no devolvio contenido');
|
||||||
|
|
||||||
|
const imagen = this.extraerImagenDeRespuesta(content, response.data);
|
||||||
|
if (!imagen) throw new Error('No se pudo extraer imagen de la respuesta');
|
||||||
|
|
||||||
|
return imagen;
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err.response?.status === 429 && attempt < intentosRateLimit) {
|
||||||
|
this.logger.warn('Rate limit (429), esperando 5s y reintentando...');
|
||||||
|
await new Promise((r) => setTimeout(r, 5000));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Fallaron todos los intentos de generacion de imagen');
|
||||||
|
}
|
||||||
|
|
||||||
|
private extraerImagenDeRespuesta(content: string, rawResponse?: any): string | null {
|
||||||
|
if (content.startsWith('data:image')) return content;
|
||||||
|
|
||||||
|
const dataUriMatch = content.match(/data:image\/[a-zA-Z]+;base64,[^\s"']+/);
|
||||||
|
if (dataUriMatch) return dataUriMatch[0];
|
||||||
|
|
||||||
|
const urlMatch = content.match(/https?:\/\/[^\s"'()]+\.(png|jpg|jpeg|webp)/i);
|
||||||
|
if (urlMatch) return urlMatch[0];
|
||||||
|
|
||||||
|
const parts = rawResponse?.choices?.[0]?.message?.content;
|
||||||
|
if (Array.isArray(parts)) {
|
||||||
|
for (const part of parts) {
|
||||||
|
if (part.type === 'image_url' && part.image_url?.url) return part.image_url.url;
|
||||||
|
if (part.image_url?.url?.startsWith('data:image')) return part.image_url.url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
mvp/image-worker/src/pipeline/pipeline.module.ts
Normal file
13
mvp/image-worker/src/pipeline/pipeline.module.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { PipelineService } from './pipeline.service';
|
||||||
|
import { PromptBuilderService } from './prompt-builder.service';
|
||||||
|
import { ImageGeneratorService } from './image-generator.service';
|
||||||
|
import { SupervisorService } from './supervisor.service';
|
||||||
|
import { ReformixModule } from '../reformix/reformix.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [ReformixModule],
|
||||||
|
providers: [PipelineService, PromptBuilderService, ImageGeneratorService, SupervisorService],
|
||||||
|
exports: [PipelineService],
|
||||||
|
})
|
||||||
|
export class PipelineModule {}
|
||||||
114
mvp/image-worker/src/pipeline/pipeline.service.ts
Normal file
114
mvp/image-worker/src/pipeline/pipeline.service.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
68
mvp/image-worker/src/pipeline/prompt-builder.service.ts
Normal file
68
mvp/image-worker/src/pipeline/prompt-builder.service.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PromptBuilderService {
|
||||||
|
private readonly logger = new Logger(PromptBuilderService.name);
|
||||||
|
private systemPrompt = '';
|
||||||
|
|
||||||
|
constructor(private readonly config: ConfigService) {
|
||||||
|
const ruta = path.join(process.cwd(), 'prompts', 'prompt-builder.txt');
|
||||||
|
if (fs.existsSync(ruta)) {
|
||||||
|
this.systemPrompt = fs.readFileSync(ruta, 'utf-8');
|
||||||
|
} else {
|
||||||
|
this.logger.warn('prompts/prompt-builder.txt no encontrado, usando default');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async generarPrompt(
|
||||||
|
tipoReforma: string,
|
||||||
|
m2Suelo: number | null,
|
||||||
|
calidad: string,
|
||||||
|
notas: string[],
|
||||||
|
): Promise<string> {
|
||||||
|
const apiKey = this.config.get<string>('OPENROUTER_API_KEY');
|
||||||
|
const model = this.config.get<string>('OPENROUTER_MODEL_TEXTO', 'anthropic/claude-3.5-haiku-20241022');
|
||||||
|
|
||||||
|
const userContent = `Generate a render prompt for a ${tipoReforma} renovation.
|
||||||
|
- Area: ${m2Suelo ?? 'unknown'} m²
|
||||||
|
- Quality level: ${calidad}
|
||||||
|
- Client notes: ${notas.join('; ') || 'none'}
|
||||||
|
- Style: modern ${tipoReforma} renovation`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
OPENROUTER_URL,
|
||||||
|
{
|
||||||
|
model,
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: this.systemPrompt },
|
||||||
|
{ role: 'user', content: userContent },
|
||||||
|
],
|
||||||
|
max_tokens: 512,
|
||||||
|
temperature: 0.5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'HTTP-Referer': 'https://reformix.es',
|
||||||
|
'X-Title': 'Reformix Image Worker',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const prompt = response.data.choices?.[0]?.message?.content?.trim();
|
||||||
|
if (!prompt) throw new Error('OpenRouter devolvio respuesta vacia');
|
||||||
|
return prompt;
|
||||||
|
} catch (err: any) {
|
||||||
|
this.logger.error(`Error generando prompt: ${err.message}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
109
mvp/image-worker/src/pipeline/supervisor.service.ts
Normal file
109
mvp/image-worker/src/pipeline/supervisor.service.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
||||||
|
|
||||||
|
export interface SupervisarResultado {
|
||||||
|
aprobado: boolean;
|
||||||
|
score: number;
|
||||||
|
motivo: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SupervisorService {
|
||||||
|
private readonly logger = new Logger(SupervisorService.name);
|
||||||
|
private systemPrompt = '';
|
||||||
|
|
||||||
|
constructor(private readonly config: ConfigService) {
|
||||||
|
const ruta = path.join(process.cwd(), 'prompts', 'supervisor.txt');
|
||||||
|
if (fs.existsSync(ruta)) {
|
||||||
|
this.systemPrompt = fs.readFileSync(ruta, 'utf-8');
|
||||||
|
} else {
|
||||||
|
this.logger.warn('prompts/supervisor.txt no encontrado, usando default');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async supervisar(
|
||||||
|
tipoReforma: string,
|
||||||
|
m2Suelo: number | null,
|
||||||
|
calidad: string,
|
||||||
|
notas: string[],
|
||||||
|
fotoAntes: string,
|
||||||
|
renderDespues: string,
|
||||||
|
): Promise<SupervisarResultado> {
|
||||||
|
const apiKey = this.config.get<string>('OPENROUTER_API_KEY');
|
||||||
|
const model = this.config.get<string>('OPENROUTER_MODEL_TEXTO', 'anthropic/claude-3.5-haiku-20241022');
|
||||||
|
|
||||||
|
const notasTexto = notas.join('; ') || 'sin notas';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
OPENROUTER_URL,
|
||||||
|
{
|
||||||
|
model,
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: this.systemPrompt },
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: `Reforma tipo: ${tipoReforma}\nMetros: ${m2Suelo ?? 'desconocido'}\nCalidad: ${calidad}\nNotas del cliente: ${notasTexto}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'image_url',
|
||||||
|
image_url: { url: fotoAntes },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'image_url',
|
||||||
|
image_url: { url: renderDespues },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
max_tokens: 256,
|
||||||
|
temperature: 0.2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'HTTP-Referer': 'https://reformix.es',
|
||||||
|
'X-Title': 'Reformix Image Worker',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const textContent = response.data.choices?.[0]?.message?.content?.trim();
|
||||||
|
if (!textContent) {
|
||||||
|
return { aprobado: false, score: 0, motivo: 'Modelo devolvio respuesta vacia' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.parsearRespuesta(textContent);
|
||||||
|
} catch (err: any) {
|
||||||
|
this.logger.error(`Error en supervisor: ${err.message}`);
|
||||||
|
return { aprobado: false, score: 0, motivo: `Error del supervisor: ${err.message}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private parsearRespuesta(texto: string): SupervisarResultado {
|
||||||
|
const jsonMatch = texto.match(/\{[^{}]*"aprobado"[^{}]*\}/i);
|
||||||
|
if (!jsonMatch) {
|
||||||
|
return { aprobado: false, score: 0, motivo: 'Error parseando respuesta del supervisor' };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(jsonMatch[0]);
|
||||||
|
return {
|
||||||
|
aprobado: Boolean(parsed.aprobado),
|
||||||
|
score: Math.min(100, Math.max(0, Number(parsed.score) || 0)),
|
||||||
|
motivo: String(parsed.motivo || 'Sin motivo'),
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { aprobado: false, score: 0, motivo: 'Error parseando respuesta del supervisor' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
mvp/image-worker/src/reformix/reformix.module.ts
Normal file
8
mvp/image-worker/src/reformix/reformix.module.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ReformixService } from './reformix.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [ReformixService],
|
||||||
|
exports: [ReformixService],
|
||||||
|
})
|
||||||
|
export class ReformixModule {}
|
||||||
87
mvp/image-worker/src/reformix/reformix.service.ts
Normal file
87
mvp/image-worker/src/reformix/reformix.service.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ReformixService {
|
||||||
|
private readonly logger = new Logger(ReformixService.name);
|
||||||
|
private readonly baseUrl: string;
|
||||||
|
private readonly apiKey: string;
|
||||||
|
|
||||||
|
constructor(private readonly config: ConfigService) {
|
||||||
|
this.baseUrl = this.config.get<string>('REFORMIX_API_URL', 'http://localhost:3000');
|
||||||
|
this.apiKey = this.config.get<string>('FUNNEL_API_KEY', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
async entregarRenders(
|
||||||
|
leadId: string,
|
||||||
|
items: Array<{ zona: string; imagen: string }>,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const body = {
|
||||||
|
items: items.map((i) => ({
|
||||||
|
tipo: 'foto',
|
||||||
|
zona: i.zona,
|
||||||
|
momento: 'despues',
|
||||||
|
imagen: i.imagen,
|
||||||
|
})),
|
||||||
|
finalizar: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const maxRetries = 3;
|
||||||
|
const delay = 2000;
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
`${this.baseUrl}/api/leads/${leadId}/ingesta`,
|
||||||
|
body,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.apiKey}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
timeout: 30000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
this.logger.log(`[${leadId}] Renders entregados correctamente`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 404) {
|
||||||
|
this.logger.error(`[${leadId}] Lead no encontrado (404), abandonando`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 422) {
|
||||||
|
this.logger.error(`[${leadId}] Payload invalido (422): ${JSON.stringify(response.data)}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status >= 500 && attempt < maxRetries) {
|
||||||
|
this.logger.warn(`[${leadId}] Intento ${attempt}/${maxRetries} fallo (${response.status}), reintentando...`);
|
||||||
|
await new Promise((r) => setTimeout(r, delay));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.error(`[${leadId}] Error inesperado (${response.status}): ${JSON.stringify(response.data)}`);
|
||||||
|
return false;
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err.response?.status === 404) {
|
||||||
|
this.logger.error(`[${leadId}] Lead no encontrado (404), abandonando`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (attempt < maxRetries) {
|
||||||
|
this.logger.warn(`[${leadId}] Intento ${attempt}/${maxRetries} error de red, reintentando...`);
|
||||||
|
await new Promise((r) => setTimeout(r, delay));
|
||||||
|
} else {
|
||||||
|
this.logger.error(`[${leadId}] Error critico entregando renders tras ${maxRetries} intentos: ${err.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
23
mvp/image-worker/src/webhook/webhook.controller.ts
Normal file
23
mvp/image-worker/src/webhook/webhook.controller.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Controller, Post, Body, Logger } from '@nestjs/common';
|
||||||
|
import { PerfilCompletoDto } from './webhook.dto';
|
||||||
|
import { PipelineService } from '../pipeline/pipeline.service';
|
||||||
|
|
||||||
|
@Controller()
|
||||||
|
export class WebhookController {
|
||||||
|
private readonly logger = new Logger(WebhookController.name);
|
||||||
|
|
||||||
|
constructor(private readonly pipelineService: PipelineService) {}
|
||||||
|
|
||||||
|
@Post('perfil-completo')
|
||||||
|
recibirPerfil(@Body() dto: PerfilCompletoDto) {
|
||||||
|
this.logger.log(`[${dto.leadId}] Webhook recibido: ${dto.zonas.length} zonas`);
|
||||||
|
|
||||||
|
setImmediate(() => {
|
||||||
|
this.pipelineService.procesarLead(dto).catch((err) => {
|
||||||
|
this.logger.error(`[${dto.leadId}] Pipeline fallo: ${err.message}`, err.stack);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ok: true, message: 'Procesando renders en background...' };
|
||||||
|
}
|
||||||
|
}
|
||||||
97
mvp/image-worker/src/webhook/webhook.dto.ts
Normal file
97
mvp/image-worker/src/webhook/webhook.dto.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { IsString, IsOptional, IsNumber, IsBoolean, IsArray, ValidateNested, IsIn, IsUUID } from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
|
||||||
|
class ClienteDto {
|
||||||
|
@IsString()
|
||||||
|
nombre: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
telefono: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
provincia?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReformaDto {
|
||||||
|
@IsString()
|
||||||
|
@IsIn(['cocina', 'bano', 'salon', 'comedor', 'integral', 'otro'])
|
||||||
|
tipo: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
m2Suelo?: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsIn(['basica', 'media', 'premium'])
|
||||||
|
calidad: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
estructural?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@IsIn(['alta', 'media', 'baja'])
|
||||||
|
urgencia?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
presupuestoTarget?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class EmpresaDto {
|
||||||
|
@IsUUID()
|
||||||
|
tenantId: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
nombre: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class FotosDto {
|
||||||
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
antes: string[];
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
despues: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
class ZonaDto {
|
||||||
|
@IsString()
|
||||||
|
zona: string;
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
notas: string[];
|
||||||
|
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => FotosDto)
|
||||||
|
fotos: FotosDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PerfilCompletoDto {
|
||||||
|
@IsUUID()
|
||||||
|
leadId: string;
|
||||||
|
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => ClienteDto)
|
||||||
|
cliente: ClienteDto;
|
||||||
|
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => ReformaDto)
|
||||||
|
reforma: ReformaDto;
|
||||||
|
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => EmpresaDto)
|
||||||
|
empresa: EmpresaDto;
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => ZonaDto)
|
||||||
|
zonas: ZonaDto[];
|
||||||
|
}
|
||||||
9
mvp/image-worker/src/webhook/webhook.module.ts
Normal file
9
mvp/image-worker/src/webhook/webhook.module.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { WebhookController } from './webhook.controller';
|
||||||
|
import { PipelineModule } from '../pipeline/pipeline.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PipelineModule],
|
||||||
|
controllers: [WebhookController],
|
||||||
|
})
|
||||||
|
export class WebhookModule {}
|
||||||
8
mvp/image-worker/tsconfig.build.json
Normal file
8
mvp/image-worker/tsconfig.build.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"declaration": false,
|
||||||
|
"noEmit": false
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
1
mvp/image-worker/tsconfig.build.tsbuildinfo
Normal file
1
mvp/image-worker/tsconfig.build.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
23
mvp/image-worker/tsconfig.json
Normal file
23
mvp/image-worker/tsconfig.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"declaration": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"target": "ES2021",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"incremental": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strictNullChecks": false,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"strictBindCallApply": false,
|
||||||
|
"forceConsistentCasingInFileNames": false,
|
||||||
|
"noFallthroughCasesInSwitch": false
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user