Download server.js from Isshi14/artsy: direct link, hf CLI and curl.
- Browser
- Download file 8.15 kB
-
https://huggingface.co/spaces/Isshi14/artsy/resolve/main/server.js
- Command line
-
hf download hf://spaces/Isshi14/artsy/server.js
-
curl -L -o server.js https://huggingface.co/spaces/Isshi14/artsy/resolve/main/server.js
8.15 kB
| import dotenv from 'dotenv'; | |
| import express from 'express'; | |
| import { readFile } from 'node:fs/promises'; | |
| import { readdirSync, existsSync } from 'node:fs'; | |
| import path from 'node:path'; | |
| import { fileURLToPath } from 'node:url'; | |
| import { GoogleGenAI } from '@google/genai'; | |
| const __filename = fileURLToPath(import.meta.url); | |
| const __dirname = path.dirname(__filename); | |
| dotenv.config({ path: path.join(__dirname, 'local.env') }); | |
| const app = express(); | |
| const port = process.env.PORT || 3000; | |
| const modelName = process.env.GEMINI_MODEL || 'gemini-2.5-flash'; | |
| app.use(express.json({ limit: '7mb' })); | |
| app.use(express.static(path.join(__dirname, 'public'))); | |
| app.get('/api/health', (_req, res) => { | |
| res.json({ | |
| ok: true, | |
| model: modelName, | |
| hasGeminiKey: Boolean(process.env.GEMINI_API_KEY) | |
| }); | |
| }); | |
| app.post('/api/generate', async (req, res) => { | |
| try { | |
| if (!process.env.GEMINI_API_KEY) { | |
| return res.status(400).json({ | |
| error: 'Missing GEMINI_API_KEY. Add it to local.env locally and to Railway variables before deployment.' | |
| }); | |
| } | |
| const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY }); | |
| const { | |
| artworkName = '', | |
| mode = 'Beginner', | |
| question = '', | |
| image, | |
| useKnowledgeBase = false | |
| } = req.body || {}; | |
| if (!artworkName.trim() && !question.trim() && !image?.data) { | |
| return res.status(400).json({ error: 'Please enter an artwork name, ask a question, or upload an image.' }); | |
| } | |
| const query = [artworkName, question].filter(Boolean).join(' '); | |
| const context = useKnowledgeBase ? await retrieveContext(query) : ''; | |
| const prompt = buildPrompt({ artworkName, mode, question, context, hasImage: Boolean(image?.data) }); | |
| const contents = buildContents({ prompt, image }); | |
| const response = await ai.models.generateContent({ | |
| model: modelName, | |
| contents | |
| }); | |
| res.json({ | |
| answer: response.text, | |
| usedKnowledgeBase: Boolean(context), | |
| model: modelName | |
| }); | |
| } catch (error) { | |
| console.error(error); | |
| res.status(500).json({ | |
| error: error.message || 'The model call failed. Check your API key, network, and server logs.' | |
| }); | |
| } | |
| }); | |
| app.post('/api/followup', async (req, res) => { | |
| try { | |
| if (!process.env.GEMINI_API_KEY) { | |
| return res.status(400).json({ | |
| error: 'Missing GEMINI_API_KEY. Add it to local.env locally and to Railway variables before deployment.' | |
| }); | |
| } | |
| const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY }); | |
| const { | |
| artworkName = '', | |
| mode = 'Beginner', | |
| originalQuestion = '', | |
| originalAnswer = '', | |
| followupQuestion = '', | |
| image, | |
| useKnowledgeBase = false | |
| } = req.body || {}; | |
| if (!followupQuestion.trim()) { | |
| return res.status(400).json({ error: 'Please enter a follow-up question.' }); | |
| } | |
| if (!originalAnswer.trim()) { | |
| return res.status(400).json({ error: 'Please generate the main artwork explanation first.' }); | |
| } | |
| const query = [artworkName, originalQuestion, followupQuestion].filter(Boolean).join(' '); | |
| const context = useKnowledgeBase ? await retrieveContext(query) : ''; | |
| const prompt = buildFollowupPrompt({ | |
| artworkName, | |
| mode, | |
| originalQuestion, | |
| originalAnswer, | |
| followupQuestion, | |
| context, | |
| hasImage: Boolean(image?.data) | |
| }); | |
| const contents = buildContents({ prompt, image }); | |
| const response = await ai.models.generateContent({ | |
| model: modelName, | |
| contents | |
| }); | |
| res.json({ | |
| answer: response.text, | |
| usedKnowledgeBase: Boolean(context), | |
| model: modelName | |
| }); | |
| } catch (error) { | |
| console.error(error); | |
| res.status(500).json({ | |
| error: error.message || 'The follow-up model call failed. Check your API key, network, and server logs.' | |
| }); | |
| } | |
| }); | |
| function buildPrompt({ artworkName, mode, question, context, hasImage }) { | |
| const sourceBlock = context | |
| ? `Use this source material when it is relevant. If it conflicts with the visible artwork or widely known art history, explain the uncertainty simply.\\n\\nSOURCE MATERIAL:\\n${context}\\n\\n` | |
| : ''; | |
| const systemPrompt = `You are Explain Art to Me, an empathetic art guide for people without formal art training. | |
| Your job is to explain artwork in simple, friendly, emotionally relatable language. Avoid unnecessary art jargon. If a technical term is useful, define it in plain English. Do not pretend to know facts that are uncertain; say when you are making a careful guess from the image or title. Keep the first explanation warm but fairly compact so it is easy to scan. | |
| Explanation mode: ${mode} | |
| Artwork title or clue: ${artworkName || hasImage ? artworkName || 'Uploaded image' : 'Not provided'}`; | |
| return `${sourceBlock}${systemPrompt} | |
| User question: | |
| ${question || 'Explain this artwork in a friendly way.'} | |
| Respond with: | |
| 1. Artist in brief | |
| 2. What you are looking at | |
| 3. Emotions and symbols | |
| 4. Why it is famous | |
| 5. Personal or historical context | |
| Use plain text section labels exactly like "Artist in brief:" without Markdown heading marks, asterisks, or bullet symbols. | |
| Keep each section to 2 or 3 concise sentences. | |
| End with one short line that helps the user look at the artwork again with fresh eyes.`; | |
| } | |
| function buildFollowupPrompt({ | |
| artworkName, | |
| mode, | |
| originalQuestion, | |
| originalAnswer, | |
| followupQuestion, | |
| context, | |
| hasImage | |
| }) { | |
| const sourceBlock = context | |
| ? `Use this source material when it is relevant. If it conflicts with the visible artwork, the previous answer, or widely known art history, explain the uncertainty simply.\\n\\nSOURCE MATERIAL:\\n${context}\\n\\n` | |
| : ''; | |
| return `${sourceBlock}You are Explain Art to Me, an empathetic art guide for people without formal art training. | |
| Continue the same simple, friendly, emotionally relatable tone from the initial explanation. Answer the user's follow-up as part of an art conversation. Avoid unnecessary art jargon. If a technical term is useful, define it in plain English. If the answer is uncertain, say so gently. | |
| Explanation mode: ${mode} | |
| Artwork title or clue: ${artworkName || hasImage ? artworkName || 'Uploaded image' : 'Not provided'} | |
| Original user question: | |
| ${originalQuestion || 'Explain this artwork in a friendly way.'} | |
| Initial explanation: | |
| ${originalAnswer} | |
| Follow-up question: | |
| ${followupQuestion} | |
| Respond with a focused answer to the follow-up question in 2 to 4 short paragraphs. Use plain text only, without Markdown heading marks, asterisks, or bullet symbols.`; | |
| } | |
| function buildContents({ prompt, image }) { | |
| if (!image?.data || !image?.mimeType) return prompt; | |
| return [ | |
| { | |
| role: 'user', | |
| parts: [ | |
| { text: prompt }, | |
| { | |
| inlineData: { | |
| mimeType: image.mimeType, | |
| data: image.data | |
| } | |
| } | |
| ] | |
| } | |
| ]; | |
| } | |
| async function retrieveContext(query) { | |
| const docsDir = path.join(__dirname, 'docs'); | |
| if (!existsSync(docsDir)) return ''; | |
| const files = readdirSync(docsDir).filter((name) => /\\.(md|txt)$/i.test(name)); | |
| const chunks = []; | |
| for (const file of files) { | |
| const text = await readFile(path.join(docsDir, file), 'utf8'); | |
| for (const chunk of chunkText(text)) { | |
| chunks.push({ file, chunk, score: scoreChunk(query, chunk) }); | |
| } | |
| } | |
| return chunks | |
| .filter((item) => item.score > 0) | |
| .sort((a, b) => b.score - a.score) | |
| .slice(0, 3) | |
| .map((item) => `[${item.file}]\\n${item.chunk}`) | |
| .join('\\n\\n---\\n\\n'); | |
| } | |
| function chunkText(text) { | |
| return text | |
| .split(/\\n\\s*\\n/g) | |
| .map((chunk) => chunk.trim()) | |
| .filter(Boolean) | |
| .slice(0, 30); | |
| } | |
| function scoreChunk(query, chunk) { | |
| const queryTerms = new Set( | |
| query | |
| .toLowerCase() | |
| .split(/[^a-z0-9]+/) | |
| .filter((term) => term.length > 3) | |
| ); | |
| if (!queryTerms.size) return 0; | |
| const lowerChunk = chunk.toLowerCase(); | |
| let score = 0; | |
| for (const term of queryTerms) { | |
| if (lowerChunk.includes(term)) score += 1; | |
| } | |
| return score; | |
| } | |
| app.listen(port, () => { | |
| console.log(`App running on http://localhost:${port}`); | |
| }); | |