square4-story-generator/ ├─ public/ │ └─ index.html ├─ server.js ├─ package.json └─ .env (não comites este ficheiro)
// server.js import express from "express"; import cors from "cors"; import multer from "multer"; import fetch from "node-fetch"; import dotenv from "dotenv"; dotenv.config(); const app = express(); app.use(cors()); app.use(express.json({ limit: "12mb" })); // multer para lidar com upload multipart/form-data (pegar buffer da imagem) const upload = multer({ limits: { fileSize: 6 * 1024 * 1024 } }); // limite 6MB por segurança // Usa a variável de ambiente OPENAI_API_KEY — NUNCA coloque a chave no frontend. const OPENAI_API_KEY = process.env.OPENAI_API_KEY; if (!OPENAI_API_KEY) { console.error("ERRO: define OPENAI_API_KEY no .env antes de arrancar o servidor"); process.exit(1); } // Serve ficheiros estáticos (frontend) app.use(express.static("public")); // Endpoint que recebe imagem via multipart/form-data (campo 'image') app.post("/api/gerar-historia", upload.single("image"), async (req, res) => { try { if (!req.file) return res.status(400).json({ error: "Nenhuma imagem recebida" }); // converte buffer para base64 const base64 = req.file.buffer.toString("base64"); const mime = req.file.mimetype || "image/jpeg"; // Prepara o payload para o endpoint Chat Completions (multimodal) // Nota: o formato pode variar conforme a versão da API. Este payload usa a // propriedade messages com partes text + image_url (data URI). const payload = { model: "gpt-4o-mini", // ajusta conforme tua subscrição / disponibilidade messages: [ { role: "system", content: "Você é um escritor sensível e conciso. Crie um parágrafo inicial evocativo em português, inspirado numa imagem." }, { role: "user", content: [ { type: "text", text: "Analise esta imagem e escreva um parágrafo de abertura de uma história. Use descrições sensoriais e atmosfera." }, { type: "image_url", image_url: `data:${mime};base64,${base64}` } ] } ], temperature: 0.8, max_tokens: 350 }; // Chamada à API OpenAI const r = await fetch("https://api.openai.com/v1/chat/completions", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${OPENAI_API_KEY}` }, body: JSON.stringify(payload) }); if (!r.ok) { const errText = await r.text(); console.error("OpenAI retornou erro:", r.status, errText); return res.status(502).json({ error: "Erro ao contactar a OpenAI", details: errText }); } const data = await r.json(); // Extrai texto da resposta (atenção: formatos podem variar) let historia = ""; if (Array.isArray(data.choices) && data.choices.length > 0) { const msg = data.choices[0].message?.content; if (!msg) { // Em alguns formatos, content vem como array de partes const parts = data.choices[0].message?.content; if (Array.isArray(parts)) { historia = parts.map(p => p.text || "").join(" "); } } else if (typeof msg === "string") { historia = msg; } else if (Array.isArray(msg)) { historia = msg.map(p => p.text || "").join(" "); } } if (!historia) historia = "Não foi possível gerar a história (resposta vazia)."; res.json({ historia }); } catch (err) { console.error("Erro interno:", err); res.status(500).json({ error: "Erro interno ao gerar a história" }); } }); // Arranque const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`Servidor a correr em http://localhost:${PORT}`); });
/** * 🌟 Gerador de Histórias com Imagens (ChatGPT + GPT-4o-mini) * Tudo em um só ficheiro: servidor + frontend * by James McSill / ajustado para uso local seguro */ import express from "express"; import cors from "cors"; import fetch from "node-fetch"; const app = express(); app.use(cors()); app.use(express.json({ limit: "10mb" })); // 🧠 TUA CHAVE DA OPENAI (mantém em segredo!) const OPENAI_API_KEY = "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"; // Servir a página HTML diretamente app.get("/", (req, res) => { res.send(` Gerador de Histórias com ChatGPT

✨ Gerador de Histórias com ChatGPT

Envie uma imagem e veja o ChatGPT criar o início de uma história inspirada nela.

Clique aqui para enviar uma imagem (PNG, JPG, GIF)