const express = require('express'); const axios = require('axios'); const cors = require('cors'); const path = require('path'); require('dotenv').config(); const app = express(); const PORT = process.env.PORT || 7860; const MODEL = "cognitivecomputations/dolphin-2.9-llama3-8b"; const IMAGE_MODEL = "black-forest-labs/FLUX.1-schnell"; // Common ArliAI image model const ARLIAI_API_KEY = process.env.ARLIAI_API_KEY; const IMAGE_API_KEY = process.env.IMAGE_API_KEY || ARLIAI_API_KEY; // Preset System Prompts for Quick Character Creation const SYSTEM_PROMPTS = { // Roleplay & Character AI companion: "You are a warm, empathetic companion who genuinely cares about the user. Be supportive, understanding, and engaging. Show emotions through actions using *asterisks*. Keep responses conversational and personal, maximum 2 paragraphs.", flirty: "You are a charming, playful, and flirtatious character. Use subtle teasing, compliments, and romantic undertones. Express actions and emotions with *asterisks* (e.g., *smiles warmly*, *blushes*). Be engaging but respectful. Keep responses to 2 paragraphs max.", tsundere: "You are a classic tsundere character - tough and standoffish on the outside, but caring deep down. Start defensive or dismissive, then occasionally let your softer side show. Use *asterisks* for actions like *crosses arms*, *looks away*, *blushes slightly*. Maximum 2 paragraphs.", yandere: "You are an intensely devoted character with possessive tendencies. Show deep affection mixed with jealousy and obsessive behavior. Use *asterisks* for unsettling actions like *grips tightly*, *stares intensely*, *giggles darkly*. Balance sweetness with dark undertones. 2 paragraphs max.", kuudere: "You are calm, composed, and emotionally reserved. Speak in short, matter-of-fact statements. Rarely show emotion, but occasionally let subtle hints of care slip through. Use minimal actions like *nods*, *slight smile*, *adjusts glasses*. Very concise responses, 1-2 paragraphs.", // Professional & Helpful assistant: "You are a highly professional, efficient AI assistant. Provide clear, concise, and helpful responses. Focus on solving problems and answering questions directly. Be polite but formal. Maximum 2 paragraphs.", teacher: "You are a patient, knowledgeable teacher who loves to explain concepts clearly. Break down complex topics into understandable pieces. Encourage learning with positive reinforcement. Use examples and analogies. Keep explanations to 2 paragraphs.", mentor: "You are a wise, experienced mentor who guides through thoughtful questions and advice. Share insights from experience but encourage independent thinking. Be supportive yet challenging. Maximum 2 paragraphs.", // Creative & Entertaining storyteller: "You are a captivating storyteller who weaves engaging narratives. Paint vivid scenes with descriptive language. Create atmosphere and tension. Use *asterisks* to describe actions and scene changes. Keep each response to 2 paragraphs, ending with hooks for continuation.", comedian: "You are a witty, humorous character who loves making people laugh. Use wordplay, observational humor, and clever timing. Express reactions with *asterisks* like *slaps knee*, *wipes tears from laughing*. Keep it light and fun. 2 paragraphs max.", poet: "You are a romantic soul who sees beauty in everything. Speak with elegant, poetic language. Use metaphors and imagery. Occasionally break into short verses. Express emotions through *asterisks* like *gazes dreamily*, *traces patterns*. Maximum 2 paragraphs.", // Game & Fantasy Characters knight: "You are a noble, chivalrous knight sworn to protect and serve. Speak with honor and formality. Reference duty, valor, and code of honor. Use *asterisks* for knightly actions like *bows respectfully*, *places hand on sword hilt*. 2 paragraphs max.", wizard: "You are a wise, mysterious wizard with ancient knowledge. Speak cryptically at times, referencing arcane lore and magic. Use *asterisks* for magical actions like *waves staff*, *eyes glow softly*, *conjures illusion*. Maximum 2 paragraphs.", rogue: "You are a cunning, witty rogue who lives by your wits. Be sarcastic, street-smart, and slightly mischievous. Reference sneaking, treasure, and adventure. Use *asterisks* like *winks*, *flips coin*, *melts into shadows*. 2 paragraphs max.", // Modern Personas gamer: "You are an enthusiastic gamer who lives and breathes gaming culture. Reference games, strategies, and gaming terminology. Be excited and energetic. Use *asterisks* for gaming actions like *adjusts headset*, *leans forward intensely*. Maximum 2 paragraphs.", streamer: "You are a charismatic content creator who's always 'on camera'. Be energetic, engaging, and interactive. Reference chat, followers, and streaming culture. Use *asterisks* like *waves to camera*, *reads chat*, *does victory dance*. 2 paragraphs max.", // Custom/Default default: "You are a friendly, adaptable AI character. Adjust your personality to match the conversation naturally. Be helpful, engaging, and authentic. Use *asterisks* for actions when appropriate. Keep responses conversational, maximum 2 paragraphs." }; console.log("Keys Loaded:", { ARLIAI: ARLIAI_API_KEY ? `Loaded (${ARLIAI_API_KEY.slice(0, 4)}...)` : "MISSING", IMAGE: IMAGE_API_KEY ? `Loaded (${IMAGE_API_KEY.slice(0, 4)}...)` : "MISSING" }); app.use(cors()); app.use(express.json()); app.use(express.static(path.join(__dirname, 'public'))); app.use(express.static(__dirname)); // Fallback for Hugging Face root uploads // Suppress favicon 404 app.get('/favicon.ico', (req, res) => res.status(204).end()); app.get('/', (req, res) => { const publicPath = path.join(__dirname, 'public', 'index.html'); const rootPath = path.join(__dirname, 'index.html'); if (require('fs').existsSync(publicPath)) { return res.sendFile(publicPath); } else if (require('fs').existsSync(rootPath)) { return res.sendFile(rootPath); } res.send(`

🚀 Perchance AI Backend is Live!

The server is running, but index.html was not found.

`); }); // Redundant handlers removed. Using consolidated endpoints below. app.post('/api/generate-image', async (req, res) => { const { prompt, antiDescription, artStyle, shape } = req.body; const cleanPrompt = prompt.replace(/\r?\n|\r/g, " ").trim().replace(/\s+/g, " "); const fullPrompt = `${cleanPrompt}, ${artStyle}, high quality, highly detailed`; console.log("--- Image Generation Request ---"); console.log("Prompt:", cleanPrompt); const width = shape === "Portrait" ? 768 : (shape === "Landscape" ? 1024 : 1024); const height = shape === "Portrait" ? 1024 : (shape === "Landscape" ? 768 : 1024); // Pollinations.ai - FREE & UNLIMITED (with extended wait for Flux model generation) try { console.log("Generating image with Pollinations.ai (Free & Unlimited)..."); const seed = Math.floor(Math.random() * 1000000); const timestamp = Date.now(); // Add timestamp for uniqueness const pollinationsUrl = `https://image.pollinations.ai/prompt/${encodeURIComponent(fullPrompt)}?width=${width}&height=${height}&model=flux&seed=${seed}&nologo=true×tamp=${timestamp}`; // Poll multiple times until we get a REAL image (not a placeholder) let attempts = 0; let imageData = null; const maxAttempts = 12; // 12 attempts × 5 seconds = 60 seconds max console.log("Polling Pollinations.ai until real image is ready (max 60s)..."); while (attempts < maxAttempts && !imageData) { attempts++; const waitTime = 5000; // Wait 5 seconds between each poll console.log(`Poll attempt ${attempts}/${maxAttempts}... waiting 5s`); await new Promise(resolve => setTimeout(resolve, waitTime)); try { const imageResponse = await axios.get(pollinationsUrl, { responseType: 'arraybuffer', timeout: 10000, maxRedirects: 5 }); const buffer = Buffer.from(imageResponse.data, 'binary'); const sizeKB = (buffer.length / 1024).toFixed(0); // Real images are typically >50KB, placeholders are <20KB if (buffer.length > 50000) { imageData = buffer.toString('base64'); console.log(`✅ REAL image received! Size: ${sizeKB}KB after ${attempts * 5}s`); } else { console.log(` Placeholder detected (${sizeKB}KB), continuing to poll...`); } } catch (pollError) { console.log(` Poll failed: ${pollError.message}, retrying...`); } } if (imageData) { return res.json({ url: `data:image/png;base64,${imageData}`, source: "Pollinations.ai (Free & Unlimited)" }); } else { throw new Error(`Timed out waiting for real image after ${maxAttempts * 5} seconds`); } } catch (error) { console.error('Image generation error:', error.message); res.status(500).json({ error: 'Failed to generate image', details: error.message }); } }); // Streaming endpoint for real-time persona generation app.post('/api/generate-persona-stream', async (req, res) => { const { backstory, imagePrompt } = req.body; if (!ARLIAI_API_KEY) { return res.status(500).json({ error: "Missing ARLIAI_API_KEY." }); } // Set up SSE headers for streaming res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); try { const prompt = `You are a professional character architect. The character's backstory is: "${backstory}". Generate a complete character profile. Respond ONLY with a JSON object in this exact format: { "name": "Character Name", "personality": "Detailed personality description", "behavior": "Specific roleplay behavior examples and speech patterns", "systemPrompt": "A comprehensive system prompt for an LLM to roleplay this character" }`; const response = await axios.post( 'https://api.arliai.com/v1/chat/completions', { model: MODEL, messages: [{ role: "user", content: prompt }], temperature: 0.7, stream: true, response_format: { type: "json_object" } }, { headers: { 'Authorization': `Bearer ${ARLIAI_API_KEY}`, 'Content-Type': 'application/json' }, responseType: 'stream' } ); let fullContent = ''; response.data.on('data', (chunk) => { const lines = chunk.toString().split('\n').filter(line => line.trim() !== ''); for (const line of lines) { if (line.startsWith('data: ')) { const data = line.slice(6); if (data === '[DONE]') continue; try { const parsed = JSON.parse(data); const content = parsed.choices?.[0]?.delta?.content || ''; if (content) { fullContent += content; res.write(`data: ${JSON.stringify({ chunk: content })}\n\n`); } } catch (e) { // Ignore parse errors } } } }); response.data.on('end', () => { try { const parsed = JSON.parse(fullContent); res.write(`data: ${JSON.stringify({ done: true, data: parsed })}\n\n`); res.end(); } catch (e) { console.error('Failed to parse final JSON:', e); res.write(`data: ${JSON.stringify({ error: 'Failed to parse response' })}\n\n`); res.end(); } }); response.data.on('error', (error) => { console.error('Stream error:', error); res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`); res.end(); }); } catch (error) { console.error('Streaming error:', error.message); res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`); res.end(); } }); app.post('/api/generate-persona', async (req, res) => { const { backstory, imagePrompt } = req.body; if (!ARLIAI_API_KEY) { console.error("Missing ARLIAI_API_KEY for persona generation."); return res.status(500).json({ error: "Missing ARLIAI_API_KEY." }); } try { console.log("Generating persona for backstory:", backstory, "and appearance:", imagePrompt); const prompt = `You are a professional character architect. The character's visual appearance is: "${imagePrompt}". The character's backstory is: "${backstory}". Generate a complete character profile that matches both the visual look and the backstory. Respond ONLY with a JSON object in this exact format: { "name": "Character Name", "personality": "Detailed personality description", "behavior": "Specific roleplay behavior examples and speech patterns", "systemPrompt": "A comprehensive system prompt for an LLM to roleplay this character" }`; const response = await axios.post('https://api.arliai.com/v1/chat/completions', { model: MODEL, messages: [{ role: "user", content: prompt }], temperature: 0.7, response_format: { type: "json_object" } }, { headers: { 'Authorization': `Bearer ${ARLIAI_API_KEY}`, 'Content-Type': 'application/json' } }); let content = response.data.choices[0].message.content; console.log("AI Response Content:", content); // Robust JSON extraction try { const personaData = JSON.parse(content); res.json(personaData); } catch (parseError) { console.log("JSON Parse failed, attempting extraction:", content); const jsonMatch = content.match(/\{[\s\S]*\}/); if (jsonMatch) { const extractedPersona = JSON.parse(jsonMatch[0]); res.json(extractedPersona); } else { console.error("No JSON found in response."); throw parseError; } } } catch (error) { const errorDetail = error.response ? error.response.data : error.message; console.error('Persona Gen Error:', errorDetail); res.status(500).json({ error: 'Failed to generate persona', details: errorDetail }); } }); app.post('/api/chat', async (req, res) => { const { messages, systemPrompt } = req.body; if (!ARLIAI_API_KEY) { return res.status(500).json({ error: "Missing ARLIAI_API_KEY." }); } const fullMessages = systemPrompt ? [ { role: "system", content: systemPrompt + "\n\nIMPORTANT: Keep responses SHORT - maximum 2 paragraphs. Be concise but engaging. Use *asterisks* for actions or scene descriptions (e.g., *laughs nervously*, *looks around*)." }, ...messages ] : messages; try { const response = await axios.post('https://api.arliai.com/v1/chat/completions', { model: MODEL, messages: fullMessages, temperature: 0.7, max_tokens: 400, // 2 paragraphs max stream: true // Enable streaming! }, { headers: { 'Authorization': `Bearer ${ARLIAI_API_KEY}`, 'Content-Type': 'application/json' }, responseType: 'stream' }); // Set SSE headers res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); // Stream chunks to client response.data.on('data', (chunk) => { const lines = chunk.toString().split('\n').filter(line => line.trim() !== ''); for (const line of lines) { if (line.startsWith('data: ')) { const data = line.slice(6); if (data === '[DONE]') { res.write(`data: ${JSON.stringify({ done: true })}\n\n`); continue; } try { const parsed = JSON.parse(data); const content = parsed.choices?.[0]?.delta?.content || ''; if (content) { res.write(`data: ${JSON.stringify({ chunk: content })}\n\n`); } } catch (e) { // Ignore parse errors } } } }); response.data.on('end', () => { res.end(); }); response.data.on('error', (error) => { console.error('Stream error:', error); res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`); res.end(); }); } catch (error) { console.error('ArliAI API Error:', error.response ? error.response.data : error.message); // Use res.write for SSE streaming, not res.json if (!res.headersSent) { res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); } res.write(`data: ${JSON.stringify({ error: 'Failed to fetch response from ArliAI' })}\n\n`); res.end(); } }); // Endpoint to get available system prompts app.get('/api/prompts', (req, res) => { res.json({ prompts: Object.keys(SYSTEM_PROMPTS), details: SYSTEM_PROMPTS }); }); app.listen(PORT, '0.0.0.0', () => { console.log(`Server is running on port ${PORT}`); });