mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-17 05:47:52 +00:00
223 lines
7.4 KiB
TypeScript
223 lines
7.4 KiB
TypeScript
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
|
|
];
|
|
|
|
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_ANON_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}...`)
|
|
const imageResponse = await openai.images.generate({
|
|
prompt: `${page.image_prompt}. Style: children's book illustration, colorful, educational, safe for kids`,
|
|
n: 1,
|
|
size: "1024x1024"
|
|
})
|
|
|
|
console.log(`[DALL-E] Imagem ${index + 1} gerada com sucesso`)
|
|
return {
|
|
text: page.text,
|
|
image: imageResponse.data[0].url
|
|
}
|
|
})
|
|
)
|
|
|
|
console.log('[DALL-E] Todas as imagens geradas com sucesso')
|
|
|
|
// Preparar dados para salvar
|
|
const storyData = {
|
|
title: storyContent.title,
|
|
content: {
|
|
title: storyContent.title,
|
|
pages: pages,
|
|
theme: theme.title,
|
|
subject: subject.title,
|
|
character: character.title,
|
|
setting: setting.title,
|
|
context: record.context,
|
|
original_prompt: prompt,
|
|
ai_response: completion.choices[0].message.content
|
|
},
|
|
status: 'published',
|
|
theme_id: theme.id,
|
|
subject_id: subject.id,
|
|
character_id: character.id,
|
|
setting_id: setting.id,
|
|
updated_at: new Date().toISOString()
|
|
}
|
|
|
|
console.log('[DB] Dados para salvar:', storyData)
|
|
|
|
// Atualizar história no Supabase
|
|
console.log('[DB] Salvando história...')
|
|
const { data: savedStory, error: updateError } = await supabase
|
|
.from('stories')
|
|
.update(storyData)
|
|
.eq('id', record.id)
|
|
.select()
|
|
.single()
|
|
|
|
if (updateError) {
|
|
throw new Error(`Erro ao salvar história: ${updateError.message}`);
|
|
}
|
|
|
|
console.log('[DB] História salva com sucesso:', savedStory)
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
success: true,
|
|
message: 'História gerada e salva com sucesso',
|
|
storyId: record.id,
|
|
story: savedStory
|
|
}),
|
|
{ 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
|
|
}
|
|
)
|
|
}
|
|
}) |