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)