Sandbox de renders en el image-worker + fixes del pipeline
Sandbox web (/sandbox) servido por el worker, reusando los mismos servicios del pipeline, para iterar prompts/modelos con una imagen y ver variaciones+score, y guardar la config ganadora (aplica al worker al instante, persistida en volumen): - SettingsService: store de config (system prompts + modelos) con defaults de prompts/*.txt + env y override persistido en /app/data (sobrevive a redeploys). - /sandbox (HTML), GET /sandbox/config, POST /sandbox/render, POST /sandbox/save (auth Bearer FUNNEL_API_KEY). DOM seguro, sin innerHTML de contenido externo. - prompt-builder/supervisor/image-generator aceptan overrides y leen de Settings. Fixes del pipeline de generación: - image-generator: pide modalities ['image','text'] y extrae la imagen de message.images[] (forma real de OpenRouter), no solo de content. - main.ts: sube el límite de body a 30mb (las fotos en data URI rompían el 100kb). Deja de versionar artefactos de build (dist/ + *.tsbuildinfo); .gitignore en image-worker y Whatsapp-bot. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1
mvp/Whatsapp-bot/.gitignore
vendored
1
mvp/Whatsapp-bot/.gitignore
vendored
@@ -2,4 +2,5 @@ node_modules/
|
|||||||
dist/
|
dist/
|
||||||
.env
|
.env
|
||||||
*.log
|
*.log
|
||||||
|
*.tsbuildinfo
|
||||||
auth_info_baileys/
|
auth_info_baileys/
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
6
mvp/image-worker/.gitignore
vendored
Normal file
6
mvp/image-worker/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
*.tsbuildinfo
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
coverage/
|
||||||
28
mvp/image-worker/dist/app.module.js
vendored
28
mvp/image-worker/dist/app.module.js
vendored
@@ -1,28 +0,0 @@
|
|||||||
"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
1
mvp/image-worker/dist/app.module.js.map
vendored
@@ -1 +0,0 @@
|
|||||||
{"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
19
mvp/image-worker/dist/main.js
vendored
@@ -1,19 +0,0 @@
|
|||||||
"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
1
mvp/image-worker/dist/main.js.map
vendored
@@ -1 +0,0 @@
|
|||||||
{"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"}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
"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 +0,0 @@
|
|||||||
{"version":3,"file":"image-generator.service.js","sourceRoot":"","sources":["../../src/pipeline/image-generator.service.ts"],"names":[],"mappings":";;;;;;;;;;;;;AAAA,2CAAoD;AACpD,2CAA+C;AAC/C,iCAA0B;AAE1B,MAAM,cAAc,GAAG,+CAA+C,CAAC;AAGhE,IAAM,qBAAqB,6BAA3B,MAAM,qBAAqB;IAGhC,YAA6B,MAAqB;QAArB,WAAM,GAAN,MAAM,CAAe;QAFjC,WAAM,GAAG,IAAI,eAAM,CAAC,uBAAqB,CAAC,IAAI,CAAC,CAAC;IAEZ,CAAC;IAEtD,KAAK,CAAC,aAAa,CAAC,MAAc,EAAE,gBAAwB;QAC1D,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAS,oBAAoB,CAAC,CAAC;QAC7D,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAS,yBAAyB,EAAE,8CAA8C,CAAC,CAAC;QAEjH,MAAM,iBAAiB,GAAG,CAAC,CAAC;QAC5B,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,iBAAiB,EAAE,OAAO,EAAE,EAAE,CAAC;YAC9D,IAAI,CAAC;gBACH,MAAM,QAAQ,GAAG,MAAM,eAAK,CAAC,IAAI,CAC/B,cAAc,EACd;oBACE,KAAK;oBACL,QAAQ,EAAE;wBACR;4BACE,IAAI,EAAE,MAAM;4BACZ,OAAO,EAAE;gCACP,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE;gCAC9B,EAAE,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,EAAE,GAAG,EAAE,gBAAgB,EAAE,EAAE;6BAC5D;yBACF;qBACF;iBACF,EACD;oBACE,OAAO,EAAE;wBACP,aAAa,EAAE,UAAU,MAAM,EAAE;wBACjC,cAAc,EAAE,kBAAkB;wBAClC,cAAc,EAAE,qBAAqB;wBACrC,SAAS,EAAE,uBAAuB;qBACnC;oBACD,OAAO,EAAE,KAAK;iBACf,CACF,CAAC;gBAEF,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC;gBAC7D,IAAI,CAAC,OAAO;oBAAE,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC;gBAElE,MAAM,MAAM,GAAG,IAAI,CAAC,wBAAwB,CAAC,OAAO,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC;gBACrE,IAAI,CAAC,MAAM;oBAAE,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;gBAE1E,OAAO,MAAM,CAAC;YAChB,CAAC;YAAC,OAAO,GAAQ,EAAE,CAAC;gBAClB,IAAI,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,GAAG,IAAI,OAAO,GAAG,iBAAiB,EAAE,CAAC;oBAChE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,kDAAkD,CAAC,CAAC;oBACrE,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;oBAC9C,SAAS;gBACX,CAAC;gBACD,MAAM,GAAG,CAAC;YACZ,CAAC;QACH,CAAC;QAED,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAC;IACzE,CAAC;IAEO,wBAAwB,CAAC,OAAe,EAAE,WAAiB;QACjE,IAAI,OAAO,CAAC,UAAU,CAAC,YAAY,CAAC;YAAE,OAAO,OAAO,CAAC;QAErD,MAAM,YAAY,GAAG,OAAO,CAAC,KAAK,CAAC,uCAAuC,CAAC,CAAC;QAC5E,IAAI,YAAY;YAAE,OAAO,YAAY,CAAC,CAAC,CAAC,CAAC;QAEzC,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,6CAA6C,CAAC,CAAC;QAC9E,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC;QAEjC,MAAM,KAAK,GAAG,WAAW,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC;QAC1D,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACzB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;gBACzB,IAAI,IAAI,CAAC,IAAI,KAAK,WAAW,IAAI,IAAI,CAAC,SAAS,EAAE,GAAG;oBAAE,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC;gBAChF,IAAI,IAAI,CAAC,SAAS,EAAE,GAAG,EAAE,UAAU,CAAC,YAAY,CAAC;oBAAE,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC;YAC/E,CAAC;QACH,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;CACF,CAAA;AA5EY,sDAAqB;gCAArB,qBAAqB;IADjC,IAAA,mBAAU,GAAE;qCAI0B,sBAAa;GAHvC,qBAAqB,CA4EjC"}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
"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 +0,0 @@
|
|||||||
{"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"}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
"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 +0,0 @@
|
|||||||
{"version":3,"file":"pipeline.service.js","sourceRoot":"","sources":["../../src/pipeline/pipeline.service.ts"],"names":[],"mappings":"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"}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
"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 +0,0 @@
|
|||||||
{"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
104
mvp/image-worker/dist/pipeline/supervisor.service.js
vendored
@@ -1,104 +0,0 @@
|
|||||||
"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 +0,0 @@
|
|||||||
{"version":3,"file":"supervisor.service.js","sourceRoot":"","sources":["../../src/pipeline/supervisor.service.ts"],"names":[],"mappings":";;;;;;;;;;;;;AAAA,2CAAoD;AACpD,2CAA+C;AAC/C,yBAAyB;AACzB,6BAA6B;AAC7B,iCAA0B;AAE1B,MAAM,cAAc,GAAG,+CAA+C,CAAC;AAShE,IAAM,iBAAiB,yBAAvB,MAAM,iBAAiB;IAI5B,YAA6B,MAAqB;QAArB,WAAM,GAAN,MAAM,CAAe;QAHjC,WAAM,GAAG,IAAI,eAAM,CAAC,mBAAiB,CAAC,IAAI,CAAC,CAAC;QACrD,iBAAY,GAAG,EAAE,CAAC;QAGxB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,gBAAgB,CAAC,CAAC;QACnE,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,sDAAsD,CAAC,CAAC;QAC3E,CAAC;IACH,CAAC;IAED,KAAK,CAAC,UAAU,CACd,WAAmB,EACnB,OAAsB,EACtB,OAAe,EACf,KAAe,EACf,SAAiB,EACjB,aAAqB;QAErB,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,UAAU,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,WAAW,CAAC;QAEnD,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;wBACE,IAAI,EAAE,MAAM;wBACZ,OAAO,EAAE;4BACP;gCACE,IAAI,EAAE,MAAM;gCACZ,IAAI,EAAE,iBAAiB,WAAW,aAAa,OAAO,IAAI,aAAa,cAAc,OAAO,wBAAwB,UAAU,EAAE;6BACjI;4BACD;gCACE,IAAI,EAAE,WAAW;gCACjB,SAAS,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE;6BAC9B;4BACD;gCACE,IAAI,EAAE,WAAW;gCACjB,SAAS,EAAE,EAAE,GAAG,EAAE,aAAa,EAAE;6BAClC;yBACF;qBACF;iBACF;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,WAAW,GAAG,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;YACzE,IAAI,CAAC,WAAW,EAAE,CAAC;gBACjB,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,iCAAiC,EAAE,CAAC;YAClF,CAAC;YAED,OAAO,IAAI,CAAC,gBAAgB,CAAC,WAAW,CAAC,CAAC;QAC5C,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAClB,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,wBAAwB,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;YACzD,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,yBAAyB,GAAG,CAAC,OAAO,EAAE,EAAE,CAAC;QACvF,CAAC;IACH,CAAC;IAEO,gBAAgB,CAAC,KAAa;QACpC,MAAM,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC,6BAA6B,CAAC,CAAC;QAC7D,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,0CAA0C,EAAE,CAAC;QAC3F,CAAC;QAED,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;YACxC,OAAO;gBACL,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC;gBAClC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;gBAC5D,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,IAAI,YAAY,CAAC;aAC9C,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,0CAA0C,EAAE,CAAC;QAC3F,CAAC;IACH,CAAC;CACF,CAAA;AA7FY,8CAAiB;4BAAjB,iBAAiB;IAD7B,IAAA,mBAAU,GAAE;qCAK0B,sBAAa;GAJvC,iBAAiB,CA6F7B"}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
"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 +0,0 @@
|
|||||||
{"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
22
mvp/image-worker/dist/webhook/webhook.module.js
vendored
@@ -1,22 +0,0 @@
|
|||||||
"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 +0,0 @@
|
|||||||
{"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"}
|
|
||||||
@@ -3,13 +3,17 @@ import { ConfigModule } from '@nestjs/config';
|
|||||||
import { WebhookModule } from './webhook/webhook.module';
|
import { WebhookModule } from './webhook/webhook.module';
|
||||||
import { PipelineModule } from './pipeline/pipeline.module';
|
import { PipelineModule } from './pipeline/pipeline.module';
|
||||||
import { ReformixModule } from './reformix/reformix.module';
|
import { ReformixModule } from './reformix/reformix.module';
|
||||||
|
import { SettingsModule } from './settings/settings.module';
|
||||||
|
import { SandboxModule } from './sandbox/sandbox.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({ isGlobal: true }),
|
ConfigModule.forRoot({ isGlobal: true }),
|
||||||
|
SettingsModule,
|
||||||
WebhookModule,
|
WebhookModule,
|
||||||
PipelineModule,
|
PipelineModule,
|
||||||
ReformixModule,
|
ReformixModule,
|
||||||
|
SandboxModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'reflect-metadata';
|
|||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { ValidationPipe } from '@nestjs/common';
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { json, urlencoded } from 'express';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
@@ -9,6 +10,10 @@ async function bootstrap() {
|
|||||||
logger: ['error', 'warn', 'log', 'debug'],
|
logger: ['error', 'warn', 'log', 'debug'],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Las fotos viajan como data URI (base64) → subir el límite por defecto de Express (100kb).
|
||||||
|
app.use(json({ limit: '30mb' }));
|
||||||
|
app.use(urlencoded({ limit: '30mb', extended: true }));
|
||||||
|
|
||||||
app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true }));
|
app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true }));
|
||||||
|
|
||||||
const config = app.get(ConfigService);
|
const config = app.get(ConfigService);
|
||||||
|
|||||||
@@ -1,84 +1,130 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { SettingsService } from '../settings/settings.service';
|
||||||
|
|
||||||
const OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
const OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
||||||
|
|
||||||
|
export interface GenerarRenderResultado {
|
||||||
|
imagen: string | null;
|
||||||
|
error?: string;
|
||||||
|
debug: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ImageGeneratorService {
|
export class ImageGeneratorService {
|
||||||
private readonly logger = new Logger(ImageGeneratorService.name);
|
private readonly logger = new Logger(ImageGeneratorService.name);
|
||||||
|
|
||||||
constructor(private readonly config: ConfigService) {}
|
constructor(
|
||||||
|
private readonly config: ConfigService,
|
||||||
|
private readonly settings: SettingsService,
|
||||||
|
) {}
|
||||||
|
|
||||||
async generarRender(prompt: string, fotoAntesDataUri: string): Promise<string> {
|
// Usado por el pipeline real: devuelve la imagen o lanza.
|
||||||
|
async generarRender(prompt: string, fotoAntesDataUri: string, opts?: { model?: string }): Promise<string> {
|
||||||
|
const r = await this.generarConDebug(prompt, fotoAntesDataUri, opts);
|
||||||
|
if (!r.imagen) throw new Error(r.error || 'No se pudo extraer imagen de la respuesta');
|
||||||
|
return r.imagen;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usado por el sandbox: nunca lanza, devuelve imagen|null + info de depuración (sin volcar base64).
|
||||||
|
async generarConDebug(
|
||||||
|
prompt: string,
|
||||||
|
fotoAntesDataUri: string,
|
||||||
|
opts?: { model?: string },
|
||||||
|
): Promise<GenerarRenderResultado> {
|
||||||
const apiKey = this.config.get<string>('OPENROUTER_API_KEY');
|
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 model = opts?.model?.trim() || this.settings.getModeloImagen();
|
||||||
|
|
||||||
const intentosRateLimit = 1;
|
try {
|
||||||
for (let attempt = 0; attempt <= intentosRateLimit; attempt++) {
|
const response = await axios.post(
|
||||||
try {
|
OPENROUTER_URL,
|
||||||
const response = await axios.post(
|
{
|
||||||
OPENROUTER_URL,
|
model,
|
||||||
{
|
// Necesario para que OpenRouter devuelva imagen además de texto.
|
||||||
model,
|
modalities: ['image', 'text'],
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: [
|
content: [
|
||||||
{ type: 'text', text: prompt },
|
{ type: 'text', text: prompt },
|
||||||
{ type: 'image_url', image_url: { url: fotoAntesDataUri } },
|
{ 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,
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'HTTP-Referer': 'https://reformix.es',
|
||||||
|
'X-Title': 'Reformix Image Worker',
|
||||||
},
|
},
|
||||||
);
|
timeout: 90000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const content = response.data.choices?.[0]?.message?.content;
|
const message = response.data?.choices?.[0]?.message;
|
||||||
if (!content) throw new Error('OpenRouter no devolvio contenido');
|
const imagen = this.extraerImagen(message);
|
||||||
|
return {
|
||||||
|
imagen,
|
||||||
|
error: imagen ? undefined : 'No se encontró imagen en la respuesta de OpenRouter',
|
||||||
|
debug: this.resumenDebug(response.data, model, imagen),
|
||||||
|
};
|
||||||
|
} catch (err: any) {
|
||||||
|
const status = err.response?.status;
|
||||||
|
const msg = err.response?.data?.error?.message || err.message;
|
||||||
|
this.logger.error(`Error generando imagen (${status ?? 'sin status'}): ${msg}`);
|
||||||
|
return {
|
||||||
|
imagen: null,
|
||||||
|
error: msg,
|
||||||
|
debug: { model, status, error: msg },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const imagen = this.extraerImagenDeRespuesta(content, response.data);
|
private extraerImagen(message: any): string | null {
|
||||||
if (!imagen) throw new Error('No se pudo extraer imagen de la respuesta');
|
if (!message) return null;
|
||||||
|
|
||||||
return imagen;
|
// 1) Forma de OpenRouter para image-gen: message.images[].image_url.url
|
||||||
} catch (err: any) {
|
if (Array.isArray(message.images)) {
|
||||||
if (err.response?.status === 429 && attempt < intentosRateLimit) {
|
for (const img of message.images) {
|
||||||
this.logger.warn('Rate limit (429), esperando 5s y reintentando...');
|
const url = img?.image_url?.url ?? img?.url ?? (typeof img === 'string' ? img : null);
|
||||||
await new Promise((r) => setTimeout(r, 5000));
|
if (typeof url === 'string' && url) return url;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error('Fallaron todos los intentos de generacion de imagen');
|
const content = message.content;
|
||||||
}
|
if (typeof content === 'string') {
|
||||||
|
if (content.startsWith('data:image')) return content;
|
||||||
private extraerImagenDeRespuesta(content: string, rawResponse?: any): string | null {
|
const dataUri = content.match(/data:image\/[a-zA-Z+]+;base64,[A-Za-z0-9+/=]+/);
|
||||||
if (content.startsWith('data:image')) return content;
|
if (dataUri) return dataUri[0];
|
||||||
|
const url = content.match(/https?:\/\/[^\s"'()]+\.(?:png|jpg|jpeg|webp)/i);
|
||||||
const dataUriMatch = content.match(/data:image\/[a-zA-Z]+;base64,[^\s"']+/);
|
if (url) return url[0];
|
||||||
if (dataUriMatch) return dataUriMatch[0];
|
} else if (Array.isArray(content)) {
|
||||||
|
for (const part of content) {
|
||||||
const urlMatch = content.match(/https?:\/\/[^\s"'()]+\.(png|jpg|jpeg|webp)/i);
|
const url = part?.image_url?.url;
|
||||||
if (urlMatch) return urlMatch[0];
|
if (typeof url === 'string' && url) return url;
|
||||||
|
|
||||||
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resumen compacto para depurar el formato real sin meter el base64 (enorme) en la respuesta.
|
||||||
|
private resumenDebug(data: any, model: string, imagen: string | null): Record<string, unknown> {
|
||||||
|
const msg = data?.choices?.[0]?.message;
|
||||||
|
return {
|
||||||
|
model,
|
||||||
|
modelDevuelto: data?.model,
|
||||||
|
finishReason: data?.choices?.[0]?.finish_reason,
|
||||||
|
messageKeys: msg ? Object.keys(msg) : [],
|
||||||
|
imagesCount: Array.isArray(msg?.images) ? msg.images.length : 0,
|
||||||
|
contentType: Array.isArray(msg?.content) ? 'array' : typeof msg?.content,
|
||||||
|
contentPreview: typeof msg?.content === 'string' ? msg.content.slice(0, 200) : undefined,
|
||||||
|
imagenEncontrada: !!imagen,
|
||||||
|
imagenTipo: imagen ? (imagen.startsWith('data:') ? 'data-uri' : 'url') : null,
|
||||||
|
usage: data?.usage,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,6 @@ import { ReformixModule } from '../reformix/reformix.module';
|
|||||||
@Module({
|
@Module({
|
||||||
imports: [ReformixModule],
|
imports: [ReformixModule],
|
||||||
providers: [PipelineService, PromptBuilderService, ImageGeneratorService, SupervisorService],
|
providers: [PipelineService, PromptBuilderService, ImageGeneratorService, SupervisorService],
|
||||||
exports: [PipelineService],
|
exports: [PipelineService, PromptBuilderService, ImageGeneratorService, SupervisorService],
|
||||||
})
|
})
|
||||||
export class PipelineModule {}
|
export class PipelineModule {}
|
||||||
|
|||||||
@@ -1,33 +1,34 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { SettingsService } from '../settings/settings.service';
|
||||||
|
|
||||||
const OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
const OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
||||||
|
|
||||||
|
export interface PromptBuilderOpts {
|
||||||
|
systemPrompt?: string;
|
||||||
|
model?: string;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PromptBuilderService {
|
export class PromptBuilderService {
|
||||||
private readonly logger = new Logger(PromptBuilderService.name);
|
private readonly logger = new Logger(PromptBuilderService.name);
|
||||||
private systemPrompt = '';
|
|
||||||
|
|
||||||
constructor(private readonly config: ConfigService) {
|
constructor(
|
||||||
const ruta = path.join(process.cwd(), 'prompts', 'prompt-builder.txt');
|
private readonly config: ConfigService,
|
||||||
if (fs.existsSync(ruta)) {
|
private readonly settings: SettingsService,
|
||||||
this.systemPrompt = fs.readFileSync(ruta, 'utf-8');
|
) {}
|
||||||
} else {
|
|
||||||
this.logger.warn('prompts/prompt-builder.txt no encontrado, usando default');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async generarPrompt(
|
async generarPrompt(
|
||||||
tipoReforma: string,
|
tipoReforma: string,
|
||||||
m2Suelo: number | null,
|
m2Suelo: number | null,
|
||||||
calidad: string,
|
calidad: string,
|
||||||
notas: string[],
|
notas: string[],
|
||||||
|
opts?: PromptBuilderOpts,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const apiKey = this.config.get<string>('OPENROUTER_API_KEY');
|
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 model = opts?.model?.trim() || this.settings.getModeloTexto();
|
||||||
|
const systemPrompt = opts?.systemPrompt ?? this.settings.getPromptBuilder();
|
||||||
|
|
||||||
const userContent = `Generate a render prompt for a ${tipoReforma} renovation.
|
const userContent = `Generate a render prompt for a ${tipoReforma} renovation.
|
||||||
- Area: ${m2Suelo ?? 'unknown'} m²
|
- Area: ${m2Suelo ?? 'unknown'} m²
|
||||||
@@ -41,7 +42,7 @@ export class PromptBuilderService {
|
|||||||
{
|
{
|
||||||
model,
|
model,
|
||||||
messages: [
|
messages: [
|
||||||
{ role: 'system', content: this.systemPrompt },
|
{ role: 'system', content: systemPrompt },
|
||||||
{ role: 'user', content: userContent },
|
{ role: 'user', content: userContent },
|
||||||
],
|
],
|
||||||
max_tokens: 512,
|
max_tokens: 512,
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { SettingsService } from '../settings/settings.service';
|
||||||
|
|
||||||
const OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
const OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
||||||
|
|
||||||
@@ -12,19 +11,19 @@ export interface SupervisarResultado {
|
|||||||
motivo: string;
|
motivo: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SupervisorOpts {
|
||||||
|
systemPrompt?: string;
|
||||||
|
model?: string;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SupervisorService {
|
export class SupervisorService {
|
||||||
private readonly logger = new Logger(SupervisorService.name);
|
private readonly logger = new Logger(SupervisorService.name);
|
||||||
private systemPrompt = '';
|
|
||||||
|
|
||||||
constructor(private readonly config: ConfigService) {
|
constructor(
|
||||||
const ruta = path.join(process.cwd(), 'prompts', 'supervisor.txt');
|
private readonly config: ConfigService,
|
||||||
if (fs.existsSync(ruta)) {
|
private readonly settings: SettingsService,
|
||||||
this.systemPrompt = fs.readFileSync(ruta, 'utf-8');
|
) {}
|
||||||
} else {
|
|
||||||
this.logger.warn('prompts/supervisor.txt no encontrado, usando default');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async supervisar(
|
async supervisar(
|
||||||
tipoReforma: string,
|
tipoReforma: string,
|
||||||
@@ -33,10 +32,11 @@ export class SupervisorService {
|
|||||||
notas: string[],
|
notas: string[],
|
||||||
fotoAntes: string,
|
fotoAntes: string,
|
||||||
renderDespues: string,
|
renderDespues: string,
|
||||||
|
opts?: SupervisorOpts,
|
||||||
): Promise<SupervisarResultado> {
|
): Promise<SupervisarResultado> {
|
||||||
const apiKey = this.config.get<string>('OPENROUTER_API_KEY');
|
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 model = opts?.model?.trim() || this.settings.getModeloTexto();
|
||||||
|
const systemPrompt = opts?.systemPrompt ?? this.settings.getSupervisor();
|
||||||
const notasTexto = notas.join('; ') || 'sin notas';
|
const notasTexto = notas.join('; ') || 'sin notas';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -45,7 +45,7 @@ export class SupervisorService {
|
|||||||
{
|
{
|
||||||
model,
|
model,
|
||||||
messages: [
|
messages: [
|
||||||
{ role: 'system', content: this.systemPrompt },
|
{ role: 'system', content: systemPrompt },
|
||||||
{
|
{
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: [
|
content: [
|
||||||
@@ -53,14 +53,8 @@ export class SupervisorService {
|
|||||||
type: 'text',
|
type: 'text',
|
||||||
text: `Reforma tipo: ${tipoReforma}\nMetros: ${m2Suelo ?? 'desconocido'}\nCalidad: ${calidad}\nNotas del cliente: ${notasTexto}`,
|
text: `Reforma tipo: ${tipoReforma}\nMetros: ${m2Suelo ?? 'desconocido'}\nCalidad: ${calidad}\nNotas del cliente: ${notasTexto}`,
|
||||||
},
|
},
|
||||||
{
|
{ type: 'image_url', image_url: { url: fotoAntes } },
|
||||||
type: 'image_url',
|
{ type: 'image_url', image_url: { url: renderDespues } },
|
||||||
image_url: { url: fotoAntes },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'image_url',
|
|
||||||
image_url: { url: renderDespues },
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
131
mvp/image-worker/src/sandbox/sandbox.controller.ts
Normal file
131
mvp/image-worker/src/sandbox/sandbox.controller.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
Req,
|
||||||
|
Header,
|
||||||
|
UnauthorizedException,
|
||||||
|
BadRequestException,
|
||||||
|
Logger,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { SettingsService } from '../settings/settings.service';
|
||||||
|
import { PromptBuilderService } from '../pipeline/prompt-builder.service';
|
||||||
|
import { ImageGeneratorService } from '../pipeline/image-generator.service';
|
||||||
|
import { SupervisorService } from '../pipeline/supervisor.service';
|
||||||
|
import { SANDBOX_HTML } from './sandbox.page';
|
||||||
|
|
||||||
|
@Controller('sandbox')
|
||||||
|
export class SandboxController {
|
||||||
|
private readonly logger = new Logger(SandboxController.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly config: ConfigService,
|
||||||
|
private readonly settings: SettingsService,
|
||||||
|
private readonly promptBuilder: PromptBuilderService,
|
||||||
|
private readonly imageGenerator: ImageGeneratorService,
|
||||||
|
private readonly supervisor: SupervisorService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private autorizado(req: any): boolean {
|
||||||
|
const key = this.config.get<string>('FUNNEL_API_KEY');
|
||||||
|
if (!key) return false;
|
||||||
|
const auth = (req.headers?.authorization as string) || '';
|
||||||
|
return auth === `Bearer ${key}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private soloTexto(v: unknown): string | undefined {
|
||||||
|
return typeof v === 'string' && v.trim() ? v : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@Header('Content-Type', 'text/html; charset=utf-8')
|
||||||
|
page(): string {
|
||||||
|
return SANDBOX_HTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('config')
|
||||||
|
getConfig(@Req() req: any) {
|
||||||
|
if (!this.autorizado(req)) throw new UnauthorizedException('No autorizado');
|
||||||
|
return {
|
||||||
|
...this.settings.getAll(),
|
||||||
|
maxRetries: Number(this.config.get('MAX_RETRIES', 2)),
|
||||||
|
minScore: Number(this.config.get('SUPERVISOR_MIN_SCORE', 70)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('save')
|
||||||
|
save(@Req() req: any, @Body() body: Record<string, any>) {
|
||||||
|
if (!this.autorizado(req)) throw new UnauthorizedException('No autorizado');
|
||||||
|
const config = this.settings.guardar({
|
||||||
|
promptBuilder: body.promptBuilder,
|
||||||
|
supervisor: body.supervisor,
|
||||||
|
modeloTexto: body.modeloTexto,
|
||||||
|
modeloImagen: body.modeloImagen,
|
||||||
|
});
|
||||||
|
this.logger.log('Config guardada desde el sandbox');
|
||||||
|
return { ok: true, config };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('render')
|
||||||
|
async render(@Req() req: any, @Body() body: Record<string, any>) {
|
||||||
|
if (!this.autorizado(req)) throw new UnauthorizedException('No autorizado');
|
||||||
|
const imagenAntes = body.imagenAntes;
|
||||||
|
if (typeof imagenAntes !== 'string' || !imagenAntes) {
|
||||||
|
throw new BadRequestException('Falta imagenAntes (data URI).');
|
||||||
|
}
|
||||||
|
|
||||||
|
const tipo = this.soloTexto(body.tipo) || 'otro';
|
||||||
|
const calidad = this.soloTexto(body.calidad) || 'media';
|
||||||
|
const m2 = typeof body.m2 === 'number' ? body.m2 : null;
|
||||||
|
const notas: string[] = Array.isArray(body.notas)
|
||||||
|
? body.notas.map((n: unknown) => String(n)).filter(Boolean)
|
||||||
|
: String(body.notas ?? '')
|
||||||
|
.split(';')
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const modeloTexto = this.soloTexto(body.modeloTexto);
|
||||||
|
const modeloImagen = this.soloTexto(body.modeloImagen);
|
||||||
|
const systemPromptBuilder = this.soloTexto(body.systemPromptBuilder);
|
||||||
|
const supervisorPrompt = this.soloTexto(body.supervisorPrompt);
|
||||||
|
const supervisar = body.supervisar !== false;
|
||||||
|
const n = Math.min(4, Math.max(1, Number(body.nVariaciones) || 1));
|
||||||
|
const minScore = Number(this.config.get('SUPERVISOR_MIN_SCORE', 70));
|
||||||
|
|
||||||
|
// Prompt de imagen: directo (si lo pasan) o vía Prompt Builder.
|
||||||
|
const promptDirecto = this.soloTexto(body.promptDirecto);
|
||||||
|
let promptUsado: string;
|
||||||
|
if (promptDirecto) {
|
||||||
|
promptUsado = promptDirecto;
|
||||||
|
} else {
|
||||||
|
promptUsado = await this.promptBuilder.generarPrompt(tipo, m2, calidad, notas, {
|
||||||
|
systemPrompt: systemPromptBuilder,
|
||||||
|
model: modeloTexto,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const variaciones: Array<Record<string, unknown>> = [];
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const gen = await this.imageGenerator.generarConDebug(promptUsado, imagenAntes, { model: modeloImagen });
|
||||||
|
const v: Record<string, unknown> = {
|
||||||
|
imagen: gen.imagen,
|
||||||
|
error: gen.error,
|
||||||
|
debug: gen.debug,
|
||||||
|
};
|
||||||
|
if (supervisar && gen.imagen) {
|
||||||
|
const sup = await this.supervisor.supervisar(tipo, m2, calidad, notas, imagenAntes, gen.imagen, {
|
||||||
|
systemPrompt: supervisorPrompt,
|
||||||
|
model: modeloTexto,
|
||||||
|
});
|
||||||
|
v.score = sup.score;
|
||||||
|
v.motivo = sup.motivo;
|
||||||
|
v.aprobado = sup.aprobado && sup.score >= minScore;
|
||||||
|
}
|
||||||
|
variaciones.push(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { promptUsado, variaciones };
|
||||||
|
}
|
||||||
|
}
|
||||||
9
mvp/image-worker/src/sandbox/sandbox.module.ts
Normal file
9
mvp/image-worker/src/sandbox/sandbox.module.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { SandboxController } from './sandbox.controller';
|
||||||
|
import { PipelineModule } from '../pipeline/pipeline.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PipelineModule],
|
||||||
|
controllers: [SandboxController],
|
||||||
|
})
|
||||||
|
export class SandboxModule {}
|
||||||
217
mvp/image-worker/src/sandbox/sandbox.page.ts
Normal file
217
mvp/image-worker/src/sandbox/sandbox.page.ts
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
// Página del sandbox (HTML autocontenido). El <script> evita backticks y la secuencia dólar+llave
|
||||||
|
// para no colisionar con el template literal que lo envuelve.
|
||||||
|
export const SANDBOX_HTML = `<!DOCTYPE html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Reformix · Sandbox de renders</title>
|
||||||
|
<style>
|
||||||
|
:root { --bg:#0f1115; --panel:#181b22; --border:#2a2f3a; --fg:#e6e8ec; --muted:#9aa3b2; --accent:#4f8cff; --ok:#2ecc71; --bad:#ff5555; }
|
||||||
|
* { box-sizing:border-box; }
|
||||||
|
body { margin:0; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif; background:var(--bg); color:var(--fg); }
|
||||||
|
header { padding:14px 20px; border-bottom:1px solid var(--border); display:flex; gap:16px; align-items:center; flex-wrap:wrap; }
|
||||||
|
header h1 { font-size:16px; margin:0; font-weight:700; }
|
||||||
|
header .key { margin-left:auto; display:flex; gap:8px; align-items:center; }
|
||||||
|
.layout { display:grid; grid-template-columns:380px 1fr; gap:16px; padding:16px; align-items:start; }
|
||||||
|
@media (max-width:900px){ .layout { grid-template-columns:1fr; } }
|
||||||
|
.panel { background:var(--panel); border:1px solid var(--border); border-radius:10px; padding:14px; }
|
||||||
|
label { display:block; font-size:12px; color:var(--muted); margin:10px 0 4px; font-weight:600; }
|
||||||
|
input, select, textarea, button { font:inherit; color:var(--fg); background:#11141a; border:1px solid var(--border); border-radius:7px; padding:8px 10px; width:100%; }
|
||||||
|
textarea { resize:vertical; font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:12px; line-height:1.4; }
|
||||||
|
.row { display:flex; gap:8px; } .row > * { flex:1; }
|
||||||
|
button { cursor:pointer; width:auto; }
|
||||||
|
.btn-primary { background:var(--accent); border-color:var(--accent); color:#fff; font-weight:700; }
|
||||||
|
.btn-ghost { background:transparent; }
|
||||||
|
.actions { display:flex; gap:8px; margin-top:14px; }
|
||||||
|
.preview { margin-top:8px; max-width:100%; border-radius:8px; border:1px solid var(--border); display:none; }
|
||||||
|
details { margin-top:10px; } summary { cursor:pointer; color:var(--muted); font-size:12px; }
|
||||||
|
.status { margin-top:10px; font-size:13px; color:var(--muted); min-height:18px; }
|
||||||
|
.promptUsado { white-space:pre-wrap; background:#11141a; border:1px solid var(--border); border-radius:8px; padding:10px; font-family:ui-monospace,monospace; font-size:12px; color:#cdd3dd; }
|
||||||
|
.grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(260px,1fr)); gap:14px; margin-top:14px; }
|
||||||
|
.card { background:#11141a; border:1px solid var(--border); border-radius:10px; overflow:hidden; }
|
||||||
|
.card img { width:100%; display:block; background:#000; }
|
||||||
|
.card .meta { padding:10px; }
|
||||||
|
.badge { display:inline-block; padding:2px 8px; border-radius:999px; font-size:12px; font-weight:700; }
|
||||||
|
.badge.ok { background:rgba(46,204,113,.15); color:var(--ok); } .badge.bad { background:rgba(255,85,85,.15); color:var(--bad); }
|
||||||
|
.motivo { font-size:12px; color:var(--muted); margin-top:6px; }
|
||||||
|
pre { white-space:pre-wrap; word-break:break-word; font-size:11px; color:#9aa3b2; background:#0c0e12; padding:8px; border-radius:6px; max-height:220px; overflow:auto; }
|
||||||
|
.hint { font-size:11px; color:var(--muted); margin-top:4px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>🎨 Reformix · Sandbox de renders</h1>
|
||||||
|
<div class="key">
|
||||||
|
<input id="apiKey" type="password" placeholder="FUNNEL_API_KEY" style="width:240px" />
|
||||||
|
<button class="btn-ghost" onclick="guardarKey()">Recordar</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="layout">
|
||||||
|
<div class="panel">
|
||||||
|
<label>Foto "antes"</label>
|
||||||
|
<input id="foto" type="file" accept="image/*" onchange="cargarFoto(event)" />
|
||||||
|
<img id="fotoPreview" class="preview" />
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div>
|
||||||
|
<label>Tipo</label>
|
||||||
|
<select id="tipo">
|
||||||
|
<option>cocina</option><option>bano</option><option>salon</option>
|
||||||
|
<option>comedor</option><option>integral</option><option>otro</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Calidad</label>
|
||||||
|
<select id="calidad"><option>basica</option><option selected>media</option><option>premium</option></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div><label>m²</label><input id="m2" type="number" value="12" /></div>
|
||||||
|
<div><label>Variaciones</label><input id="n" type="number" value="1" min="1" max="4" /></div>
|
||||||
|
</div>
|
||||||
|
<label>Notas del cliente (separa con ;)</label>
|
||||||
|
<input id="notas" placeholder="encimera de cuarzo; suelo porcelánico claro" />
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div><label>Modelo texto</label><input id="modeloTexto" /></div>
|
||||||
|
<div><label>Modelo imagen</label><input id="modeloImagen" /></div>
|
||||||
|
</div>
|
||||||
|
<div class="hint">Imagen sugerida: google/gemini-2.5-flash-image-preview</div>
|
||||||
|
|
||||||
|
<label><input type="checkbox" id="supervisar" checked style="width:auto;margin-right:6px" />Pasar por el supervisor (puntúa)</label>
|
||||||
|
|
||||||
|
<label>Prompt de imagen directo (opcional — si lo rellenas, se salta el Prompt Builder)</label>
|
||||||
|
<textarea id="promptDirecto" rows="3" placeholder="Photorealistic render of a modern kitchen..."></textarea>
|
||||||
|
|
||||||
|
<details open>
|
||||||
|
<summary>System prompt · Prompt Builder</summary>
|
||||||
|
<textarea id="systemPromptBuilder" rows="10"></textarea>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary>System prompt · Supervisor (avanzado)</summary>
|
||||||
|
<textarea id="supervisorPrompt" rows="8"></textarea>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn-primary" onclick="generar()">Generar</button>
|
||||||
|
<button class="btn-ghost" onclick="guardar()">Guardar config en el worker</button>
|
||||||
|
</div>
|
||||||
|
<div class="status" id="status"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<label>Prompt usado</label>
|
||||||
|
<div class="promptUsado" id="promptUsado">—</div>
|
||||||
|
<div class="grid" id="resultados"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function key(){ return document.getElementById('apiKey').value.trim(); }
|
||||||
|
function guardarKey(){ localStorage.setItem('reformix_funnel_key', key()); setStatus('Key recordada en este navegador.'); }
|
||||||
|
function setStatus(t){ document.getElementById('status').textContent = t || ''; }
|
||||||
|
function headers(){ return { 'Content-Type':'application/json', 'Authorization':'Bearer ' + key() }; }
|
||||||
|
function val(id){ return document.getElementById(id).value; }
|
||||||
|
|
||||||
|
var fotoDataUri = '';
|
||||||
|
function cargarFoto(e){
|
||||||
|
var f = e.target.files[0]; if(!f) return;
|
||||||
|
var r = new FileReader();
|
||||||
|
r.onload = function(){ fotoDataUri = r.result; var img = document.getElementById('fotoPreview'); img.src = fotoDataUri; img.style.display='block'; };
|
||||||
|
r.readAsDataURL(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
function notasArray(){ return val('notas').split(';').map(function(s){return s.trim();}).filter(Boolean); }
|
||||||
|
|
||||||
|
async function cargarConfig(){
|
||||||
|
if(!key()) return;
|
||||||
|
try {
|
||||||
|
var res = await fetch('/sandbox/config', { headers: headers() });
|
||||||
|
if(!res.ok) return;
|
||||||
|
var c = await res.json();
|
||||||
|
if(!val('systemPromptBuilder')) document.getElementById('systemPromptBuilder').value = c.promptBuilder || '';
|
||||||
|
if(!val('supervisorPrompt')) document.getElementById('supervisorPrompt').value = c.supervisor || '';
|
||||||
|
if(!val('modeloTexto')) document.getElementById('modeloTexto').value = c.modeloTexto || '';
|
||||||
|
if(!val('modeloImagen')) document.getElementById('modeloImagen').value = c.modeloImagen || '';
|
||||||
|
setStatus('Config actual cargada (maxRetries=' + c.maxRetries + ', minScore=' + c.minScore + ').');
|
||||||
|
} catch(e){}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generar(){
|
||||||
|
if(!key()) return setStatus('Pon la FUNNEL_API_KEY arriba.');
|
||||||
|
if(!fotoDataUri) return setStatus('Sube una foto "antes".');
|
||||||
|
setStatus('Generando... (puede tardar bastantes segundos por variación)');
|
||||||
|
document.getElementById('resultados').innerHTML = '';
|
||||||
|
document.getElementById('promptUsado').textContent = '—';
|
||||||
|
var body = {
|
||||||
|
imagenAntes: fotoDataUri,
|
||||||
|
tipo: val('tipo'), calidad: val('calidad'),
|
||||||
|
m2: Number(val('m2')) || null, notas: notasArray(),
|
||||||
|
promptDirecto: val('promptDirecto').trim() || null,
|
||||||
|
systemPromptBuilder: val('systemPromptBuilder'),
|
||||||
|
supervisorPrompt: val('supervisorPrompt'),
|
||||||
|
modeloTexto: val('modeloTexto').trim() || null,
|
||||||
|
modeloImagen: val('modeloImagen').trim() || null,
|
||||||
|
nVariaciones: Number(val('n')) || 1,
|
||||||
|
supervisar: document.getElementById('supervisar').checked
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
var res = await fetch('/sandbox/render', { method:'POST', headers: headers(), body: JSON.stringify(body) });
|
||||||
|
var data = await res.json();
|
||||||
|
if(!res.ok){ setStatus('Error: ' + (data.error || res.status)); return; }
|
||||||
|
document.getElementById('promptUsado').textContent = data.promptUsado || '(prompt directo)';
|
||||||
|
pintar(data.variaciones || []);
|
||||||
|
setStatus('Listo: ' + (data.variaciones||[]).length + ' variación(es).');
|
||||||
|
} catch(e){ setStatus('Fallo de red: ' + e.message); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construido con DOM seguro (sin innerHTML de contenido externo): src/textContent no inyectan.
|
||||||
|
function el(tag, props){ var e = document.createElement(tag); if(props) Object.keys(props).forEach(function(k){ e[k] = props[k]; }); return e; }
|
||||||
|
function pintar(vars){
|
||||||
|
var cont = document.getElementById('resultados');
|
||||||
|
cont.replaceChildren();
|
||||||
|
vars.forEach(function(v, i){
|
||||||
|
var card = el('div', { className:'card' });
|
||||||
|
if(v.imagen){ card.appendChild(el('img', { src: v.imagen, alt: 'render ' + (i+1) })); }
|
||||||
|
else { card.appendChild(el('div', { textContent:'Sin imagen', style:'padding:20px;color:#ff5555;font-size:13px' })); }
|
||||||
|
var meta = el('div', { className:'meta' });
|
||||||
|
if(typeof v.score === 'number'){
|
||||||
|
meta.appendChild(el('span', { className:'badge ' + (v.aprobado?'ok':'bad'), textContent:'score ' + v.score + (v.aprobado?' · aprobada':' · rechazada') }));
|
||||||
|
}
|
||||||
|
if(v.motivo){ meta.appendChild(el('div', { className:'motivo', textContent: v.motivo })); }
|
||||||
|
if(v.error){ var er = el('div', { className:'motivo', textContent: v.error }); er.style.color = '#ff5555'; meta.appendChild(er); }
|
||||||
|
var det = el('details'); det.appendChild(el('summary', { textContent:'debug' }));
|
||||||
|
det.appendChild(el('pre', { textContent: JSON.stringify(v.debug||{}, null, 2) }));
|
||||||
|
meta.appendChild(det);
|
||||||
|
card.appendChild(meta);
|
||||||
|
cont.appendChild(card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function guardar(){
|
||||||
|
if(!key()) return setStatus('Pon la FUNNEL_API_KEY arriba.');
|
||||||
|
var body = {
|
||||||
|
promptBuilder: val('systemPromptBuilder'),
|
||||||
|
supervisor: val('supervisorPrompt'),
|
||||||
|
modeloTexto: val('modeloTexto').trim() || undefined,
|
||||||
|
modeloImagen: val('modeloImagen').trim() || undefined
|
||||||
|
};
|
||||||
|
if(!confirm('Guardar estos system prompts y modelos como la config del worker? Afecta a los renders reales de inmediato.')) return;
|
||||||
|
setStatus('Guardando...');
|
||||||
|
try {
|
||||||
|
var res = await fetch('/sandbox/save', { method:'POST', headers: headers(), body: JSON.stringify(body) });
|
||||||
|
var data = await res.json();
|
||||||
|
if(!res.ok){ setStatus('Error guardando: ' + (data.error || res.status)); return; }
|
||||||
|
setStatus('Guardado. El worker ya usa esta config (sin redeploy).');
|
||||||
|
} catch(e){ setStatus('Fallo de red: ' + e.message); }
|
||||||
|
}
|
||||||
|
|
||||||
|
(function init(){
|
||||||
|
var k = localStorage.getItem('reformix_funnel_key');
|
||||||
|
if(k){ document.getElementById('apiKey').value = k; cargarConfig(); }
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
9
mvp/image-worker/src/settings/settings.module.ts
Normal file
9
mvp/image-worker/src/settings/settings.module.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Global, Module } from '@nestjs/common';
|
||||||
|
import { SettingsService } from './settings.service';
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [SettingsService],
|
||||||
|
exports: [SettingsService],
|
||||||
|
})
|
||||||
|
export class SettingsModule {}
|
||||||
98
mvp/image-worker/src/settings/settings.service.ts
Normal file
98
mvp/image-worker/src/settings/settings.service.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
export interface SandboxConfig {
|
||||||
|
promptBuilder: string;
|
||||||
|
supervisor: string;
|
||||||
|
modeloTexto: string;
|
||||||
|
modeloImagen: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DATA_DIR = process.env.SANDBOX_DATA_DIR || path.join(process.cwd(), 'data');
|
||||||
|
const CONFIG_FILE = path.join(DATA_DIR, 'sandbox-config.json');
|
||||||
|
const CAMPOS = ['promptBuilder', 'supervisor', 'modeloTexto', 'modeloImagen'] as const;
|
||||||
|
|
||||||
|
// Config efectiva del pipeline (system prompts + modelos). Arranca de los defaults (ficheros
|
||||||
|
// prompts/*.txt + env) y se superpone lo guardado desde el sandbox, persistido en un volumen
|
||||||
|
// (CONFIG_FILE) para que sobreviva a redeploys. El pipeline real lee de aquí en cada llamada,
|
||||||
|
// así que "guardar" en el sandbox aplica al worker al instante sin redeploy.
|
||||||
|
@Injectable()
|
||||||
|
export class SettingsService {
|
||||||
|
private readonly logger = new Logger(SettingsService.name);
|
||||||
|
private config: SandboxConfig;
|
||||||
|
|
||||||
|
constructor(private readonly env: ConfigService) {
|
||||||
|
this.config = this.cargarDefaults();
|
||||||
|
this.overlayPersistido();
|
||||||
|
}
|
||||||
|
|
||||||
|
private leerPrompt(archivo: string): string {
|
||||||
|
try {
|
||||||
|
const ruta = path.join(process.cwd(), 'prompts', archivo);
|
||||||
|
if (fs.existsSync(ruta)) return fs.readFileSync(ruta, 'utf-8');
|
||||||
|
} catch {
|
||||||
|
/* defaults vacíos si no hay fichero */
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private cargarDefaults(): SandboxConfig {
|
||||||
|
return {
|
||||||
|
promptBuilder: this.leerPrompt('prompt-builder.txt'),
|
||||||
|
supervisor: this.leerPrompt('supervisor.txt'),
|
||||||
|
modeloTexto: this.env.get<string>('OPENROUTER_MODEL_TEXTO', 'anthropic/claude-3.5-haiku-20241022'),
|
||||||
|
modeloImagen: this.env.get<string>('OPENROUTER_MODEL_IMAGEN', 'google/gemini-2.5-flash-image-preview'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private overlayPersistido(): void {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(CONFIG_FILE)) {
|
||||||
|
const saved = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
|
||||||
|
this.config = { ...this.config, ...this.limpiar(saved) };
|
||||||
|
this.logger.log(`Config de sandbox cargada de ${CONFIG_FILE}`);
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
this.logger.warn(`No se pudo leer ${CONFIG_FILE}: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private limpiar(o: any): Partial<SandboxConfig> {
|
||||||
|
const out: Partial<SandboxConfig> = {};
|
||||||
|
for (const k of CAMPOS) {
|
||||||
|
if (typeof o?.[k] === 'string' && o[k].trim()) out[k] = o[k];
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAll(): SandboxConfig {
|
||||||
|
return { ...this.config };
|
||||||
|
}
|
||||||
|
getPromptBuilder(): string {
|
||||||
|
return this.config.promptBuilder;
|
||||||
|
}
|
||||||
|
getSupervisor(): string {
|
||||||
|
return this.config.supervisor;
|
||||||
|
}
|
||||||
|
getModeloTexto(): string {
|
||||||
|
return this.config.modeloTexto;
|
||||||
|
}
|
||||||
|
getModeloImagen(): string {
|
||||||
|
return this.config.modeloImagen;
|
||||||
|
}
|
||||||
|
|
||||||
|
guardar(partial: Partial<SandboxConfig>): SandboxConfig {
|
||||||
|
this.config = { ...this.config, ...this.limpiar(partial) };
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||||
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(this.config, null, 2), 'utf-8');
|
||||||
|
this.logger.log(`Config de sandbox guardada en ${CONFIG_FILE}`);
|
||||||
|
} catch (err: any) {
|
||||||
|
this.logger.error(`No se pudo guardar la config: ${err.message}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
return this.getAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user