import { serve } from 'https://deno.land/std@0.168.0/http/server.ts' import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' import OpenAI from 'https://esm.sh/openai@4.20.1' const openai = new OpenAI({ apiKey: Deno.env.get('OPENAI_API_KEY') }); interface StoryPrompt { theme_id: string; subject_id: string; character_id: string; setting_id: string; context?: string; } const ALLOWED_ORIGINS = [ 'http://localhost:5173', // Vite dev server 'http://localhost:3000', // Caso use outro port 'https://historiasmagicas.netlify.app' // Produção ]; // Função para otimizar URL da imagem function getOptimizedImageUrl(originalUrl: string, width = 800): string { // Se já for uma URL do Supabase Storage, adicionar transformações if (originalUrl.includes('storage.googleapis.com')) { return `${originalUrl}?width=${width}&quality=80&format=webp`; } return originalUrl; } serve(async (req) => { const origin = req.headers.get('origin') || ''; const corsHeaders = { 'Access-Control-Allow-Origin': ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0], 'Access-Control-Allow-Methods': 'POST, OPTIONS', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', 'Access-Control-Max-Age': '86400', // 24 horas }; // Preflight request if (req.method === 'OPTIONS') { return new Response('ok', { headers: corsHeaders }) } const { record } = await req.json() console.log('[Request]', record) try { const supabase = createClient( Deno.env.get('SUPABASE_URL') ?? '', Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? '' ) console.log('[Supabase] Cliente inicializado') console.log('[DB] Buscando categorias...') const [themeResult, subjectResult, characterResult, settingResult] = await Promise.all([ supabase.from('story_themes').select('*').eq('id', record.theme_id).single(), supabase.from('story_subjects').select('*').eq('id', record.subject_id).single(), supabase.from('story_characters').select('*').eq('id', record.character_id).single(), supabase.from('story_settings').select('*').eq('id', record.setting_id).single() ]) console.log('[DB] Resultados das consultas:', { theme: themeResult, subject: subjectResult, character: characterResult, setting: settingResult }) if (themeResult.error) throw new Error(`Erro ao buscar tema: ${themeResult.error.message}`); if (subjectResult.error) throw new Error(`Erro ao buscar disciplina: ${subjectResult.error.message}`); if (characterResult.error) throw new Error(`Erro ao buscar personagem: ${characterResult.error.message}`); if (settingResult.error) throw new Error(`Erro ao buscar cenário: ${settingResult.error.message}`); if (!themeResult.data) throw new Error(`Tema não encontrado: ${record.theme_id}`); if (!subjectResult.data) throw new Error(`Disciplina não encontrada: ${record.subject_id}`); if (!characterResult.data) throw new Error(`Personagem não encontrado: ${record.character_id}`); if (!settingResult.data) throw new Error(`Cenário não encontrado: ${record.setting_id}`); const theme = themeResult.data; const subject = subjectResult.data; const character = characterResult.data; const setting = settingResult.data; console.log('[Validation] Categorias validadas com sucesso') console.log('[GPT] Construindo prompt...') const prompt = ` Crie uma história educativa para crianças com as seguintes características: Tema: ${theme.title} Disciplina: ${subject.title} Personagem Principal: ${character.title} Cenário: ${setting.title} ${record.context ? `Contexto Adicional: ${record.context}` : ''} Requisitos: - História adequada para crianças de 6-12 anos - Conteúdo educativo focado em ${subject.title} - Linguagem clara e envolvente - 3-5 páginas de conteúdo - Cada página deve ter um texto curto e sugestão para uma imagem - Evitar conteúdo sensível ou inadequado - Incluir elementos de ${theme.title} - Ambientado em ${setting.title} - Personagem principal baseado em ${character.title} Formato da resposta: { "title": "Título da História", "pages": [ { "text": "Texto da página", "image_prompt": "Descrição para gerar a imagem" } ] } ` console.log('[GPT] Prompt construído:', prompt) console.log('[GPT] Iniciando geração da história...') const completion = await openai.chat.completions.create({ model: "gpt-4o-mini", messages: [ { role: "system", content: "Você é um contador de histórias infantis especializado em conteúdo educativo." }, { role: "user", content: prompt } ], temperature: 0.7, max_tokens: 1000 }) console.log('[GPT] História gerada:', completion.choices[0].message) const storyContent = JSON.parse(completion.choices[0].message.content || '{}') // Validar estrutura do retorno da IA if (!storyContent.title || !Array.isArray(storyContent.pages)) { throw new Error('Formato inválido retornado pela IA'); } console.log('[DALL-E] Iniciando geração de imagens...') const pages = await Promise.all( storyContent.pages.map(async (page: any, index: number) => { console.log(`[DALL-E] Gerando imagem ${index + 1}/${storyContent.pages.length}...`) // Gerar imagem com DALL-E const imageResponse = await openai.images.generate({ prompt: `${page.image_prompt}. Style: children's book illustration, colorful, educational, safe for kids. Preferable non-white ethnicity.`, n: 1, size: "1024x1024", model: "dall-e-3", style: "natural" }) // Baixar a imagem do URL do DALL-E console.log(`[Storage] Baixando imagem ${index + 1}...`) const imageUrl = imageResponse.data[0].url const imageRes = await fetch(imageUrl) const imageBuffer = await imageRes.arrayBuffer() // Gerar nome único para o arquivo const fileName = `${record.id}/page-${index + 1}-${Date.now()}.png` // Salvar no Storage do Supabase console.log(`[Storage] Salvando imagem ${index + 1} no bucket...`) const { data: storageData, error: storageError } = await supabase .storage .from('story-images') .upload(fileName, imageBuffer, { contentType: 'image/png', cacheControl: '3600', upsert: false }) if (storageError) { throw new Error(`Erro ao salvar imagem ${index + 1} no storage: ${storageError.message}`) } // Gerar URL público da imagem const { data: publicUrl } = supabase .storage .from('story-images') .getPublicUrl(fileName) console.log(`[Storage] Imagem ${index + 1} salva com sucesso`) // Otimizar URL antes de salvar const optimizedUrl = getOptimizedImageUrl(imageResponse.data[0].url); return { text: page.text, image: optimizedUrl, image_path: fileName } }) ) console.log('[DALL-E] Todas as imagens geradas com sucesso') // Preparar e salvar os dados const { data: story, error: storyError } = await supabase .from('stories') .update({ title: storyContent.title, status: 'published', theme_id: theme.id, subject_id: subject.id, character_id: character.id, setting_id: setting.id, context: record.context, updated_at: new Date().toISOString() }) .eq('id', record.id) .select() .single(); if (storyError) throw new Error(`Erro ao atualizar história: ${storyError.message}`); // Salvar páginas const { error: pagesError } = await supabase .from('story_pages') .insert( pages.map((page, index) => ({ story_id: record.id, page_number: index + 1, text: page.text, image_url: page.image, image_path: page.image_path })) ); if (pagesError) throw new Error(`Erro ao salvar páginas: ${pagesError.message}`); // Salvar metadados da geração const { error: genError } = await supabase .from('story_generations') .insert({ story_id: record.id, original_prompt: prompt, ai_response: completion.choices[0].message.content, model_used: 'gpt-4-turbo' }); if (genError) throw new Error(`Erro ao salvar metadados: ${genError.message}`); // Buscar história completa com relacionamentos const { data: fullStory, error: fetchError } = await supabase .from('stories') .select(` *, pages:story_pages(*) `) .eq('id', record.id) .single(); if (fetchError) throw new Error(`Erro ao buscar história completa: ${fetchError.message}`); return new Response( JSON.stringify({ success: true, message: 'História gerada e salva com sucesso', story: fullStory }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } catch (error) { console.error('[Error] Erro ao gerar história:', error) console.error('[Error] Stack trace:', error.stack) return new Response( JSON.stringify({ error: error.message, stack: error.stack, timestamp: new Date().toISOString() }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 500 } ) } })