import OpenAI from 'openai'; import type { IdeaMotriz, Personaje, GuionConductor, Escaleta, PlanoEscaleta, PreEscaleta, CharacterAsset } from '@/types/project'; import { getIdeaMotrizGeneratePrompt, getIdeaMotrizRefinePrompt, getPersonajesGeneratePrompt, getPersonajeRefinePrompt, getPersonajesImprovePrompt, getNewPersonajePrompt, getGuionConductorPrompt, getRegenerateActosPrompt, getEscaletaSegmentoPrompt, getImagePromptsPrompt, getCharacterAssetPromptsPrompt, TPOSE_VARIANTS, EMOTION_VARIANTS, } from '@/lib/prompts'; let openaiClient: OpenAI | null = null; function getOpenAI(): OpenAI { if (!openaiClient) { if (!process.env.OPENAI_API_KEY) { throw new Error('OPENAI_API_KEY no está configurada. Crea un archivo .env.local con tu API key.'); } openaiClient = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, }); } return openaiClient; } async function callLLM(systemPrompt: string, userMessage?: string, options?: { temperature?: number; maxTokens?: number }): Promise { const messages: { role: 'system' | 'user'; content: string }[] = [ { role: 'system', content: systemPrompt }, ]; if (userMessage) { messages.push({ role: 'user', content: userMessage }); } const response = await getOpenAI().chat.completions.create({ model: 'gpt-4o', messages, response_format: { type: 'json_object' }, temperature: options?.temperature ?? 0.8, ...(options?.maxTokens ? { max_tokens: options.maxTokens } : {}), }); const content = response.choices[0]?.message?.content; if (!content) throw new Error('No se generó respuesta'); return content; } // ===================== // IDEA MOTRIZ // ===================== export async function generateIdeaMotriz(userInput: string): Promise { const content = await callLLM(getIdeaMotrizGeneratePrompt(), userInput); return JSON.parse(content) as IdeaMotriz; } export async function refineIdeaMotriz(current: IdeaMotriz, feedback: string): Promise { const content = await callLLM(getIdeaMotrizRefinePrompt(current), feedback, { temperature: 0.7 }); return JSON.parse(content) as IdeaMotriz; } // ===================== // PERSONAJES // ===================== export async function generatePersonajes(ideaMotriz: IdeaMotriz): Promise { const content = await callLLM(getPersonajesGeneratePrompt(ideaMotriz)); const result = JSON.parse(content); return result.personajes as Personaje[]; } export async function refinePersonaje(personaje: Personaje, feedback: string): Promise { const content = await callLLM(getPersonajeRefinePrompt(personaje), feedback, { temperature: 0.7 }); return JSON.parse(content) as Personaje; } export async function improvePersonajes(personajes: Personaje[], feedback: string): Promise { const content = await callLLM(getPersonajesImprovePrompt(personajes), feedback, { temperature: 0.7 }); const result = JSON.parse(content); return result.personajes as Personaje[]; } export async function generateNewPersonaje( datos: { nombre: string; tipo: string; descripcion: string }, ideaMotriz: IdeaMotriz, personajesExistentes: Personaje[] ): Promise { const content = await callLLM(getNewPersonajePrompt(datos, ideaMotriz, personajesExistentes)); const personaje = JSON.parse(content) as Personaje; personaje.id = personaje.id || crypto.randomUUID(); return personaje; } // ===================== // GUIÓN CONDUCTOR // ===================== export async function generateGuionConductor( ideaMotriz: IdeaMotriz, personajes: Personaje[], capituloNumero: number, virtudSugerida?: string ): Promise { const content = await callLLM(getGuionConductorPrompt(ideaMotriz, personajes, capituloNumero, virtudSugerida)); return JSON.parse(content) as GuionConductor; } export async function regenerateGuionActos( guionMetadata: { titulo: string; duracionTotal: string; virtudGanada: string; nudoDelMundo: string; aliado: { nombre: string; descripcion: string }; gemaGanada: string; }, ideaMotriz: IdeaMotriz, personajes: Personaje[], capituloNumero: number ): Promise<{ actos: GuionConductor['actos'] }> { const content = await callLLM(getRegenerateActosPrompt(guionMetadata, ideaMotriz, personajes, capituloNumero)); return JSON.parse(content) as { actos: GuionConductor['actos'] }; } // ===================== // ESCALETA // ===================== export async function generateEscaleta( guion: GuionConductor, ideaMotriz: IdeaMotriz, personajes: Personaje[], preEscaleta: PreEscaleta, onProgress?: (current: number, total: number, label: string) => void ): Promise { const allPlanos: PlanoEscaleta[] = []; let planoCounter = 1; const MAX_PLANOS_PER_CALL = 25; const totalSegments = preEscaleta.segmentos.length; // Use pre-escaleta segments as the source of truth for plano counts for (let segIdx = 0; segIdx < preEscaleta.segmentos.length; segIdx++) { const preSeg = preEscaleta.segmentos[segIdx]; onProgress?.(segIdx, totalSegments, `Segmento ${preSeg.segmentoId} "${preSeg.nombre}"`); // Find matching segment in guion actos const acto = guion.actos?.find(a => a.numero === preSeg.actoNumero); const segmento = acto?.segmentos[preSeg.segmentoIndex]; if (!segmento) continue; const targetPlanos = preSeg.totalPlanos; const duracionSeg = preSeg.duracionSegundos; if (targetPlanos <= MAX_PLANOS_PER_CALL) { const prompt = getEscaletaSegmentoPrompt( segmento, preSeg.actoNumero, preSeg.segmentoIndex, guion, ideaMotriz, personajes, targetPlanos, duracionSeg ); const content = await callLLM(prompt, undefined, { temperature: 0.7, maxTokens: 16000 }); const result = JSON.parse(content); const planos = (result.planos || []).map((p: any, i: number) => ({ ...p, id: `plano-${String(planoCounter + i).padStart(3, '0')}`, tiempoSegundos: p.tiempoSegundos || 5, })); planoCounter += planos.length; allPlanos.push(...planos); } else { // Split into sub-batches for long segments const totalBatches = Math.ceil(targetPlanos / MAX_PLANOS_PER_CALL); let remaining = targetPlanos; for (let batch = 0; batch < totalBatches; batch++) { const planosInBatch = Math.min(MAX_PLANOS_PER_CALL, remaining); const startPlano = targetPlanos - remaining + 1; const prompt = getEscaletaSegmentoPrompt( segmento, preSeg.actoNumero, preSeg.segmentoIndex, guion, ideaMotriz, personajes, targetPlanos, duracionSeg, { batchIndex: batch, totalBatches, planosInThisBatch: planosInBatch, startPlano } ); const content = await callLLM(prompt, undefined, { temperature: 0.7, maxTokens: 16000 }); const result = JSON.parse(content); const planos = (result.planos || []).map((p: any, i: number) => ({ ...p, id: `plano-${String(planoCounter + i).padStart(3, '0')}`, tiempoSegundos: p.tiempoSegundos || 5, })); planoCounter += planos.length; remaining -= planos.length; allPlanos.push(...planos); } } } return { capitulo: guion.titulo, virtudGanada: guion.virtudGanada, duracionTotalSegundos: allPlanos.reduce((sum, p) => sum + (p.tiempoSegundos || 0), 0), planos: allPlanos, } as Escaleta; } // ===================== // PROMPTS PARA IMÁGENES // ===================== export async function generateImagePrompts( escaleta: Escaleta, personajes: Personaje[], onProgress?: (current: number, total: number, label: string) => void ): Promise { const systemPrompt = getImagePromptsPrompt(personajes); const batchSize = 20; const allPlanos: PlanoEscaleta[] = []; const planos = escaleta.planos || []; const totalBatches = Math.ceil(planos.length / batchSize); for (let i = 0; i < planos.length; i += batchSize) { const batchNum = Math.floor(i / batchSize); onProgress?.(batchNum, totalBatches, `Lote ${batchNum + 1}/${totalBatches} (planos ${i + 1}-${Math.min(i + batchSize, planos.length)})`); const batch = planos.slice(i, i + batchSize); const content = await callLLM( systemPrompt, `Genera prompts para estos planos:\n${JSON.stringify(batch, null, 2)}`, { temperature: 0.7 } ); const result = JSON.parse(content); // Merge LLM output with original plano data to preserve personajesDetalle etc. const merged = (result.planos || []).map((llmPlano: any, idx: number) => { const original = batch[idx]; return { ...original, ...llmPlano, personajesDetalle: original?.personajesDetalle || llmPlano.personajesDetalle }; }); allPlanos.push(...merged); } return allPlanos; } // ===================== // CHARACTER ASSET PROMPTS (Nano Banana Pro) // ===================== export async function generateCharacterAssetPrompts( personaje: Personaje ): Promise { const systemPrompt = getCharacterAssetPromptsPrompt(personaje); const content = await callLLM( systemPrompt, `Genera el character sheet completo para ${personaje.nombre} (${personaje.especie}). Incluye 3 T-poses y 6 emociones.`, { temperature: 0.7 } ); const result = JSON.parse(content); const assets: CharacterAsset[] = (result.assets || []).map((a: any, i: number) => { const variantInfo = a.type === 'tpose' ? TPOSE_VARIANTS.find(v => v.variant === a.variant) : EMOTION_VARIANTS.find(v => v.variant === a.variant); return { id: `${personaje.id}-${a.type}-${a.variant}`, type: a.type, variant: a.variant, label: variantInfo?.label || a.variant, prompt: a.prompt, status: 'prompt_ready' as const, }; }); return assets; }