@realtimex/email-automator 2.21.2 → 2.21.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/.env.example +4 -4
  2. package/README.md +5 -4
  3. package/api/src/config/index.ts +10 -8
  4. package/api/src/lib/agent-knowledge-base.ts +83 -0
  5. package/api/src/middleware/validation.ts +1 -0
  6. package/api/src/routes/agent.ts +179 -0
  7. package/api/src/routes/health.ts +1 -1
  8. package/api/src/routes/index.ts +7 -0
  9. package/api/src/routes/migrate.ts +8 -1
  10. package/api/src/routes/tts.ts +187 -0
  11. package/api/src/services/AgentService.ts +206 -0
  12. package/api/src/services/RAGService.ts +245 -0
  13. package/api/src/services/SDKService.ts +23 -2
  14. package/api/src/services/supabase.ts +2 -2
  15. package/bin/email-automator-setup.js +6 -6
  16. package/dist/api/src/config/index.js +10 -8
  17. package/dist/api/src/lib/agent-knowledge-base.js +72 -0
  18. package/dist/api/src/middleware/validation.js +1 -0
  19. package/dist/api/src/routes/agent.js +152 -0
  20. package/dist/api/src/routes/health.js +1 -1
  21. package/dist/api/src/routes/index.js +4 -0
  22. package/dist/api/src/routes/migrate.js +7 -1
  23. package/dist/api/src/routes/tts.js +174 -0
  24. package/dist/api/src/services/AgentService.js +167 -0
  25. package/dist/api/src/services/RAGService.js +170 -0
  26. package/dist/api/src/services/SDKService.js +20 -2
  27. package/dist/api/src/services/supabase.js +2 -2
  28. package/dist/assets/es-BQiNEjuo.js +4 -0
  29. package/dist/assets/fr-Di3HVss0.js +4 -0
  30. package/dist/assets/index-CotbFXGe.js +193 -0
  31. package/dist/assets/index-DTtR5hN2.css +1 -0
  32. package/dist/assets/ja-OkCqAyrq.js +4 -0
  33. package/dist/assets/ko-ZvnlIxWp.js +4 -0
  34. package/dist/assets/vi-BATQPoNy.js +4 -0
  35. package/dist/index.html +2 -2
  36. package/docs/README.md +33 -0
  37. package/docs/user-guide/ACCOUNT.md +39 -0
  38. package/docs/user-guide/AUTOMATION.md +47 -0
  39. package/docs/user-guide/CONFIGURATION.md +75 -0
  40. package/docs/user-guide/DASHBOARD.md +77 -0
  41. package/docs/user-guide/GETTING-STARTED.md +79 -0
  42. package/docs/user-guide/TROUBLESHOOTING.md +64 -0
  43. package/package.json +5 -2
  44. package/scripts/ingest-knowledge-rag.ts +253 -0
  45. package/scripts/migrate.sh +88 -2
  46. package/supabase/migrations/20260203000001_add_auto_speak_setting.sql +10 -0
  47. package/supabase/migrations/20260203000002_add_tts_settings.sql +29 -0
  48. package/supabase/migrations/20260203000003_add_knowledge_base_rag.sql +92 -0
  49. package/supabase/migrations/20260203000004_add_embedding_settings.sql +11 -0
  50. package/dist/assets/es-CYWTwB_c.js +0 -1
  51. package/dist/assets/fr-C7ON1HVi.js +0 -1
  52. package/dist/assets/index-BF23Ab2e.js +0 -130
  53. package/dist/assets/index-DwCPoGUC.css +0 -1
  54. package/dist/assets/ja-DGyVGEJe.js +0 -1
  55. package/dist/assets/ko-DMcYqJvz.js +0 -1
  56. package/dist/assets/vi-a_omNqsV.js +0 -1
package/.env.example CHANGED
@@ -8,10 +8,10 @@ VITE_SUPABASE_ANON_KEY=your_supabase_anon_key
8
8
  VITE_API_URL=http://localhost:3004
9
9
  PORT=3004
10
10
 
11
- # OpenAI / LLM Configuration
12
- LLM_API_KEY=your_llm_api_key
13
- LLM_BASE_URL=https://api.openai.com/v1
14
- LLM_MODEL=gpt-4o-mini
11
+ # LLM Configuration
12
+ # Managed by RealTimeX SDK - No environment variables needed!
13
+ # Configure providers via RealTimeX Desktop (port 3001) or Configuration UI
14
+ # The SDK automatically connects to RealTimeX Desktop for AI operations
15
15
 
16
16
  # Security (required in production)
17
17
  JWT_SECRET="your-secure-jwt-secret-min-32-chars"
package/README.md CHANGED
@@ -178,10 +178,11 @@ VITE_SUPABASE_ANON_KEY=your-anon-key
178
178
  VITE_API_URL=http://localhost:3004
179
179
  PORT=3004
180
180
 
181
- # LLM
182
- LLM_API_KEY=your-llm-key
183
- LLM_BASE_URL=https://api.openai.com/v1
184
- LLM_MODEL=gpt-4o-mini
181
+ # LLM Configuration
182
+ # Managed by RealTimeX SDK - No API keys needed!
183
+ # 1. Start RealTimeX Desktop (port 3001)
184
+ # 2. Configure providers via Configuration UI or RealTimeX Desktop
185
+ # The SDK automatically handles provider discovery and routing
185
186
 
186
187
  # Development
187
188
  DISABLE_AUTH=true
@@ -72,27 +72,29 @@ export const config = {
72
72
  scriptsDir: join(packageRoot, 'scripts'),
73
73
 
74
74
  // Supabase
75
+ // Supabase Configuration
76
+ // Primary: BYOK via Setup Wizard (credentials passed via HTTP headers)
77
+ // Fallback: Environment variables (for backend development/testing only)
78
+ // WARNING: Env var fallback will be removed in future versions - use Setup Wizard!
75
79
  supabase: {
76
80
  url: process.env.SUPABASE_URL || process.env.VITE_SUPABASE_URL || '',
77
81
  anonKey: process.env.SUPABASE_ANON_KEY || process.env.VITE_SUPABASE_ANON_KEY || '',
78
82
  serviceRoleKey: process.env.SUPABASE_SERVICE_ROLE_KEY || '',
79
83
  },
80
84
 
81
- // LLM
82
- llm: {
83
- apiKey: process.env.LLM_API_KEY || '',
84
- baseUrl: process.env.LLM_BASE_URL,
85
- model: process.env.LLM_MODEL || 'gpt-4o-mini',
86
- },
85
+ // LLM Configuration: Managed by RealTimeX SDK (no env vars needed)
86
+ // Users configure providers via RealTimeX Desktop or Configuration UI
87
87
 
88
- // OAuth - Gmail
88
+ // OAuth Configuration
89
+ // Primary: BYOK via Setup Wizard (stored in user_settings)
90
+ // Fallback: Environment variables (for backend development/testing only)
91
+ // WARNING: Env var fallback will be removed in future versions - use Setup Wizard!
89
92
  gmail: {
90
93
  clientId: process.env.GMAIL_CLIENT_ID || '',
91
94
  clientSecret: process.env.GMAIL_CLIENT_SECRET || '',
92
95
  redirectUri: process.env.GMAIL_REDIRECT_URI || 'urn:ietf:wg:oauth:2.0:oob',
93
96
  },
94
97
 
95
- // OAuth - Microsoft
96
98
  microsoft: {
97
99
  clientId: process.env.MS_GRAPH_CLIENT_ID || '',
98
100
  tenantId: process.env.MS_GRAPH_TENANT_ID || 'common',
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Agent Knowledge Base (Backend)
3
+ * Provides comprehensive context about Email-Automator to the LLM
4
+ *
5
+ * This file reads from docs/AGENT-KNOWLEDGE.md which is auto-generated
6
+ * at build time by scripts/generate-agent-docs.ts from user documentation
7
+ *
8
+ * Sources:
9
+ * - docs/user-guide/*.md (primary source)
10
+ * - docs/README.md
11
+ * - CLAUDE.md (technical architecture)
12
+ */
13
+
14
+ import fs from 'fs';
15
+ import path from 'path';
16
+ import { fileURLToPath } from 'url';
17
+
18
+ const __filename = fileURLToPath(import.meta.url);
19
+ const __dirname = path.dirname(__filename);
20
+
21
+ // Read generated documentation at runtime
22
+ let CORE_APP_KNOWLEDGE = '';
23
+
24
+ try {
25
+ // Try to read from generated knowledge base
26
+ const docsPath = path.join(__dirname, '../../../docs/AGENT-KNOWLEDGE.md');
27
+ CORE_APP_KNOWLEDGE = fs.readFileSync(docsPath, 'utf-8');
28
+ console.log('[AgentKnowledge] ✓ Loaded knowledge base from docs/AGENT-KNOWLEDGE.md');
29
+ console.log(`[AgentKnowledge] Size: ${(CORE_APP_KNOWLEDGE.length / 1024).toFixed(2)} KB`);
30
+ } catch (error) {
31
+ console.warn('[AgentKnowledge] ⚠️ Could not load docs/AGENT-KNOWLEDGE.md, using fallback');
32
+ console.warn('[AgentKnowledge] Run: npm run build:docs');
33
+
34
+ // Fallback minimal knowledge
35
+ CORE_APP_KNOWLEDGE = `# Email-Automator Assistant
36
+
37
+ I'm your Email-Automator assistant. I can help you with:
38
+ - Connecting email accounts (Gmail, Outlook)
39
+ - Creating automation rules
40
+ - Managing drafts
41
+ - Configuring AI and voice settings
42
+
43
+ **⚠️ Note:** Full knowledge base not available. Please regenerate by running:
44
+ \`\`\`bash
45
+ npm run build:docs
46
+ \`\`\`
47
+ `;
48
+ }
49
+
50
+ export { CORE_APP_KNOWLEDGE };
51
+
52
+ export function getEnhancedSystemInstruction(
53
+ pageId: string,
54
+ userInstruction: string,
55
+ data?: any
56
+ ): string {
57
+ // Base instruction with knowledge
58
+ let instruction = `${userInstruction}
59
+
60
+ ${CORE_APP_KNOWLEDGE}
61
+
62
+ # Current Context
63
+ - **Current Page**: ${pageId}
64
+ - **Page Data**: ${JSON.stringify(data || {}, null, 2)}
65
+
66
+ # Response Instructions
67
+
68
+ **CRITICAL - Anti-Hallucination Rules:**
69
+ 1. ONLY answer using information explicitly documented in the knowledge base above
70
+ 2. If you don't find information about something, say: "I don't have documentation about that"
71
+ 3. NEVER invent features, buttons, settings, or workflows not documented above
72
+ 4. When uncertain, acknowledge it clearly rather than guessing
73
+
74
+ **When Answering:**
75
+ - Search the knowledge base for relevant information first
76
+ - Reference exact page names and UI elements as documented
77
+ - Provide step-by-step instructions when they exist in the guides
78
+ - If user asks about a different page, guide them: "Go to [Page Name] → [Section]"
79
+ - Use tools when available on current page
80
+ - Be concise and accurate - better to say "I'm not sure" than provide wrong information`;
81
+
82
+ return instruction;
83
+ }
@@ -83,6 +83,7 @@ export const schemas = {
83
83
  migrate: z.object({
84
84
  projectRef: z.string().min(1, 'Project reference is required'),
85
85
  accessToken: z.string().min(1, 'Access token is required for automatic migration'),
86
+ anonKey: z.string().optional(), // For knowledge base ingestion during migration
86
87
  }),
87
88
 
88
89
  // Rule schemas - supports both single action (legacy) and actions array
@@ -0,0 +1,179 @@
1
+ import { Request, Response, Router } from 'express';
2
+ import { createClient, SupabaseClient } from '@supabase/supabase-js';
3
+ import { AgentService } from '../services/AgentService.js';
4
+
5
+ const router = Router();
6
+
7
+ /**
8
+ * Get Supabase client from request headers (BYOK mode) or environment (server mode)
9
+ */
10
+ function getSupabaseFromRequest(req: Request): SupabaseClient | null {
11
+ // Try request headers first (BYOK mode - credentials from Setup Wizard)
12
+ const headerUrl = req.headers['x-supabase-url'] as string;
13
+ const headerKey = req.headers['x-supabase-anon-key'] as string;
14
+
15
+ if (headerUrl && headerKey) {
16
+ try {
17
+ return createClient(headerUrl, headerKey);
18
+ } catch (error) {
19
+ console.error('[Agent API] Failed to create Supabase client from headers:', error);
20
+ }
21
+ }
22
+
23
+ // Fallback to environment variables (server mode)
24
+ const envUrl = process.env.SUPABASE_URL;
25
+ const envKey = process.env.SUPABASE_ANON_KEY;
26
+
27
+ if (envUrl && envKey) {
28
+ try {
29
+ return createClient(envUrl, envKey);
30
+ } catch (error) {
31
+ console.error('[Agent API] Failed to create Supabase client from env:', error);
32
+ }
33
+ }
34
+
35
+ return null;
36
+ }
37
+
38
+ /**
39
+ * POST /api/agent/chat
40
+ * Context-aware chat endpoint with optional RAG
41
+ */
42
+ router.post('/chat', async (req: Request, res: Response) => {
43
+ try {
44
+ const userId = req.headers['x-user-id'] as string;
45
+ const { message, context, history } = req.body;
46
+
47
+ if (!message) {
48
+ return res.status(400).json({ success: false, error: 'Message is required' });
49
+ }
50
+
51
+ // Get Supabase client from request (BYOK) or environment
52
+ const supabase = getSupabaseFromRequest(req);
53
+
54
+ // If Supabase is available, use RAG-enhanced agent
55
+ if (supabase) {
56
+ console.log('[Agent API] Using RAG-enhanced agent');
57
+ const agentService = new AgentService(supabase);
58
+
59
+ const response = await agentService.chat(
60
+ userId || 'anonymous',
61
+ message,
62
+ context || { page_id: 'global' },
63
+ history || []
64
+ );
65
+
66
+ res.json({
67
+ success: true,
68
+ response
69
+ });
70
+ } else {
71
+ // Fallback: Basic agent without RAG
72
+ console.warn('[Agent API] Running without RAG - Supabase not configured');
73
+
74
+ const { SDKService } = await import('../services/SDKService.js');
75
+ const sdk = SDKService.getSDK();
76
+
77
+ if (!sdk) {
78
+ return res.status(503).json({
79
+ success: false,
80
+ error: 'AI service unavailable. Please ensure RealTimeX Desktop is running.'
81
+ });
82
+ }
83
+
84
+ // Build system prompt without RAG
85
+ const baseInstruction = context?.system_instruction ||
86
+ "You are a helpful Email Automator Assistant.";
87
+
88
+ let systemPrompt = `${baseInstruction}
89
+
90
+ # Current Context
91
+ - **Current Page**: ${context?.page_id || 'global'}
92
+ - **Page Data**: ${JSON.stringify(context?.data || {}, null, 2)}
93
+
94
+ # Response Instructions
95
+ **When Answering:**
96
+ - Provide helpful guidance based on the context
97
+ - Use tools when available on current page
98
+ - Be concise and actionable`;
99
+
100
+ // Add Tool Instructions if tools are available
101
+ if (context?.tools && context.tools.length > 0) {
102
+ const toolDescs = context.tools.map((t: any) => {
103
+ const params = t.parameters?.properties
104
+ ? Object.entries(t.parameters.properties).map(([key, val]: [string, any]) =>
105
+ `${key}: ${val.type}${val.description ? ` (${val.description})` : ''}`
106
+ ).join(', ')
107
+ : 'none';
108
+ const required = t.parameters?.required ? ` [Required: ${t.parameters.required.join(', ')}]` : '';
109
+ return `- ${t.name}: ${t.description}\n Parameters: {${params}}${required}`;
110
+ }).join('\n');
111
+
112
+ systemPrompt += `\n\n# AVAILABLE TOOLS\n${toolDescs}\n\n## Tool Usage Rules:
113
+ 1. To execute a tool, output the marker <<<ACTION>>> followed by a JSON object on the same line
114
+ 2. The JSON MUST have "name" (tool name) and "args" (object with parameters)
115
+ 3. Example: <<<ACTION>>>{"name": "send_draft", "args": {"draft_id": "123"}}
116
+ 4. Do NOT wrap the JSON in markdown code blocks
117
+ 5. Always include all required parameters as specified above
118
+ 6. Provide a friendly explanation BEFORE the <<<ACTION>>> marker`;
119
+ }
120
+
121
+ // Prepare messages
122
+ const historyMessages = (history || []).slice(-5).map((m: any) => ({
123
+ role: m.role as 'system' | 'user' | 'assistant',
124
+ content: m.content
125
+ }));
126
+
127
+ const messages = [
128
+ { role: 'system' as const, content: systemPrompt },
129
+ ...historyMessages,
130
+ { role: 'user' as const, content: message }
131
+ ];
132
+
133
+ // Call SDK
134
+ const { provider, model } = await SDKService.resolveChatProvider({});
135
+ const llmResponse = await sdk.llm.chat(messages, { provider, model });
136
+
137
+ let content = llmResponse.response?.content || "I couldn't generate a response.";
138
+ let action: { name: string; args: any } | undefined = undefined;
139
+
140
+ // Parse Action
141
+ if (content.includes('<<<ACTION>>>')) {
142
+ const parts = content.split('<<<ACTION>>>');
143
+ content = parts[0].trim();
144
+ try {
145
+ let actionJson = parts[1].trim().replace(/```json\s*/g, '').replace(/```\s*/g, '');
146
+ const parsed = JSON.parse(actionJson);
147
+
148
+ if (parsed?.name && typeof parsed.name === 'string') {
149
+ const toolExists = context?.tools?.some((t: any) => t.name === parsed.name);
150
+ if (toolExists) {
151
+ action = { name: parsed.name, args: parsed.args || {} };
152
+ } else {
153
+ content += `\n\n⚠️ Note: Tool "${parsed.name}" not available.`;
154
+ }
155
+ }
156
+ } catch (e) {
157
+ content += `\n\n⚠️ Note: I tried to take an action but the format was incorrect.`;
158
+ }
159
+ }
160
+
161
+ res.json({
162
+ success: true,
163
+ response: {
164
+ content,
165
+ action,
166
+ usage: (llmResponse.response as any)?.metrics || undefined
167
+ }
168
+ });
169
+ }
170
+ } catch (error: any) {
171
+ console.error('[Agent API] Chat failed:', error);
172
+ res.status(500).json({
173
+ success: false,
174
+ error: error.message
175
+ });
176
+ }
177
+ });
178
+
179
+ export default router;
@@ -38,7 +38,7 @@ router.get('/', async (_req, res) => {
38
38
  environment: config.nodeEnv,
39
39
  services: {
40
40
  database: dbStatus,
41
- llm: config.llm.apiKey ? 'configured' : 'not_configured',
41
+ llm: 'realtimex_sdk', // Managed by RealTimeX SDK
42
42
  gmail: config.gmail.clientId ? 'configured' : 'not_configured',
43
43
  microsoft: config.microsoft.clientId ? 'configured' : 'not_configured',
44
44
  },
@@ -8,11 +8,15 @@ import settingsRoutes from './settings.js';
8
8
  import emailsRoutes from './emails.js';
9
9
  import migrateRoutes from './migrate.js';
10
10
  import sdkRoutes from './sdk.js';
11
+ import ttsRoutes from './tts.js';
12
+ import agentRoutes from './agent.js';
11
13
  import rulePacksRoutes from './rulePacks.js';
12
14
  import draftsRoutes from './drafts.js';
13
15
 
16
+
14
17
  const router = Router();
15
18
 
19
+
16
20
  router.use('/health', healthRoutes);
17
21
  router.use('/auth', authRoutes);
18
22
  router.use('/sync', syncRoutes);
@@ -22,7 +26,10 @@ router.use('/settings', settingsRoutes);
22
26
  router.use('/emails', emailsRoutes);
23
27
  router.use('/migrate', migrateRoutes);
24
28
  router.use('/sdk', sdkRoutes);
29
+ router.use('/tts', ttsRoutes);
30
+ router.use('/agent', agentRoutes);
25
31
  router.use('/rule-packs', rulePacksRoutes);
26
32
  router.use('/drafts', draftsRoutes);
27
33
 
34
+
28
35
  export default router;
@@ -13,7 +13,7 @@ const logger = createLogger('MigrateRoutes');
13
13
  router.post('/',
14
14
  validateBody(schemas.migrate),
15
15
  asyncHandler(async (req, res) => {
16
- const { projectRef, accessToken } = req.body;
16
+ const { projectRef, accessToken, anonKey } = req.body;
17
17
 
18
18
  logger.info('Starting migration', { projectRef });
19
19
 
@@ -30,10 +30,17 @@ router.post('/',
30
30
 
31
31
  const scriptPath = join(config.scriptsDir, 'migrate.sh');
32
32
 
33
+ // Construct Supabase URL from project ref
34
+ const supabaseUrl = `https://${projectRef}.supabase.co`;
35
+
33
36
  const env = {
34
37
  ...process.env,
35
38
  SUPABASE_PROJECT_ID: projectRef,
36
39
  SUPABASE_ACCESS_TOKEN: accessToken || '',
40
+ SUPABASE_URL: supabaseUrl,
41
+ // Note: migrate.sh will fetch SERVICE_ROLE_KEY using access token
42
+ // anonKey provided as fallback if API fetch fails
43
+ SUPABASE_ANON_KEY: anonKey || '',
37
44
  SKIP_FUNCTIONS: '0',
38
45
  };
39
46
 
@@ -0,0 +1,187 @@
1
+ import { Request, Response, Router } from 'express';
2
+ import { SDKService } from '../services/SDKService.js';
3
+
4
+ const router = Router();
5
+
6
+ /**
7
+ * GET /api/tts/providers
8
+ * List available TTS providers and their configuration options
9
+ */
10
+ router.get('/providers', async (req: Request, res: Response) => {
11
+ try {
12
+ const sdk = SDKService.getSDK();
13
+ if (!sdk) {
14
+ return res.status(503).json({
15
+ success: false,
16
+ error: 'RealTimeX SDK not available. Please ensure RealTimeX Desktop is running.'
17
+ });
18
+ }
19
+
20
+ // Check if TTS module is available
21
+ if (!sdk.tts) {
22
+ return res.status(503).json({
23
+ success: false,
24
+ error: 'TTS module not available. Please ensure RealTimeX Desktop is running and updated to v1.2.3+.'
25
+ });
26
+ }
27
+
28
+ const providers = await sdk.tts.listProviders();
29
+
30
+ res.json({
31
+ success: true,
32
+ providers
33
+ });
34
+ } catch (error: any) {
35
+ console.error('[TTS API] Failed to list providers:', error);
36
+ res.status(503).json({
37
+ success: false,
38
+ error: error.message || 'Failed to list TTS providers. Ensure RealTimeX Desktop is running.'
39
+ });
40
+ }
41
+ });
42
+
43
+ /**
44
+ * POST /api/tts/speak
45
+ * Generate full audio buffer for text
46
+ * Body: { text: string, provider?: string, voice?: string, speed?: number }
47
+ */
48
+ router.post('/speak', async (req: Request, res: Response) => {
49
+ try {
50
+ const { text, provider, voice, speed, quality } = req.body;
51
+
52
+ console.log('[TTS API] Received TTS request:', { text: text?.substring(0, 50) + '...', provider, voice, speed, quality });
53
+
54
+ if (!text || typeof text !== 'string') {
55
+ return res.status(400).json({
56
+ success: false,
57
+ error: 'Text is required'
58
+ });
59
+ }
60
+
61
+ const sdk = SDKService.getSDK();
62
+ if (!sdk) {
63
+ return res.status(503).json({
64
+ success: false,
65
+ error: 'RealTimeX SDK not available'
66
+ });
67
+ }
68
+
69
+ // Check if TTS module is available
70
+ if (!sdk.tts) {
71
+ return res.status(503).json({
72
+ success: false,
73
+ error: 'TTS module not available. Please ensure RealTimeX Desktop is running and updated to v1.2.3+.'
74
+ });
75
+ }
76
+
77
+ const options: any = {};
78
+ if (provider) options.provider = provider;
79
+ if (voice) options.voice = voice;
80
+ if (speed) options.speed = parseFloat(speed);
81
+ if (quality) options.num_inference_steps = parseInt(quality);
82
+
83
+ console.log('[TTS API] Calling SDK with options:', options);
84
+ const audioBuffer = await sdk.tts.speak(text, options);
85
+
86
+ // Return audio as binary
87
+ res.setHeader('Content-Type', 'audio/mpeg');
88
+ res.send(Buffer.from(audioBuffer));
89
+ } catch (error: any) {
90
+ console.error('[TTS API] Failed to generate speech:', error);
91
+ res.status(500).json({
92
+ success: false,
93
+ error: error.message || 'Failed to generate speech'
94
+ });
95
+ }
96
+ });
97
+
98
+ /**
99
+ * POST /api/tts/stream
100
+ * Stream audio chunks via Server-Sent Events
101
+ * Body: { text: string, provider?: string, voice?: string, speed?: number }
102
+ */
103
+ router.post('/stream', async (req: Request, res: Response) => {
104
+ try {
105
+ const { text, provider, voice, speed, quality } = req.body;
106
+
107
+ if (!text || typeof text !== 'string') {
108
+ return res.status(400).json({
109
+ success: false,
110
+ error: 'Text is required'
111
+ });
112
+ }
113
+
114
+ const sdk = SDKService.getSDK();
115
+ if (!sdk) {
116
+ return res.status(503).json({
117
+ success: false,
118
+ error: 'RealTimeX SDK not available'
119
+ });
120
+ }
121
+
122
+ // Check if TTS module is available
123
+ if (!sdk.tts) {
124
+ return res.status(503).json({
125
+ success: false,
126
+ error: 'TTS module not available. Please ensure RealTimeX Desktop is running and updated to v1.2.3+.'
127
+ });
128
+ }
129
+
130
+ // Set up SSE
131
+ res.setHeader('Content-Type', 'text/event-stream');
132
+ res.setHeader('Cache-Control', 'no-cache');
133
+ res.setHeader('Connection', 'keep-alive');
134
+
135
+ const options: any = {};
136
+ if (provider) options.provider = provider;
137
+ if (voice) options.voice = voice;
138
+ if (speed) options.speed = parseFloat(speed);
139
+ if (quality) options.num_inference_steps = parseInt(quality);
140
+
141
+ // Send info event
142
+ res.write(`event: info\n`);
143
+ res.write(`data: ${JSON.stringify({ message: 'Starting TTS generation...' })}\n\n`);
144
+
145
+ try {
146
+ // Stream chunks
147
+ for await (const chunk of sdk.tts.speakStream(text, options)) {
148
+ // Encode ArrayBuffer to base64
149
+ const base64Audio = Buffer.from(chunk.audio).toString('base64');
150
+
151
+ res.write(`event: chunk\n`);
152
+ res.write(`data: ${JSON.stringify({
153
+ index: chunk.index,
154
+ total: chunk.total,
155
+ audio: base64Audio,
156
+ mimeType: chunk.mimeType
157
+ })}\n\n`);
158
+ }
159
+
160
+ // Send done event
161
+ res.write(`event: done\n`);
162
+ res.write(`data: ${JSON.stringify({ message: 'TTS generation complete' })}\n\n`);
163
+ } catch (streamError: any) {
164
+ res.write(`event: error\n`);
165
+ res.write(`data: ${JSON.stringify({ error: streamError.message })}\n\n`);
166
+ }
167
+
168
+ res.end();
169
+ } catch (error: any) {
170
+ console.error('[TTS API] Failed to stream speech:', error);
171
+
172
+ // If headers not sent yet, send JSON error
173
+ if (!res.headersSent) {
174
+ res.status(500).json({
175
+ success: false,
176
+ error: error.message || 'Failed to stream speech'
177
+ });
178
+ } else {
179
+ // Send SSE error event
180
+ res.write(`event: error\n`);
181
+ res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
182
+ res.end();
183
+ }
184
+ }
185
+ });
186
+
187
+ export default router;