Domify commited on
Commit
e5961fe
Β·
verified Β·
1 Parent(s): 54eb7de

Update index.js

Browse files
Files changed (1) hide show
  1. index.js +192 -349
index.js CHANGED
@@ -2,19 +2,172 @@ import express from 'express';
2
  import cors from 'cors';
3
  import fetch from 'node-fetch';
4
  import rateLimit from 'express-rate-limit';
 
5
  const app = express();
6
- // ── Isabella Consultant Endpoint (standalone) ─────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  import { searchDuckDuckGo, scrapeSiteKnowledge, buildIsabellaPrompt, callNVIDIA } from './consultant.js';
8
 
9
  const ISABELLA_NVIDIA_KEY = process.env.ISABELLA_NVIDIA_KEY || '';
10
 
11
- // URLs to scrape for knowledge
12
  const KNOWLEDGE_URLS = [
13
  'https://domify-academy.free.nf/about-us',
14
- 'domify-academy.free.nf/product-Price',
15
  'https://domify-academy.free.nf/refund',
16
- 'https://domify-academy.free.nf/privacy',
17
- 'https://domify-academy.free.nf'
18
  ];
19
 
20
  let cachedKnowledge = null;
@@ -22,9 +175,7 @@ let knowledgeLastFetched = 0;
22
 
23
  async function getKnowledge() {
24
  const now = Date.now();
25
- if (cachedKnowledge && (now - knowledgeLastFetched) < 30 * 60 * 1000) {
26
- return cachedKnowledge;
27
- }
28
  cachedKnowledge = await scrapeSiteKnowledge(KNOWLEDGE_URLS);
29
  knowledgeLastFetched = now;
30
  console.log('πŸ“š Isabella knowledge base refreshed');
@@ -34,371 +185,63 @@ async function getKnowledge() {
34
  app.post('/api/isabella/chat', async (req, res) => {
35
  try {
36
  const { message, userName, pageName, conversationHistory = [] } = req.body;
 
 
37
 
38
- if (!message) {
39
- return res.status(400).json({ error: 'Message required' });
40
- }
41
-
42
- if (!ISABELLA_NVIDIA_KEY) {
43
- return res.status(500).json({ error: 'Isabella NVIDIA key not configured' });
44
- }
45
-
46
- // Get knowledge and search in parallel
47
- const [knowledge, searchResults] = await Promise.all([
48
- getKnowledge(),
49
- searchDuckDuckGo(message)
50
- ]);
51
-
52
- // Build enriched system prompt
53
- const systemPrompt = buildIsabellaPrompt(
54
- userName || '',
55
- pageName || 'unknown',
56
- knowledge,
57
- searchResults
58
- );
59
-
60
- // Call NVIDIA for actual response
61
  const reply = await callNVIDIA(systemPrompt, message, ISABELLA_NVIDIA_KEY, conversationHistory);
62
 
63
- // Check for embedded issue reports
64
  let issue = null;
65
  const issueMatch = reply.match(/\[ISSUE:\s*(.+?)\]/);
66
- if (issueMatch) {
67
- issue = issueMatch[1];
68
- reply = reply.replace(/\[ISSUE:.*?\]/g, '').trim();
69
- }
70
-
71
- res.json({
72
- reply,
73
- issue,
74
- knowledgeRefreshed: Date.now() - knowledgeLastFetched < 60000
75
- });
76
 
 
77
  } catch (err) {
78
  console.error('Isabella error:', err.message);
79
  res.status(500).json({ error: 'Isabella is thinking... try again' });
80
  }
81
  });
82
 
83
- // ── Knowledge refresh ────────────────────────────
84
  app.post('/api/isabella/refresh-knowledge', async (req, res) => {
85
  cachedKnowledge = await scrapeSiteKnowledge(KNOWLEDGE_URLS);
86
  knowledgeLastFetched = Date.now();
87
  res.json({ success: true, pages: cachedKnowledge.length });
88
  });
89
 
90
-
91
-
92
- const ALLOWED_ORIGINS = (process.env.ALLOWED_ORIGINS || 'https://domify-academy.free.nf')
93
- .split(',')
94
- .map(s => s.trim())
95
- .filter(Boolean);
96
-
97
- app.use(cors({
98
- origin: (origin, callback) => {
99
- if (!origin) return callback(null, true); // allow server-to-server / tools
100
- if (ALLOWED_ORIGINS.includes(origin)) return callback(null, true);
101
- return callback(new Error('CORS blocked'));
102
- }
103
- }));
104
-
105
- app.use(express.json({ limit: '50kb' }));
106
-
107
- app.use('/api/', rateLimit({
108
- windowMs: 15 * 60 * 1000,
109
- max: 30
110
- }));
111
-
112
- const rawKeys = process.env.NOIZ_API_KEYS || '';
113
- const NOIZ_API_KEYS = rawKeys
114
- ? rawKeys.split(',').map(k => k.trim()).filter(Boolean)
115
- : [];
116
-
117
- console.log(`πŸ”‘ Loaded ${NOIZ_API_KEYS.length} key(s)`);
118
-
119
- const NOIZ_BASE = 'https://noiz.ai/v1';
120
-
121
- const VOICE_MAP = {
122
- 'educational-male': '95814add',
123
- 'healer-serena': '5a68d66b',
124
- 'naturalist-soren': 'a845c7de',
125
- 'mentor-kai': '883b6b7c',
126
- 'mentor-maya': '0e4ab6ec',
127
- 'explainer-male': '95814add',
128
- 'narrator-female': '5a68d66b',
129
- 'robot-ai': '883b6b7b', // if you have a confirmed ID, replace this
130
- 'dark-narrator': '883b6b7b',
131
- 'epic-narrator': '95814add'
132
- };
133
-
134
- const EMOTION_MAP = {
135
- calm: { Neutral: 0.8 },
136
- neutral: { Neutral: 0.8 },
137
- joyful: { Joy: 0.8 },
138
- happy: { Joy: 0.8 },
139
- sad: { Sadness: 0.8 },
140
- angry: { Anger: 0.8 },
141
- surprised: { Surprise: 0.8 },
142
- dramatic: { Anger: 0.4, Sadness: 0.4 },
143
- excited: { Joy: 0.6, Surprise: 0.4 },
144
- urgent: { Anger: 0.5, Surprise: 0.5 },
145
- mysterious: { Neutral: 0.3, Surprise: 0.7 },
146
- confident: { Joy: 0.3, Neutral: 0.7 },
147
- fearful: { Sadness: 0.4, Surprise: 0.4 },
148
- "😊": { Joy: 0.8 },
149
- "😒": { Sadness: 0.8 },
150
- "😑": { Anger: 0.8 },
151
- "😲": { Surprise: 0.8 },
152
- "πŸ€”": { Neutral: 0.5 },
153
- "πŸ₯Ή": { Sadness: 0.5, Joy: 0.3 },
154
- "πŸ˜”": { Sadness: 0.6 },
155
- "πŸ’ͺ": { Confidence: 0.8 }
156
- };
157
-
158
- function clampNumber(value, min, max, fallback) {
159
- const n = Number(value);
160
- if (Number.isNaN(n)) return fallback;
161
- return Math.max(min, Math.min(max, n));
162
- }
163
-
164
- function normalizeVoiceId(tagValue) {
165
- const v = String(tagValue || '').trim();
166
- return VOICE_MAP[v] || v;
167
- }
168
-
169
- function parseCinematicScript(script, defaultVoiceId = 'mentor-kai', defaultSpeed = 1.0) {
170
- const segments = [];
171
- const regex = /\[([^\]]+)\]/g;
172
-
173
- let lastIndex = 0;
174
- let currentVoice = defaultVoiceId;
175
- let currentSpeed = clampNumber(defaultSpeed, 0.5, 2.0, 1.0);
176
- let currentEmotion = null;
177
-
178
- let match;
179
- while ((match = regex.exec(script)) !== null) {
180
- const tagContent = match[1].trim();
181
- const tagStart = match.index;
182
- const tagEnd = match.index + match[0].length;
183
-
184
- const textBeforeTag = script.slice(lastIndex, tagStart).trim();
185
- if (textBeforeTag) {
186
- segments.push({
187
- type: 'speech',
188
- text: textBeforeTag,
189
- voiceId: currentVoice,
190
- speed: currentSpeed,
191
- emotion: currentEmotion
192
- });
193
- }
194
-
195
- const lower = tagContent.toLowerCase();
196
-
197
- if (lower.startsWith('pause:')) {
198
- const duration = clampNumber(lower.split(':')[1], 0, 5000, 500);
199
- segments.push({
200
- type: 'pause',
201
- duration
202
- });
203
- } else if (lower.startsWith('voice:')) {
204
- const voiceName = tagContent.slice(tagContent.indexOf(':') + 1).trim();
205
- if (voiceName) {
206
- currentVoice = normalizeVoiceId(voiceName);
207
- }
208
- } else if (lower.startsWith('speed:')) {
209
- const speedValue = tagContent.slice(tagContent.indexOf(':') + 1).trim();
210
- currentSpeed = clampNumber(speedValue, 0.5, 2.0, currentSpeed);
211
- } else if (EMOTION_MAP[tagContent] || EMOTION_MAP[lower]) {
212
- currentEmotion = EMOTION_MAP[tagContent] || EMOTION_MAP[lower];
213
- }
214
-
215
- lastIndex = tagEnd;
216
- }
217
-
218
- const tail = script.slice(lastIndex).trim();
219
- if (tail) {
220
- segments.push({
221
- type: 'speech',
222
- text: tail,
223
- voiceId: currentVoice,
224
- speed: currentSpeed,
225
- emotion: currentEmotion
226
- });
227
- }
228
-
229
- if (segments.length === 0 && script.trim()) {
230
- segments.push({
231
- type: 'speech',
232
- text: script.trim(),
233
- voiceId: defaultVoiceId,
234
- speed: currentSpeed,
235
- emotion: null
236
- });
237
- }
238
-
239
- return segments;
240
- }
241
-
242
- function isAudioResponse(res) {
243
- const ct = (res.headers.get('content-type') || '').toLowerCase();
244
- return ct.includes('audio') || ct.includes('mpeg') || ct.includes('mp3');
245
- }
246
-
247
- async function callNoizTTS({ text, voiceId, speed = 1.0, emotion = null }) {
248
- const resolvedVoiceId = normalizeVoiceId(voiceId);
249
-
250
- const form = new URLSearchParams();
251
- form.append('text', text);
252
- form.append('voice_id', resolvedVoiceId);
253
- form.append('output_format', 'mp3');
254
- form.append('speed', String(speed));
255
-
256
- if (emotion) {
257
- const emo = typeof emotion === 'string' ? emotion : JSON.stringify(emotion);
258
- form.append('emo', emo);
259
- form.append('emotion', emo);
260
- }
261
-
262
- const keysToTry = [...NOIZ_API_KEYS, null]; // guest fallback last
263
-
264
- for (const key of keysToTry) {
265
- const headers = {
266
- 'Content-Type': 'application/x-www-form-urlencoded',
267
- 'Accept': 'audio/mpeg'
268
- };
269
-
270
- if (key) {
271
- headers['Authorization'] = key;
272
- }
273
-
274
- try {
275
- const res = await fetch(`${NOIZ_BASE}/text-to-speech`, {
276
- method: 'POST',
277
- headers,
278
- body: form.toString(),
279
- signal: AbortSignal.timeout(60000)
280
- });
281
-
282
- console.log({
283
- status: res.status,
284
- contentType: res.headers.get('content-type'),
285
- contentLength: res.headers.get('content-length'),
286
- mode: key ? 'api' : 'guest'
287
- });
288
-
289
- if (!res.ok) {
290
- const errText = await res.text().catch(() => '');
291
- console.error(`❌ Noiz error ${res.status}: ${errText}`);
292
- if (res.status === 401 && key) continue;
293
- if (!key) continue;
294
- throw new Error(errText || `Noiz error ${res.status}`);
295
- }
296
-
297
- if (!isAudioResponse(res)) {
298
- const body = await res.text().catch(() => '');
299
- throw new Error(`Expected audio, got: ${body.slice(0, 200)}`);
300
- }
301
-
302
- const audioBuffer = Buffer.from(await res.arrayBuffer());
303
- console.log(`🎡 Audio bytes received: ${audioBuffer.length}`);
304
-
305
- if (audioBuffer.length < 500) {
306
- console.warn('⚠️ Tiny response, skipping');
307
- continue;
308
- }
309
-
310
- return audioBuffer;
311
- } catch (err) {
312
- if (err?.name === 'AbortError') {
313
- console.warn('⏱ Request timed out');
314
- continue;
315
- }
316
- console.error('🌐 Fetch error:', err.message);
317
- continue;
318
- }
319
- }
320
-
321
- throw new Error('All Noiz attempts failed');
322
- }
323
-
324
  app.get('/health', (req, res) => {
325
- res.json({
326
- ok: true,
327
- keysLoaded: NOIZ_API_KEYS.length,
328
- service: 'Noiz Voice Studio'
329
- });
330
  });
331
 
332
  app.post('/api/generate-voice', async (req, res) => {
333
- const { script, voiceId = 'mentor-kai', speed = 1.0 } = req.body || {};
334
-
335
- if (!script || typeof script !== 'string') {
336
- return res.status(400).json({ error: 'Missing script' });
337
- }
338
-
339
- if (script.length > 5000) {
340
- return res.status(400).json({ error: 'Script too long (max 5000 characters)' });
341
- }
342
-
343
- try {
344
- const segments = parseCinematicScript(script, voiceId, speed);
345
- console.log(`🎬 Parsed ${segments.length} segment(s)`);
346
-
347
- const outputSegments = [];
348
-
349
- for (const seg of segments) {
350
- if (seg.type === 'pause') {
351
- outputSegments.push({
352
- type: 'pause',
353
- duration: seg.duration
354
- });
355
- continue;
356
- }
357
-
358
- console.log(
359
- `🎀 ${seg.voiceId} | speed=${seg.speed} | emotion=${seg.emotion ? 'yes' : 'no'} | text="${seg.text.slice(0, 50)}"`
360
- );
361
-
362
- const audioBuffer = await callNoizTTS({
363
- text: seg.text,
364
- voiceId: seg.voiceId,
365
- speed: seg.speed,
366
- emotion: seg.emotion
367
- });
368
-
369
- outputSegments.push({
370
- type: 'speech',
371
- voiceId: seg.voiceId,
372
- speed: seg.speed,
373
- emotion: seg.emotion,
374
- text: seg.text,
375
- audio: audioBuffer.toString('base64'),
376
- format: 'mp3'
377
- });
378
- }
379
 
380
- const speechCount = outputSegments.filter(s => s.type === 'speech').length;
 
 
 
 
 
 
 
 
 
 
381
 
382
- if (speechCount === 1 && outputSegments.length === 1) {
383
- return res.json({
384
- mode: 'single',
385
- format: 'mp3',
386
- audio: outputSegments[0].audio,
387
- segments: outputSegments
388
- });
 
389
  }
390
-
391
- return res.json({
392
- mode: 'timeline',
393
- format: 'mp3',
394
- segments: outputSegments
395
- });
396
- } catch (err) {
397
- console.error('❌ Generation failed:', err.message);
398
- return res.status(500).json({ error: err.message });
399
- }
400
  });
401
 
402
- app.listen(7860, () => {
403
- console.log('πŸš€ Cinematic Voice Studio ready');
404
- });
 
2
  import cors from 'cors';
3
  import fetch from 'node-fetch';
4
  import rateLimit from 'express-rate-limit';
5
+
6
  const app = express();
7
+
8
+ // ── CORS & Middleware (MUST come before routes) ──
9
+ const ALLOWED_ORIGINS = (process.env.ALLOWED_ORIGINS || 'https://domify-academy.free.nf')
10
+ .split(',')
11
+ .map(s => s.trim())
12
+ .filter(Boolean);
13
+
14
+ app.use(cors({
15
+ origin: (origin, callback) => {
16
+ if (!origin) return callback(null, true);
17
+ if (ALLOWED_ORIGINS.includes(origin)) return callback(null, true);
18
+ return callback(new Error('CORS blocked'));
19
+ }
20
+ }));
21
+ app.use(express.json({ limit: '50kb' }));
22
+ app.use('/api/', rateLimit({ windowMs: 15 * 60 * 1000, max: 30 }));
23
+
24
+ // ── TTS Configuration ──
25
+ const rawKeys = process.env.NOIZ_API_KEYS || '';
26
+ const NOIZ_API_KEYS = rawKeys ? rawKeys.split(',').map(k => k.trim()).filter(Boolean) : [];
27
+ console.log(`πŸ”‘ Loaded ${NOIZ_API_KEYS.length} key(s)`);
28
+
29
+ const NOIZ_BASE = 'https://noiz.ai/v1';
30
+ const VOICE_MAP = {
31
+ 'educational-male': '95814add', 'healer-serena': '5a68d66b',
32
+ 'naturalist-soren': 'a845c7de', 'mentor-kai': '883b6b7c',
33
+ 'mentor-maya': '0e4ab6ec', 'explainer-male': '95814add',
34
+ 'narrator-female': '5a68d66b', 'robot-ai': '883b6b7b',
35
+ 'dark-narrator': '883b6b7b', 'epic-narrator': '95814add'
36
+ };
37
+
38
+ const EMOTION_MAP = {
39
+ calm: { Neutral: 0.8 }, neutral: { Neutral: 0.8 }, joyful: { Joy: 0.8 },
40
+ happy: { Joy: 0.8 }, sad: { Sadness: 0.8 }, angry: { Anger: 0.8 },
41
+ surprised: { Surprise: 0.8 }, dramatic: { Anger: 0.4, Sadness: 0.4 },
42
+ excited: { Joy: 0.6, Surprise: 0.4 }, urgent: { Anger: 0.5, Surprise: 0.5 },
43
+ mysterious: { Neutral: 0.3, Surprise: 0.7 }, confident: { Joy: 0.3, Neutral: 0.7 },
44
+ fearful: { Sadness: 0.4, Surprise: 0.4 },
45
+ '😊': { Joy: 0.8 }, '😒': { Sadness: 0.8 }, '😑': { Anger: 0.8 },
46
+ '😲': { Surprise: 0.8 }, 'πŸ€”': { Neutral: 0.5 }, 'πŸ₯Ή': { Sadness: 0.5, Joy: 0.3 },
47
+ 'πŸ˜”': { Sadness: 0.6 }, 'πŸ’ͺ': { Confidence: 0.8 }
48
+ };
49
+
50
+ // ── TTS Helper Functions ──
51
+ function clampNumber(value, min, max, fallback) {
52
+ const n = Number(value);
53
+ if (Number.isNaN(n)) return fallback;
54
+ return Math.max(min, Math.min(max, n));
55
+ }
56
+
57
+ function normalizeVoiceId(tagValue) {
58
+ const v = String(tagValue || '').trim();
59
+ return VOICE_MAP[v] || v;
60
+ }
61
+
62
+ function parseCinematicScript(script, defaultVoiceId = 'mentor-kai', defaultSpeed = 1.0) {
63
+ const segments = [];
64
+ const regex = /\[([^\]]+)\]/g;
65
+ let lastIndex = 0;
66
+ let currentVoice = defaultVoiceId;
67
+ let currentSpeed = clampNumber(defaultSpeed, 0.5, 2.0, 1.0);
68
+ let currentEmotion = null;
69
+ let match;
70
+
71
+ while ((match = regex.exec(script)) !== null) {
72
+ const tagContent = match[1].trim();
73
+ const tagStart = match.index;
74
+ const tagEnd = match.index + match[0].length;
75
+ const textBeforeTag = script.slice(lastIndex, tagStart).trim();
76
+
77
+ if (textBeforeTag) {
78
+ segments.push({ type: 'speech', text: textBeforeTag, voiceId: currentVoice, speed: currentSpeed, emotion: currentEmotion });
79
+ }
80
+
81
+ const lower = tagContent.toLowerCase();
82
+ if (lower.startsWith('pause:')) {
83
+ const duration = clampNumber(lower.split(':')[1], 0, 5000, 500);
84
+ segments.push({ type: 'pause', duration });
85
+ } else if (lower.startsWith('voice:')) {
86
+ const voiceName = tagContent.slice(tagContent.indexOf(':') + 1).trim();
87
+ if (voiceName) currentVoice = normalizeVoiceId(voiceName);
88
+ } else if (lower.startsWith('speed:')) {
89
+ const speedValue = tagContent.slice(tagContent.indexOf(':') + 1).trim();
90
+ currentSpeed = clampNumber(speedValue, 0.5, 2.0, currentSpeed);
91
+ } else if (EMOTION_MAP[tagContent] || EMOTION_MAP[lower]) {
92
+ currentEmotion = EMOTION_MAP[tagContent] || EMOTION_MAP[lower];
93
+ }
94
+ lastIndex = tagEnd;
95
+ }
96
+
97
+ const tail = script.slice(lastIndex).trim();
98
+ if (tail) segments.push({ type: 'speech', text: tail, voiceId: currentVoice, speed: currentSpeed, emotion: currentEmotion });
99
+ if (segments.length === 0 && script.trim()) {
100
+ segments.push({ type: 'speech', text: script.trim(), voiceId: defaultVoiceId, speed: currentSpeed, emotion: null });
101
+ }
102
+ return segments;
103
+ }
104
+
105
+ function isAudioResponse(res) {
106
+ const ct = (res.headers.get('content-type') || '').toLowerCase();
107
+ return ct.includes('audio') || ct.includes('mpeg') || ct.includes('mp3');
108
+ }
109
+
110
+ async function callNoizTTS({ text, voiceId, speed = 1.0, emotion = null }) {
111
+ const resolvedVoiceId = normalizeVoiceId(voiceId);
112
+ const form = new URLSearchParams();
113
+ form.append('text', text);
114
+ form.append('voice_id', resolvedVoiceId);
115
+ form.append('output_format', 'mp3');
116
+ form.append('speed', String(speed));
117
+ if (emotion) {
118
+ const emo = typeof emotion === 'string' ? emotion : JSON.stringify(emotion);
119
+ form.append('emo', emo);
120
+ form.append('emotion', emo);
121
+ }
122
+
123
+ const keysToTry = [...NOIZ_API_KEYS, null];
124
+ for (const key of keysToTry) {
125
+ const headers = { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'audio/mpeg' };
126
+ if (key) headers['Authorization'] = key;
127
+
128
+ try {
129
+ const res = await fetch(`${NOIZ_BASE}/text-to-speech`, { method: 'POST', headers, body: form.toString(), signal: AbortSignal.timeout(60000) });
130
+ console.log({ status: res.status, contentType: res.headers.get('content-type'), contentLength: res.headers.get('content-length'), mode: key ? 'api' : 'guest' });
131
+
132
+ if (!res.ok) {
133
+ const errText = await res.text().catch(() => '');
134
+ console.error(`❌ Noiz error ${res.status}: ${errText}`);
135
+ if (res.status === 401 && key) continue;
136
+ if (!key) continue;
137
+ throw new Error(errText || `Noiz error ${res.status}`);
138
+ }
139
+
140
+ if (!isAudioResponse(res)) {
141
+ const body = await res.text().catch(() => '');
142
+ throw new Error(`Expected audio, got: ${body.slice(0, 200)}`);
143
+ }
144
+
145
+ const audioBuffer = Buffer.from(await res.arrayBuffer());
146
+ console.log(`🎡 Audio bytes received: ${audioBuffer.length}`);
147
+ if (audioBuffer.length < 500) { console.warn('⚠️ Tiny response, skipping'); continue; }
148
+ return audioBuffer;
149
+ } catch (err) {
150
+ if (err?.name === 'AbortError') { console.warn('⏱ Request timed out'); continue; }
151
+ console.error('🌐 Fetch error:', err.message);
152
+ continue;
153
+ }
154
+ }
155
+ throw new Error('All Noiz attempts failed');
156
+ }
157
+
158
+ // ═══════════════════════════════════════════
159
+ // ISABELLA CONSULTANT (imports + routes)
160
+ // ═══════════════════════════════════════════
161
  import { searchDuckDuckGo, scrapeSiteKnowledge, buildIsabellaPrompt, callNVIDIA } from './consultant.js';
162
 
163
  const ISABELLA_NVIDIA_KEY = process.env.ISABELLA_NVIDIA_KEY || '';
164
 
 
165
  const KNOWLEDGE_URLS = [
166
  'https://domify-academy.free.nf/about-us',
167
+ 'https://domify-academy.free.nf/product-Price',
168
  'https://domify-academy.free.nf/refund',
169
+ 'https://domify-academy.free.nf/privacy',
170
+ 'https://domify-academy.free.nf'
171
  ];
172
 
173
  let cachedKnowledge = null;
 
175
 
176
  async function getKnowledge() {
177
  const now = Date.now();
178
+ if (cachedKnowledge && (now - knowledgeLastFetched) < 30 * 60 * 1000) return cachedKnowledge;
 
 
179
  cachedKnowledge = await scrapeSiteKnowledge(KNOWLEDGE_URLS);
180
  knowledgeLastFetched = now;
181
  console.log('πŸ“š Isabella knowledge base refreshed');
 
185
  app.post('/api/isabella/chat', async (req, res) => {
186
  try {
187
  const { message, userName, pageName, conversationHistory = [] } = req.body;
188
+ if (!message) return res.status(400).json({ error: 'Message required' });
189
+ if (!ISABELLA_NVIDIA_KEY) return res.status(500).json({ error: 'Isabella NVIDIA key not configured' });
190
 
191
+ const [knowledge, searchResults] = await Promise.all([getKnowledge(), searchDuckDuckGo(message)]);
192
+ const systemPrompt = buildIsabellaPrompt(userName || '', pageName || 'unknown', knowledge, searchResults);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  const reply = await callNVIDIA(systemPrompt, message, ISABELLA_NVIDIA_KEY, conversationHistory);
194
 
 
195
  let issue = null;
196
  const issueMatch = reply.match(/\[ISSUE:\s*(.+?)\]/);
197
+ if (issueMatch) { issue = issueMatch[1]; reply = reply.replace(/\[ISSUE:.*?\]/g, '').trim(); }
 
 
 
 
 
 
 
 
 
198
 
199
+ res.json({ reply, issue, knowledgeRefreshed: Date.now() - knowledgeLastFetched < 60000 });
200
  } catch (err) {
201
  console.error('Isabella error:', err.message);
202
  res.status(500).json({ error: 'Isabella is thinking... try again' });
203
  }
204
  });
205
 
 
206
  app.post('/api/isabella/refresh-knowledge', async (req, res) => {
207
  cachedKnowledge = await scrapeSiteKnowledge(KNOWLEDGE_URLS);
208
  knowledgeLastFetched = Date.now();
209
  res.json({ success: true, pages: cachedKnowledge.length });
210
  });
211
 
212
+ // ═══════════════════════════════════════════
213
+ // TTS ENDPOINTS
214
+ // ═══════════════════════════════════════════
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  app.get('/health', (req, res) => {
216
+ res.json({ ok: true, keysLoaded: NOIZ_API_KEYS.length, service: 'Noiz Voice Studio' });
 
 
 
 
217
  });
218
 
219
  app.post('/api/generate-voice', async (req, res) => {
220
+ const { script, voiceId = 'mentor-kai', speed = 1.0 } = req.body || {};
221
+ if (!script || typeof script !== 'string') return res.status(400).json({ error: 'Missing script' });
222
+ if (script.length > 5000) return res.status(400).json({ error: 'Script too long (max 5000 characters)' });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
 
224
+ try {
225
+ const segments = parseCinematicScript(script, voiceId, speed);
226
+ console.log(`πŸ“‹ Parsed ${segments.length} segment(s)`);
227
+ const outputSegments = [];
228
+
229
+ for (const seg of segments) {
230
+ if (seg.type === 'pause') { outputSegments.push({ type: 'pause', duration: seg.duration }); continue; }
231
+ console.log(`🎀 ${seg.voiceId} | speed=${seg.speed} | emotion=${seg.emotion ? 'yes' : 'no'} | text="${seg.text.slice(0, 50)}"`);
232
+ const audioBuffer = await callNoizTTS({ text: seg.text, voiceId: seg.voiceId, speed: seg.speed, emotion: seg.emotion });
233
+ outputSegments.push({ type: 'speech', voiceId: seg.voiceId, speed: seg.speed, emotion: seg.emotion, text: seg.text, audio: audioBuffer.toString('base64'), format: 'mp3' });
234
+ }
235
 
236
+ const speechCount = outputSegments.filter(s => s.type === 'speech').length;
237
+ if (speechCount === 1 && outputSegments.length === 1) {
238
+ return res.json({ mode: 'single', format: 'mp3', audio: outputSegments[0].audio, segments: outputSegments });
239
+ }
240
+ return res.json({ mode: 'timeline', format: 'mp3', segments: outputSegments });
241
+ } catch (err) {
242
+ console.error('❌ Generation failed:', err.message);
243
+ return res.status(500).json({ error: err.message });
244
  }
 
 
 
 
 
 
 
 
 
 
245
  });
246
 
247
+ app.listen(7860, () => { console.log('πŸš€ Cinematic Voice Studio ready'); });