Spaces:
Running
Running
| import express from 'express'; | |
| import cors from 'cors'; | |
| import fetch from 'node-fetch'; | |
| import rateLimit from 'express-rate-limit'; | |
| const app = express(); | |
| // ββ CORS & Middleware (MUST come before routes) ββ | |
| const ALLOWED_ORIGINS = (process.env.ALLOWED_ORIGINS || 'https://domify-academy.free.nf') | |
| .split(',') | |
| .map(s => s.trim()) | |
| .filter(Boolean); | |
| app.use(cors({ | |
| origin: (origin, callback) => { | |
| if (!origin) return callback(null, true); | |
| if (ALLOWED_ORIGINS.includes(origin)) return callback(null, true); | |
| return callback(new Error('CORS blocked')); | |
| } | |
| })); | |
| app.use(express.json({ limit: '50kb' })); | |
| app.use('/api/', rateLimit({ windowMs: 15 * 60 * 1000, max: 30 })); | |
| // ββ TTS Configuration ββ | |
| const rawKeys = process.env.NOIZ_API_KEYS || ''; | |
| const NOIZ_API_KEYS = rawKeys ? rawKeys.split(',').map(k => k.trim()).filter(Boolean) : []; | |
| console.log(`π Loaded ${NOIZ_API_KEYS.length} key(s)`); | |
| const NOIZ_BASE = 'https://noiz.ai/v1'; | |
| const VOICE_MAP = { | |
| 'educational-male': '95814add', 'healer-serena': '5a68d66b', | |
| 'naturalist-soren': 'a845c7de', 'mentor-kai': '883b6b7c', | |
| 'mentor-maya': '0e4ab6ec', 'explainer-male': '95814add', | |
| 'narrator-female': '5a68d66b', 'robot-ai': '883b6b7b', | |
| 'dark-narrator': '883b6b7b', 'epic-narrator': '95814add' | |
| }; | |
| const EMOTION_MAP = { | |
| calm: { Neutral: 0.8 }, neutral: { Neutral: 0.8 }, joyful: { Joy: 0.8 }, | |
| happy: { Joy: 0.8 }, sad: { Sadness: 0.8 }, angry: { Anger: 0.8 }, | |
| surprised: { Surprise: 0.8 }, dramatic: { Anger: 0.4, Sadness: 0.4 }, | |
| excited: { Joy: 0.6, Surprise: 0.4 }, urgent: { Anger: 0.5, Surprise: 0.5 }, | |
| mysterious: { Neutral: 0.3, Surprise: 0.7 }, confident: { Joy: 0.3, Neutral: 0.7 }, | |
| fearful: { Sadness: 0.4, Surprise: 0.4 }, | |
| 'π': { Joy: 0.8 }, 'π’': { Sadness: 0.8 }, 'π‘': { Anger: 0.8 }, | |
| 'π²': { Surprise: 0.8 }, 'π€': { Neutral: 0.5 }, 'π₯Ή': { Sadness: 0.5, Joy: 0.3 }, | |
| 'π': { Sadness: 0.6 }, 'πͺ': { Confidence: 0.8 } | |
| }; | |
| // ββ TTS Helper Functions ββ | |
| function clampNumber(value, min, max, fallback) { | |
| const n = Number(value); | |
| if (Number.isNaN(n)) return fallback; | |
| return Math.max(min, Math.min(max, n)); | |
| } | |
| function normalizeVoiceId(tagValue) { | |
| const v = String(tagValue || '').trim(); | |
| return VOICE_MAP[v] || v; | |
| } | |
| function parseCinematicScript(script, defaultVoiceId = 'mentor-kai', defaultSpeed = 1.0) { | |
| const segments = []; | |
| const regex = /\[([^\]]+)\]/g; | |
| let lastIndex = 0; | |
| let currentVoice = defaultVoiceId; | |
| let currentSpeed = clampNumber(defaultSpeed, 0.5, 2.0, 1.0); | |
| let currentEmotion = null; | |
| let match; | |
| while ((match = regex.exec(script)) !== null) { | |
| const tagContent = match[1].trim(); | |
| const tagStart = match.index; | |
| const tagEnd = match.index + match[0].length; | |
| const textBeforeTag = script.slice(lastIndex, tagStart).trim(); | |
| if (textBeforeTag) { | |
| segments.push({ type: 'speech', text: textBeforeTag, voiceId: currentVoice, speed: currentSpeed, emotion: currentEmotion }); | |
| } | |
| const lower = tagContent.toLowerCase(); | |
| if (lower.startsWith('pause:')) { | |
| const duration = clampNumber(lower.split(':')[1], 0, 5000, 500); | |
| segments.push({ type: 'pause', duration }); | |
| } else if (lower.startsWith('voice:')) { | |
| const voiceName = tagContent.slice(tagContent.indexOf(':') + 1).trim(); | |
| if (voiceName) currentVoice = normalizeVoiceId(voiceName); | |
| } else if (lower.startsWith('speed:')) { | |
| const speedValue = tagContent.slice(tagContent.indexOf(':') + 1).trim(); | |
| currentSpeed = clampNumber(speedValue, 0.5, 2.0, currentSpeed); | |
| } else if (EMOTION_MAP[tagContent] || EMOTION_MAP[lower]) { | |
| currentEmotion = EMOTION_MAP[tagContent] || EMOTION_MAP[lower]; | |
| } | |
| lastIndex = tagEnd; | |
| } | |
| const tail = script.slice(lastIndex).trim(); | |
| if (tail) segments.push({ type: 'speech', text: tail, voiceId: currentVoice, speed: currentSpeed, emotion: currentEmotion }); | |
| if (segments.length === 0 && script.trim()) { | |
| segments.push({ type: 'speech', text: script.trim(), voiceId: defaultVoiceId, speed: currentSpeed, emotion: null }); | |
| } | |
| return segments; | |
| } | |
| function isAudioResponse(res) { | |
| const ct = (res.headers.get('content-type') || '').toLowerCase(); | |
| return ct.includes('audio') || ct.includes('mpeg') || ct.includes('mp3'); | |
| } | |
| async function callNoizTTS({ text, voiceId, speed = 1.0, emotion = null }) { | |
| const resolvedVoiceId = normalizeVoiceId(voiceId); | |
| const form = new URLSearchParams(); | |
| form.append('text', text); | |
| form.append('voice_id', resolvedVoiceId); | |
| form.append('output_format', 'mp3'); | |
| form.append('speed', String(speed)); | |
| if (emotion) { | |
| const emo = typeof emotion === 'string' ? emotion : JSON.stringify(emotion); | |
| form.append('emo', emo); | |
| form.append('emotion', emo); | |
| } | |
| const keysToTry = [...NOIZ_API_KEYS, null]; | |
| for (const key of keysToTry) { | |
| const headers = { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'audio/mpeg' }; | |
| if (key) headers['Authorization'] = key; | |
| try { | |
| const res = await fetch(`${NOIZ_BASE}/text-to-speech`, { method: 'POST', headers, body: form.toString(), signal: AbortSignal.timeout(60000) }); | |
| console.log({ status: res.status, contentType: res.headers.get('content-type'), contentLength: res.headers.get('content-length'), mode: key ? 'api' : 'guest' }); | |
| if (!res.ok) { | |
| const errText = await res.text().catch(() => ''); | |
| console.error(`β Noiz error ${res.status}: ${errText}`); | |
| if (res.status === 401 && key) continue; | |
| if (!key) continue; | |
| throw new Error(errText || `Noiz error ${res.status}`); | |
| } | |
| if (!isAudioResponse(res)) { | |
| const body = await res.text().catch(() => ''); | |
| throw new Error(`Expected audio, got: ${body.slice(0, 200)}`); | |
| } | |
| const audioBuffer = Buffer.from(await res.arrayBuffer()); | |
| console.log(`π΅ Audio bytes received: ${audioBuffer.length}`); | |
| if (audioBuffer.length < 500) { console.warn('β οΈ Tiny response, skipping'); continue; } | |
| return audioBuffer; | |
| } catch (err) { | |
| if (err?.name === 'AbortError') { console.warn('β± Request timed out'); continue; } | |
| console.error('π Fetch error:', err.message); | |
| continue; | |
| } | |
| } | |
| throw new Error('All Noiz attempts failed'); | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββ | |
| // ISABELLA CONSULTANT (imports + routes) | |
| // βββββββββββββββββββββββββββββββββββββββββββ | |
| import { searchDuckDuckGo, scrapeSiteKnowledge, buildIsabellaPrompt, callNVIDIA } from './consultant.js'; | |
| const ISABELLA_NVIDIA_KEY = process.env.ISABELLA_NVIDIA_KEY || ''; | |
| const KNOWLEDGE_URLS = [ | |
| 'https://domify-academy.free.nf/about-us', | |
| 'https://domify-academy.free.nf/product-Price', | |
| 'https://domify-academy.free.nf/refund', | |
| 'https://domify-academy.free.nf/privacy', | |
| 'https://domify-academy.free.nf' | |
| ]; | |
| let cachedKnowledge = null; | |
| let knowledgeLastFetched = 0; | |
| async function getKnowledge() { | |
| const now = Date.now(); | |
| if (cachedKnowledge && (now - knowledgeLastFetched) < 30 * 60 * 1000) return cachedKnowledge; | |
| cachedKnowledge = await scrapeSiteKnowledge(KNOWLEDGE_URLS); | |
| knowledgeLastFetched = now; | |
| console.log('π Isabella knowledge base refreshed'); | |
| return cachedKnowledge; | |
| } | |
| app.post('/api/isabella/chat', async (req, res) => { | |
| try { | |
| const { message, userName, pageName, conversationHistory = [] } = req.body; | |
| if (!message) return res.status(400).json({ error: 'Message required' }); | |
| if (!ISABELLA_NVIDIA_KEY) return res.status(500).json({ error: 'Isabella NVIDIA key not configured' }); | |
| const [knowledge, searchResults] = await Promise.all([getKnowledge(), searchDuckDuckGo(message)]); | |
| const systemPrompt = buildIsabellaPrompt(userName || '', pageName || 'unknown', knowledge, searchResults); | |
| const reply = await callNVIDIA(systemPrompt, message, ISABELLA_NVIDIA_KEY, conversationHistory); | |
| let issue = null; | |
| const issueMatch = reply.match(/\[ISSUE:\s*(.+?)\]/); | |
| if (issueMatch) { issue = issueMatch[1]; reply = reply.replace(/\[ISSUE:.*?\]/g, '').trim(); } | |
| res.json({ reply, issue, knowledgeRefreshed: Date.now() - knowledgeLastFetched < 60000 }); | |
| } catch (err) { | |
| console.error('Isabella error:', err.message); | |
| res.status(500).json({ error: 'Isabella is thinking... try again' }); | |
| } | |
| }); | |
| app.post('/api/isabella/refresh-knowledge', async (req, res) => { | |
| cachedKnowledge = await scrapeSiteKnowledge(KNOWLEDGE_URLS); | |
| knowledgeLastFetched = Date.now(); | |
| res.json({ success: true, pages: cachedKnowledge.length }); | |
| }); | |
| // βββββββββββββββββββββββββββββββββββββββββββ | |
| // TTS ENDPOINTS | |
| // βββββββββββββββββββββββββββββββββββββββββββ | |
| app.get('/health', (req, res) => { | |
| res.json({ ok: true, keysLoaded: NOIZ_API_KEYS.length, service: 'Noiz Voice Studio' }); | |
| }); | |
| app.post('/api/generate-voice', async (req, res) => { | |
| const { script, voiceId = 'mentor-kai', speed = 1.0 } = req.body || {}; | |
| if (!script || typeof script !== 'string') return res.status(400).json({ error: 'Missing script' }); | |
| if (script.length > 5000) return res.status(400).json({ error: 'Script too long (max 5000 characters)' }); | |
| try { | |
| const segments = parseCinematicScript(script, voiceId, speed); | |
| console.log(`π Parsed ${segments.length} segment(s)`); | |
| const outputSegments = []; | |
| for (const seg of segments) { | |
| if (seg.type === 'pause') { outputSegments.push({ type: 'pause', duration: seg.duration }); continue; } | |
| console.log(`π€ ${seg.voiceId} | speed=${seg.speed} | emotion=${seg.emotion ? 'yes' : 'no'} | text="${seg.text.slice(0, 50)}"`); | |
| const audioBuffer = await callNoizTTS({ text: seg.text, voiceId: seg.voiceId, speed: seg.speed, emotion: seg.emotion }); | |
| outputSegments.push({ type: 'speech', voiceId: seg.voiceId, speed: seg.speed, emotion: seg.emotion, text: seg.text, audio: audioBuffer.toString('base64'), format: 'mp3' }); | |
| } | |
| const speechCount = outputSegments.filter(s => s.type === 'speech').length; | |
| if (speechCount === 1 && outputSegments.length === 1) { | |
| return res.json({ mode: 'single', format: 'mp3', audio: outputSegments[0].audio, segments: outputSegments }); | |
| } | |
| return res.json({ mode: 'timeline', format: 'mp3', segments: outputSegments }); | |
| } catch (err) { | |
| console.error('β Generation failed:', err.message); | |
| return res.status(500).json({ error: err.message }); | |
| } | |
| }); | |
| app.listen(7860, () => { console.log('π Cinematic Voice Studio ready'); }); |