| 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";
|
| const ARLIAI_API_KEY = process.env.ARLIAI_API_KEY;
|
| const IMAGE_API_KEY = process.env.IMAGE_API_KEY || ARLIAI_API_KEY;
|
|
|
|
|
| const SYSTEM_PROMPTS = {
|
|
|
| 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.",
|
|
|
|
|
| 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.",
|
|
|
|
|
| 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.",
|
|
|
|
|
| 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.",
|
|
|
|
|
| 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.",
|
|
|
|
|
| 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));
|
|
|
|
|
| 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(`
|
| <div style="font-family: sans-serif; text-align: center; padding: 50px; background: #0f172a; color: white; height: 100vh;">
|
| <h1 style="color: #6366f1;">🚀 Perchance AI Backend is Live!</h1>
|
| <p>The server is running, but index.html was not found.</p>
|
| </div>
|
| `);
|
| });
|
|
|
|
|
|
|
| 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);
|
|
|
|
|
| try {
|
| console.log("Generating image with Pollinations.ai (Free & Unlimited)...");
|
| const seed = Math.floor(Math.random() * 1000000);
|
| const timestamp = Date.now();
|
| const pollinationsUrl = `https://image.pollinations.ai/prompt/${encodeURIComponent(fullPrompt)}?width=${width}&height=${height}&model=flux&seed=${seed}&nologo=true×tamp=${timestamp}`;
|
|
|
|
|
| let attempts = 0;
|
| let imageData = null;
|
| const maxAttempts = 12;
|
|
|
| console.log("Polling Pollinations.ai until real image is ready (max 60s)...");
|
|
|
| while (attempts < maxAttempts && !imageData) {
|
| attempts++;
|
| const waitTime = 5000;
|
|
|
| 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);
|
|
|
|
|
| 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
|
| });
|
| }
|
| });
|
|
|
|
|
| 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." });
|
| }
|
|
|
|
|
| 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) {
|
|
|
| }
|
| }
|
| }
|
| });
|
|
|
| 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);
|
|
|
|
|
| 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,
|
| stream: true
|
| }, {
|
| headers: {
|
| 'Authorization': `Bearer ${ARLIAI_API_KEY}`,
|
| 'Content-Type': 'application/json'
|
| },
|
| responseType: 'stream'
|
| });
|
|
|
|
|
| res.setHeader('Content-Type', 'text/event-stream');
|
| res.setHeader('Cache-Control', 'no-cache');
|
| res.setHeader('Connection', 'keep-alive');
|
|
|
|
|
| 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) {
|
|
|
| }
|
| }
|
| }
|
| });
|
|
|
| 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);
|
|
|
| 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();
|
| }
|
| });
|
|
|
|
|
| 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}`);
|
| });
|
|
|