aichat / server.js
kspchary's picture
Upload server.js
33b32b7 verified
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(`
<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>
`);
});
// 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&timestamp=${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}`);
});