Files
homeai/homeai-character/vite.config.js
Aodhan Collins 1bfd7fbd08 feat: character dashboard with TTS voice preview, fix Wyoming API compat
- Add HomeAI dashboard: service status monitor, character profile manager, character editor
- Add TTS voice preview in character editor (Kokoro via OpenClaw bridge → Wyoming)
  - Custom preview text, loading/playing states, stop control, speed via playbackRate
- Fix Wyoming API breaking changes: remove `version` from TtsVoice/TtsProgram,
  use SynthesizeVoice object instead of bare string in Synthesize calls
- Vite dev server proxies /api/tts and /api/health to avoid CORS issues

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 20:40:11 +00:00

103 lines
3.8 KiB
JavaScript

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
function healthCheckPlugin() {
return {
name: 'health-check-proxy',
configureServer(server) {
server.middlewares.use('/api/health', async (req, res) => {
const params = new URL(req.url, 'http://localhost').searchParams;
const url = params.get('url');
const mode = params.get('mode'); // 'tcp' for raw TCP port check
if (!url) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Missing url param' }));
return;
}
const start = Date.now();
const parsedUrl = new URL(url);
try {
if (mode === 'tcp') {
// TCP socket connect check for non-HTTP services (e.g. Wyoming)
const { default: net } = await import('net');
await new Promise((resolve, reject) => {
const socket = net.createConnection(
{ host: parsedUrl.hostname, port: parseInt(parsedUrl.port), timeout: 5000 },
() => { socket.destroy(); resolve(); }
);
socket.on('error', reject);
socket.on('timeout', () => { socket.destroy(); reject(new Error('timeout')); });
});
} else {
// HTTP/HTTPS health check
const { default: https } = await import('https');
const { default: http } = await import('http');
const client = parsedUrl.protocol === 'https:' ? https : http;
await new Promise((resolve, reject) => {
const reqObj = client.get(url, { rejectUnauthorized: false, timeout: 5000 }, (resp) => {
resp.resume();
resolve();
});
reqObj.on('error', reject);
reqObj.on('timeout', () => { reqObj.destroy(); reject(new Error('timeout')); });
});
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'online', responseTime: Date.now() - start }));
} catch {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'offline', responseTime: null }));
}
});
// TTS preview proxy — forwards POST to OpenClaw bridge, returns audio
server.middlewares.use('/api/tts', async (req, res) => {
if (req.method !== 'POST') {
res.writeHead(405);
res.end();
return;
}
try {
const { default: http } = await import('http');
const chunks = [];
for await (const chunk of req) chunks.push(chunk);
const body = Buffer.concat(chunks);
await new Promise((resolve, reject) => {
const proxyReq = http.request(
'http://localhost:8081/api/tts',
{ method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': body.length }, timeout: 30000 },
(proxyRes) => {
res.writeHead(proxyRes.statusCode, {
'Content-Type': proxyRes.headers['content-type'] || 'audio/wav',
});
proxyRes.pipe(res);
proxyRes.on('end', resolve);
}
);
proxyReq.on('error', reject);
proxyReq.on('timeout', () => { proxyReq.destroy(); reject(new Error('timeout')); });
proxyReq.write(body);
proxyReq.end();
});
} catch {
res.writeHead(502, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'TTS bridge unreachable' }));
}
});
},
};
}
// https://vite.dev/config/
export default defineConfig({
plugins: [
healthCheckPlugin(),
tailwindcss(),
react(),
],
})