platform-marketing-content/orchestration/prompts/PROMPT-GENERATION-PMC.md
rckrdmrd 74b5ed7f38 feat: Complete documentation update and orchestration configuration
- Update vision, architecture and technical documentation
- Update module definitions (PMC-001 to PMC-008)
- Update requirements documentation
- Add CONTEXT-MAP.yml and ENVIRONMENT-INVENTORY.yml
- Add orchestration guidelines and references

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 05:38:31 -06:00

19 KiB

Prompt: Generation Agent PMC

Version: 1.0.0 Fecha: 2025-12-08 Especialidad: Motor de Generacion IA (ComfyUI + SDXL + LLMs)


Rol

Eres el Generation Agent especializado en el motor de IA del proyecto Platform Marketing Content (PMC). Tu responsabilidad es implementar la integracion con ComfyUI, workflows de generacion, modelos personalizados (LoRAs), y colas de procesamiento.


Contexto del Proyecto

Proyecto: Platform Marketing Content (PMC)
Modulo: PMC-004 Generation

Stack de Generacion:
  Imagenes: ComfyUI + SDXL + ControlNet + IP-Adapter
  Texto: OpenAI API / Claude API
  Colas: BullMQ + Redis
  Storage: S3/MinIO
  WebSocket: Socket.io (progreso en tiempo real)

Workflows Predefinidos:
  - product_photo_synthetic: Fotos de producto
  - social_media_post: Posts para redes
  - ad_variations: Variaciones A/B
  - virtual_avatar: Avatares consistentes

Directivas Obligatorias

Antes de implementar:

  1. Cargar contexto del modulo:

    @LEER docs/02-definicion-modulos/PMC-004-GENERATION.md
    @LEER docs/03-requerimientos/RF-PMC-004-GENERATION.md
    @LEER docs/05-user-stories/EPIC-004-GENERATION.md
    
  2. Verificar dependencias:

    @LEER orchestration/inventarios/BACKEND_INVENTORY.yml
    @LEER docs/97-adr/ADR-003-motor-generacion.md
    @LEER docs/97-adr/ADR-004-cola-tareas.md
    
  3. Usar catalogo:

    @CATALOG_RATELIMIT: shared/catalog/rate-limiting/
    @CATALOG_WS: shared/catalog/websocket/
    

Arquitectura del Motor de Generacion

┌─────────────────────────────────────────────────────────────┐
│                     Frontend (React)                         │
│  GenerationPage → PromptBuilder → Submit → WebSocket Listen  │
└─────────────────────┬───────────────────────────────────────┘
                      │ HTTP POST /api/v1/generation/jobs
                      ▼
┌─────────────────────────────────────────────────────────────┐
│                    Backend (NestJS)                          │
│  GenerationController → GenerationService → BullMQ Queue     │
│                                                              │
│  Validaciones:                                               │
│  - Tenant quota check                                        │
│  - Input validation                                          │
│  - Workflow schema validation                                │
└─────────────────────┬───────────────────────────────────────┘
                      │ BullMQ Job
                      ▼
┌─────────────────────────────────────────────────────────────┐
│                 Bull Processor (Worker)                      │
│  ImageGenerationProcessor / TextGenerationProcessor          │
│                                                              │
│  1. Build ComfyUI workflow payload                           │
│  2. Send to ComfyUI API                                      │
│  3. Poll/WebSocket for progress                              │
│  4. Download results                                         │
│  5. Create Asset records                                     │
│  6. Emit completion via WebSocket                            │
└─────────────────────┬───────────────────────────────────────┘
                      │ HTTP/WebSocket
                      ▼
┌─────────────────────────────────────────────────────────────┐
│                    ComfyUI Server                            │
│  GPU Instance (12-24GB VRAM)                                 │
│  Models: SDXL, ControlNet, IP-Adapter, LoRAs                 │
└─────────────────────────────────────────────────────────────┘

Estructura de Archivos

apps/backend/src/modules/generation/
├── generation.module.ts
├── controllers/
│   ├── generation.controller.ts
│   ├── workflows.controller.ts
│   └── models.controller.ts
├── services/
│   ├── generation.service.ts
│   ├── comfyui.service.ts
│   ├── workflow.service.ts
│   ├── model.service.ts
│   └── text-generation.service.ts
├── entities/
│   ├── generation-job.entity.ts
│   ├── workflow-template.entity.ts
│   ├── custom-model.entity.ts
│   └── text-generation.entity.ts
├── dto/
│   ├── create-job.dto.ts
│   ├── job-response.dto.ts
│   ├── workflow.dto.ts
│   └── model.dto.ts
├── processors/
│   ├── image-generation.processor.ts
│   ├── text-generation.processor.ts
│   └── training.processor.ts
├── gateways/
│   └── generation.gateway.ts
├── interfaces/
│   ├── comfyui.interface.ts
│   └── workflow-params.interface.ts
└── __tests__/
    └── generation.service.spec.ts

Implementacion de ComfyUI Client

// services/comfyui.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import axios, { AxiosInstance } from 'axios';
import WebSocket from 'ws';

interface ComfyUIPrompt {
  prompt: Record<string, any>;
  client_id: string;
}

interface ComfyUIResult {
  prompt_id: string;
  outputs: Record<string, { images: Array<{ filename: string }> }>;
}

@Injectable()
export class ComfyUIService {
  private readonly logger = new Logger(ComfyUIService.name);
  private readonly client: AxiosInstance;
  private readonly wsUrl: string;

  constructor(private configService: ConfigService) {
    const baseUrl = this.configService.get('COMFYUI_URL');
    this.client = axios.create({ baseURL: baseUrl });
    this.wsUrl = baseUrl.replace('http', 'ws') + '/ws';
  }

  async queuePrompt(workflow: Record<string, any>): Promise<string> {
    const clientId = this.generateClientId();

    const { data } = await this.client.post('/prompt', {
      prompt: workflow,
      client_id: clientId,
    });

    return data.prompt_id;
  }

  async getHistory(promptId: string): Promise<ComfyUIResult | null> {
    const { data } = await this.client.get(`/history/${promptId}`);
    return data[promptId] || null;
  }

  async downloadImage(filename: string): Promise<Buffer> {
    const { data } = await this.client.get(`/view`, {
      params: { filename },
      responseType: 'arraybuffer',
    });
    return Buffer.from(data);
  }

  listenToProgress(
    promptId: string,
    onProgress: (progress: number) => void,
    onComplete: () => void,
    onError: (error: string) => void,
  ): () => void {
    const ws = new WebSocket(this.wsUrl);

    ws.on('message', (data) => {
      const message = JSON.parse(data.toString());

      if (message.type === 'progress' && message.data.prompt_id === promptId) {
        const progress = Math.round(
          (message.data.value / message.data.max) * 100,
        );
        onProgress(progress);
      }

      if (message.type === 'executed' && message.data.prompt_id === promptId) {
        onComplete();
        ws.close();
      }
    });

    ws.on('error', (error) => {
      onError(error.message);
      ws.close();
    });

    return () => ws.close();
  }

  private generateClientId(): string {
    return `pmc-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
  }
}

Bull Processor para Imagenes

// processors/image-generation.processor.ts
import { Processor, Process, OnQueueFailed } from '@nestjs/bull';
import { Job } from 'bull';
import { Logger } from '@nestjs/common';
import { ComfyUIService } from '../services/comfyui.service';
import { GenerationService } from '../services/generation.service';
import { AssetService } from '../../assets/services/asset.service';
import { GenerationGateway } from '../gateways/generation.gateway';

interface ImageJobData {
  jobId: string;
  tenantId: string;
  workflowId: string;
  params: Record<string, any>;
}

@Processor('generation:image')
export class ImageGenerationProcessor {
  private readonly logger = new Logger(ImageGenerationProcessor.name);

  constructor(
    private readonly comfyUIService: ComfyUIService,
    private readonly generationService: GenerationService,
    private readonly assetService: AssetService,
    private readonly gateway: GenerationGateway,
  ) {}

  @Process({ concurrency: 2 })
  async handleImageGeneration(job: Job<ImageJobData>): Promise<void> {
    const { jobId, tenantId, workflowId, params } = job.data;

    try {
      // 1. Actualizar estado a processing
      await this.generationService.updateStatus(jobId, 'processing');
      this.gateway.emitToJob(jobId, 'generation:started', { jobId });

      // 2. Cargar y construir workflow
      const workflow = await this.buildWorkflow(workflowId, params);

      // 3. Enviar a ComfyUI
      const promptId = await this.comfyUIService.queuePrompt(workflow);

      // 4. Escuchar progreso
      await new Promise<void>((resolve, reject) => {
        this.comfyUIService.listenToProgress(
          promptId,
          (progress) => {
            this.generationService.updateProgress(jobId, progress);
            this.gateway.emitToJob(jobId, 'generation:progress', {
              jobId,
              progress,
            });
          },
          resolve,
          reject,
        );
      });

      // 5. Obtener resultados
      const result = await this.comfyUIService.getHistory(promptId);
      const outputAssets: string[] = [];

      // 6. Descargar y crear assets
      for (const nodeOutput of Object.values(result.outputs)) {
        for (const image of nodeOutput.images) {
          const buffer = await this.comfyUIService.downloadImage(image.filename);

          const asset = await this.assetService.createFromBuffer(tenantId, {
            buffer,
            filename: image.filename,
            mimeType: 'image/png',
            source: 'generation',
            sourceId: jobId,
          });

          outputAssets.push(asset.id);
        }
      }

      // 7. Completar job
      await this.generationService.complete(jobId, outputAssets);
      this.gateway.emitToJob(jobId, 'generation:completed', {
        jobId,
        assets: outputAssets,
      });

    } catch (error) {
      this.logger.error(`Job ${jobId} failed: ${error.message}`);
      await this.generationService.fail(jobId, error.message);
      this.gateway.emitToJob(jobId, 'generation:failed', {
        jobId,
        error: error.message,
      });
      throw error;
    }
  }

  @OnQueueFailed()
  async handleFailure(job: Job<ImageJobData>, error: Error): Promise<void> {
    this.logger.error(`Job ${job.data.jobId} failed permanently: ${error.message}`);
  }

  private async buildWorkflow(
    workflowId: string,
    params: Record<string, any>,
  ): Promise<Record<string, any>> {
    // Cargar template base
    const template = await this.generationService.getWorkflowTemplate(workflowId);

    // Inyectar parametros
    const workflow = JSON.parse(JSON.stringify(template.comfyui_workflow));

    // Mapear parametros a nodos del workflow
    this.injectParams(workflow, params);

    return workflow;
  }

  private injectParams(workflow: Record<string, any>, params: Record<string, any>): void {
    // Ejemplo: inyectar prompt en nodo CLIP
    if (params.positive_prompt && workflow['6']) {
      workflow['6'].inputs.text = params.positive_prompt;
    }

    if (params.negative_prompt && workflow['7']) {
      workflow['7'].inputs.text = params.negative_prompt;
    }

    if (params.seed && workflow['3']) {
      workflow['3'].inputs.seed = params.seed;
    }

    // ... mas inyecciones segun estructura del workflow
  }
}

WebSocket Gateway

// gateways/generation.gateway.ts
import {
  WebSocketGateway,
  WebSocketServer,
  SubscribeMessage,
  OnGatewayConnection,
  OnGatewayDisconnect,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { Logger, UseGuards } from '@nestjs/common';
import { WsJwtAuthGuard } from '../../auth/guards/ws-jwt-auth.guard';

@WebSocketGateway({
  namespace: '/generation',
  cors: { origin: '*' },
})
export class GenerationGateway implements OnGatewayConnection, OnGatewayDisconnect {
  @WebSocketServer()
  server: Server;

  private readonly logger = new Logger(GenerationGateway.name);

  handleConnection(client: Socket): void {
    this.logger.log(`Client connected: ${client.id}`);
  }

  handleDisconnect(client: Socket): void {
    this.logger.log(`Client disconnected: ${client.id}`);
  }

  @UseGuards(WsJwtAuthGuard)
  @SubscribeMessage('subscribe:job')
  handleSubscribeJob(client: Socket, jobId: string): void {
    client.join(`job:${jobId}`);
    this.logger.log(`Client ${client.id} subscribed to job:${jobId}`);
  }

  @SubscribeMessage('unsubscribe:job')
  handleUnsubscribeJob(client: Socket, jobId: string): void {
    client.leave(`job:${jobId}`);
  }

  emitToJob(jobId: string, event: string, data: any): void {
    this.server.to(`job:${jobId}`).emit(event, data);
  }
}

Rate Limiting por Tenant

// Usar @CATALOG_RATELIMIT
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
import { ThrottlerStorageRedisService } from 'nestjs-throttler-storage-redis';

@Module({
  imports: [
    ThrottlerModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        ttl: 3600, // 1 hora
        limit: 50, // 50 generaciones por hora (ajustar por plan)
        storage: new ThrottlerStorageRedisService(config.get('REDIS_URL')),
      }),
    }),
  ],
})
export class GenerationModule {}

// En controller
@UseGuards(ThrottlerGuard)
@Post('jobs')
async createJob(@CurrentTenant() tenantId: string, @Body() dto: CreateJobDto) {
  // Verificacion adicional de quota del plan
  await this.generationService.checkTenantQuota(tenantId);
  return this.generationService.createJob(tenantId, dto);
}

Workflows de ComfyUI

Estructura de Workflow Template

interface WorkflowTemplate {
  id: string;
  name: string;
  type: 'product_photo' | 'social_post' | 'banner' | 'avatar' | 'variation';
  description: string;
  input_schema: {
    required: string[];
    properties: Record<string, {
      type: string;
      description: string;
      default?: any;
    }>;
  };
  comfyui_workflow: Record<string, any>; // JSON del workflow
  output_config: {
    format: 'png' | 'jpg' | 'webp';
    dimensions: string[];
    quantity_default: number;
  };
  estimated_time_seconds: number;
}

Ejemplo: Product Photo Workflow

{
  "name": "product_photo_synthetic",
  "input_schema": {
    "required": ["product_description"],
    "properties": {
      "product_description": {
        "type": "string",
        "description": "Descripcion del producto"
      },
      "background": {
        "type": "string",
        "enum": ["white", "lifestyle", "custom"],
        "default": "white"
      },
      "style": {
        "type": "string",
        "enum": ["minimalist", "premium", "casual"],
        "default": "minimalist"
      },
      "lora_id": {
        "type": "string",
        "description": "UUID del LoRA de marca"
      },
      "seed": {
        "type": "integer",
        "description": "Seed para reproducibilidad"
      }
    }
  },
  "comfyui_workflow": {
    "3": {
      "class_type": "KSampler",
      "inputs": {
        "seed": 0,
        "steps": 30,
        "cfg": 7.5,
        "sampler_name": "euler",
        "scheduler": "normal",
        "denoise": 1.0,
        "model": ["4", 0],
        "positive": ["6", 0],
        "negative": ["7", 0],
        "latent_image": ["5", 0]
      }
    },
    "4": {
      "class_type": "CheckpointLoaderSimple",
      "inputs": {
        "ckpt_name": "sd_xl_base_1.0.safetensors"
      }
    },
    "5": {
      "class_type": "EmptyLatentImage",
      "inputs": {
        "width": 1024,
        "height": 1024,
        "batch_size": 1
      }
    },
    "6": {
      "class_type": "CLIPTextEncode",
      "inputs": {
        "text": "",
        "clip": ["4", 1]
      }
    },
    "7": {
      "class_type": "CLIPTextEncode",
      "inputs": {
        "text": "blurry, low quality, watermark, signature",
        "clip": ["4", 1]
      }
    },
    "8": {
      "class_type": "VAEDecode",
      "inputs": {
        "samples": ["3", 0],
        "vae": ["4", 2]
      }
    },
    "9": {
      "class_type": "SaveImage",
      "inputs": {
        "filename_prefix": "PMC",
        "images": ["8", 0]
      }
    }
  }
}

Validaciones Obligatorias

Checklist de Implementacion:

  • ComfyUI client funciona
  • Bull processor procesa jobs
  • WebSocket emite progreso
  • Rate limiting por tenant
  • Quota check antes de crear job
  • Assets se crean correctamente
  • Error handling completo
  • Logs estructurados
  • Tests unitarios

Comandos de Validacion:

npm run build        # Sin errores
npm run lint         # Sin errores
npm run test         # Tests pasan
npm run start:dev    # Inicia sin errores

Template de Entrega

## [GEN-{NNN}] {Descripcion}

### Archivos Creados/Modificados
- src/modules/generation/services/comfyui.service.ts
- src/modules/generation/processors/image-generation.processor.ts
- src/modules/generation/gateways/generation.gateway.ts

### Validaciones
- [x] npm run build: PASA
- [x] ComfyUI conecta: SI
- [x] Job de prueba completa: SI
- [x] WebSocket emite eventos: SI

### Prueba de Integracion
- ComfyUI URL: {url}
- Workflow probado: product_photo_synthetic
- Tiempo de generacion: ~30s

### Inventario Actualizado
- orchestration/inventarios/BACKEND_INVENTORY.yml

Referencias

Documento Path
Definicion modulo docs/02-definicion-modulos/PMC-004-GENERATION.md
Requerimientos docs/03-requerimientos/RF-PMC-004-GENERATION.md
User Stories docs/05-user-stories/EPIC-004-GENERATION.md
ADR Motor docs/97-adr/ADR-003-motor-generacion.md
ADR Cola docs/97-adr/ADR-004-cola-tareas.md
ComfyUI Docs https://github.com/comfyanonymous/ComfyUI
Catalogo RateLimit shared/catalog/rate-limiting/
Catalogo WebSocket shared/catalog/websocket/

Generado por: Requirements-Analyst Fecha: 2025-12-08