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; difficulty: 'easy' | 'medium' | 'hard'; } interface EnhancedPayload { // Campos existentes voice_context?: string; audio_metadata?: { duration: number; sample_rate: number; language: string; }; } const ALLOWED_ORIGINS = [ 'http://localhost:5173', // Vite dev server 'http://localhost:3000', // Caso use outro port 'https://leiturama.ai', // Produção 'https://leiturama.netlify.app' ]; interface StoryResponse { title: string; content: { pages: Array<{ text: string; imagePrompt: string; keywords?: string[]; phonemes?: string[]; syllablePatterns?: string[]; }>; }; metadata?: { targetAge?: number; difficulty?: string; exerciseWords?: { pronunciation?: string[]; formation?: string[]; completion?: string[]; }; }; } serve(async (req) => { const origin = req.headers.get('origin') || ''; const corsHeaders = { 'Cross-Origin-Resource-Policy': 'cross-origin', 'Access-Control-Allow-Origin': ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0], 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, PUT, DELETE', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', 'Access-Control-Max-Age': '86400', // 24 horas 'Cross-Origin-Embedder-Policy': 'credentialless' }; // Preflight request if (req.method === 'OPTIONS') { return new Response('ok', { headers: corsHeaders }) } const { voice_context, ...rest } = await req.json() console.log('[Request]', rest) 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', rest.theme_id).single(), supabase.from('story_subjects').select('*').eq('id', rest.subject_id).single(), supabase.from('story_characters').select('*').eq('id', rest.character_id).single(), supabase.from('story_settings').select('*').eq('id', rest.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: ${rest.theme_id}`); if (!subjectResult.data) throw new Error(`Disciplina não encontrada: ${rest.subject_id}`); if (!characterResult.data) throw new Error(`Personagem não encontrado: ${rest.character_id}`); if (!settingResult.data) throw new Error(`Cenário não encontrado: ${rest.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 = buildPrompt(rest, voice_context); 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 || '{}') as StoryResponse; // Validar estrutura do retorno da IA if (!storyContent.title || !storyContent.content?.pages?.length) { console.error('Resposta da IA:', storyContent); throw new Error('Formato inválido retornado pela IA'); } console.log('[DALL-E] Iniciando geração de imagens...') const pages = await Promise.all( storyContent.content.pages.map(async (page, index) => { console.log(`[DALL-E] Gerando imagem ${index + 1}/${storyContent.content.pages.length}...`) // Gerar imagem com DALL-E const imageResponse = await openai.images.generate({ prompt: `${page.imagePrompt}. For Kids. Educational Purpose. Style inpired by DreamWorks and Pixar. Provide a high quality image. Provide a ethnic diversity.`, n: 1, size: "1024x1024", model: "dall-e-3" }) // 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 = `${rest.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 sem transformações const { data: publicUrl } = supabase .storage .from('story-images') .getPublicUrl(fileName) console.log(`[Storage] Imagem ${index + 1} salva com sucesso`) return { text: page.text, image: publicUrl.publicUrl, // Salvar apenas o caminho do arquivo 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: rest.context, updated_at: new Date().toISOString() }) .eq('id', rest.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: rest.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: rest.id, original_prompt: prompt, ai_response: completion.choices[0].message.content, model_used: 'gpt-4o-mini' }); 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', rest.id) .single(); if (fetchError) throw new Error(`Erro ao buscar história completa: ${fetchError.message}`); // Salvar palavras dos exercícios se existirem if (storyContent.metadata?.exerciseWords) { const exerciseWords = Object.entries(storyContent.metadata.exerciseWords) .flatMap(([type, words]) => (words || []).map(word => { const pageWithWord = storyContent.content.pages .find(p => p.text.toLowerCase().includes(word.toLowerCase())); // Só incluir palavras que realmente existem no texto if (!pageWithWord) return null; return { story_id: rest.id, word, exercise_type: type, phonemes: pageWithWord?.phonemes || null, syllable_pattern: pageWithWord?.syllablePatterns?.[0] || null }; }) ) .filter(Boolean); // Remove null values if (exerciseWords.length > 0) { const { error: wordsError } = await supabase .from('story_exercise_words') .insert(exerciseWords); if (wordsError) { console.error('Erro ao salvar palavras dos exercícios:', wordsError); } } } 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 } ) } }) function buildPrompt(base: StoryPrompt, voice?: string) { return ` Crie uma história educativa para crianças com as seguintes características: Tema: ${base.theme_id} Disciplina: ${base.subject_id} Personagem Principal: ${base.character_id} Cenário: ${base.setting_id} ${base.context ? `Contexto Adicional: ${base.context}` : ''} Requisitos: - História adequada para crianças de 6-12 anos - Conteúdo educativo focado em ${base.subject_id} - 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 ${base.theme_id} - Ambientado em ${base.setting_id} - Personagem principal baseado em ${base.character_id} Requisitos específicos para exercícios: 1. Para o exercício de completar frases: - Selecione 5-8 palavras importantes do texto - Escolha palavras que sejam substantivos, verbos ou adjetivos - Evite artigos, preposições ou palavras muito simples - As palavras devem ser relevantes para o contexto da história 2. Para o exercício de formação de palavras: - Selecione palavras com diferentes padrões silábicos - Inclua palavras que possam ser divididas em sílabas - Priorize palavras com 2-4 sílabas 3. Para o exercício de pronúncia: - Selecione palavras que trabalhem diferentes fonemas - Inclua palavras com encontros consonantais - Priorize palavras que sejam desafiadoras para a faixa etária Formato da resposta: { "title": "Título da História", "content": { "pages": [ { "text": "Texto da página com frases completas...", "imagePrompt": "Descrição para gerar imagem...", "keywords": ["palavra1", "palavra2"], "phonemes": ["fonema1", "fonema2"], "syllablePatterns": ["CV", "CVC", "CCVC"] } ] }, "metadata": { "targetAge": number, "difficulty": string, "exerciseWords": { "pronunciation": ["palavra1", "palavra2"], "formation": ["palavra3", "palavra4"], "completion": ["palavra5", "palavra6"] } } } ${voice ? ` Contexto Adicional por Voz: "${voice}" Diretrizes Adicionais: - Priorizar elementos mencionados na descrição oral - Manter tom e estilo consistentes com a gravação - Incluir palavras-chave identificadas` : ''} `; }