webtalk 1.0.0

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.
package/config.js ADDED
@@ -0,0 +1,36 @@
1
+ const path = require('path');
2
+
3
+ function createConfig(options = {}) {
4
+ const sdkDir = options.sdkDir || __dirname;
5
+
6
+ return {
7
+ // Server configuration
8
+ port: options.port || parseInt(process.env.PORT, 10) || 8080,
9
+
10
+ // Path configuration
11
+ sdkDir,
12
+ modelsDir: options.modelsDir || process.env.MODELS_DIR || path.join(sdkDir, 'models'),
13
+ ttsModelsDir: options.ttsModelsDir || process.env.TTS_MODELS_DIR || path.join(sdkDir, 'models', 'tts'),
14
+ assetsDir: options.assetsDir || path.join(sdkDir, 'assets'),
15
+ ttsDir: options.ttsDir || path.join(sdkDir, 'tts'),
16
+
17
+ // Model configuration
18
+ defaultWhisperModel: options.defaultWhisperModel || process.env.WHISPER_MODEL || 'onnx-community/whisper-base',
19
+ whisperBaseUrl: options.whisperBaseUrl || process.env.WHISPER_BASE_URL || 'https://huggingface.co/',
20
+ ttsBaseUrl: options.ttsBaseUrl || process.env.TTS_BASE_URL || 'https://huggingface.co/KevinAHM/pocket-tts-onnx/resolve/main/onnx/',
21
+
22
+ // Worker configuration
23
+ workerFile: options.workerFile || process.env.WORKER_FILE || 'worker-BPxxCWVT.js',
24
+ workerBackup: options.workerBackup || process.env.WORKER_BACKUP || 'worker-BPxxCWVT-original.js',
25
+ ttsWorkerFile: options.ttsWorkerFile || process.env.TTS_WORKER_FILE || 'inference-worker.js',
26
+
27
+ // URL configuration
28
+ onnxWasmUrl: options.onnxWasmUrl || process.env.ONNX_WASM_URL || 'https://cdn.jsdelivr.net/npm/onnxruntime-web@1.20.1/dist/ort-wasm-simd-threaded.jsep.wasm',
29
+
30
+ // Mount configuration
31
+ mountPath: options.mountPath || process.env.MOUNT_PATH || '/webtalk',
32
+ apiBasePath: options.apiBasePath || process.env.API_BASE_PATH || '',
33
+ };
34
+ }
35
+
36
+ module.exports = { createConfig };
package/debug.js ADDED
@@ -0,0 +1,21 @@
1
+ const fs = require('fs');
2
+
3
+ function createDebugAPI(state) {
4
+ return {
5
+ getDebugInfo() {
6
+ let whisperFiles = 0;
7
+ let ttsFiles = 0;
8
+ try { whisperFiles = fs.readdirSync(state.config.modelsDir).filter(f => !f.startsWith('.')).length; } catch (e) {}
9
+ try { ttsFiles = fs.readdirSync(state.config.ttsModelsDir).filter(f => !f.startsWith('.')).length; } catch (e) {}
10
+ return {
11
+ uptime: process.uptime(),
12
+ memory: process.memoryUsage(),
13
+ nodeVersion: process.version,
14
+ platform: process.platform,
15
+ models: { whisperFiles, ttsFiles }
16
+ };
17
+ }
18
+ };
19
+ }
20
+
21
+ module.exports = { createDebugAPI };
@@ -0,0 +1,26 @@
1
+ const locks = new Map();
2
+ const promises = new Map();
3
+
4
+ function createDownloadLock(key) {
5
+ if (locks.has(key)) return promises.get(key);
6
+ const promise = new Promise((resolve, reject) => {
7
+ locks.set(key, { resolve, reject });
8
+ });
9
+ promises.set(key, promise);
10
+ return promise;
11
+ }
12
+
13
+ function resolveDownloadLock(key, value) {
14
+ const lock = locks.get(key);
15
+ if (lock) { lock.resolve(value); locks.delete(key); promises.delete(key); }
16
+ }
17
+
18
+ function rejectDownloadLock(key, error) {
19
+ const lock = locks.get(key);
20
+ if (lock) { lock.reject(error); locks.delete(key); promises.delete(key); }
21
+ }
22
+
23
+ function getDownloadPromise(key) { return promises.get(key); }
24
+ function isDownloading(key) { return promises.has(key); }
25
+
26
+ module.exports = { createDownloadLock, resolveDownloadLock, rejectDownloadLock, getDownloadPromise, isDownloading };
package/hot-reload.js ADDED
@@ -0,0 +1,78 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { getInFlightCount, setDraining, recordDrain, recordReload } = require('./persistent-state');
4
+
5
+ const DRAIN_TIMEOUT = 5000;
6
+ const DRAIN_INTERVAL = 10;
7
+ const MAX_DRAIN_INTERVAL = 100;
8
+ const WATCH_DEBOUNCE = 300;
9
+
10
+ async function drain(timeout = DRAIN_TIMEOUT) {
11
+ recordDrain();
12
+ setDraining(true);
13
+ const startTime = Date.now();
14
+ let interval = DRAIN_INTERVAL;
15
+
16
+ while (getInFlightCount() > 0) {
17
+ if (Date.now() - startTime > timeout) {
18
+ setDraining(false);
19
+ throw new Error(`Drain timeout: ${getInFlightCount()} requests still in flight`);
20
+ }
21
+ await new Promise(r => setTimeout(r, interval));
22
+ interval = Math.min(interval * 2, MAX_DRAIN_INTERVAL);
23
+ }
24
+
25
+ setDraining(false);
26
+ }
27
+
28
+ function clearRequireCache(modules) {
29
+ modules.forEach(moduleName => {
30
+ const modulePath = path.resolve(moduleName.endsWith('.js') ? moduleName : moduleName + '.js');
31
+ delete require.cache[modulePath];
32
+ try { delete require.cache[require.resolve(modulePath)]; } catch (e) {}
33
+ });
34
+ }
35
+
36
+ function startFileWatcher(watchedFiles, reloadCallback) {
37
+ const watchers = [];
38
+ let reloadTimer = null;
39
+ let pending = false;
40
+
41
+ const onFileChange = () => {
42
+ if (reloadTimer) clearTimeout(reloadTimer);
43
+ pending = true;
44
+
45
+ reloadTimer = setTimeout(async () => {
46
+ if (pending) {
47
+ pending = false;
48
+ try {
49
+ await reloadCallback();
50
+ } catch (error) {
51
+ recordReload(error);
52
+ }
53
+ }
54
+ }, WATCH_DEBOUNCE);
55
+ };
56
+
57
+ try {
58
+ watchedFiles.forEach(filePath => {
59
+ const resolved = path.resolve(filePath);
60
+ const dir = path.dirname(resolved);
61
+ const basename = path.basename(resolved);
62
+ const watcher = fs.watch(dir, { persistent: false }, (eventType, filename) => {
63
+ if (filename === basename) onFileChange();
64
+ });
65
+ watchers.push(watcher);
66
+ });
67
+ } catch (error) {
68
+ recordReload(error);
69
+ }
70
+
71
+ return () => {
72
+ watchers.forEach(w => { try { w.close(); } catch (e) {} });
73
+ watchers.length = 0;
74
+ if (reloadTimer) clearTimeout(reloadTimer);
75
+ };
76
+ }
77
+
78
+ module.exports = { drain, clearRequireCache, startFileWatcher };
package/middleware.js ADDED
@@ -0,0 +1,62 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { initState } = require('./persistent-state');
4
+ const { ensureModel, downloadFile } = require('./whisper-models');
5
+ const { ensureTTSModels, checkTTSModelExists } = require('./tts-models');
6
+ const { patchWorker } = require('./worker-patch');
7
+ const { serveStatic } = require('./serve-static');
8
+
9
+ function webtalk(app, options = {}) {
10
+ const state = initState({ sdkDir: options.sdkDir || __dirname, ...options });
11
+ const config = state.config;
12
+ const mountPath = options.path || config.mountPath;
13
+
14
+ app.use((req, res, next) => {
15
+ res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
16
+ res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
17
+ res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
18
+ next();
19
+ });
20
+
21
+ app.get('/api/tts-status', async (req, res) => {
22
+ try {
23
+ const exists = await checkTTSModelExists(config);
24
+ res.json({ available: exists, modelDir: config.ttsModelsDir });
25
+ } catch (err) {
26
+ res.status(500).json({ error: err.message });
27
+ }
28
+ });
29
+
30
+ app.get(mountPath + '/sdk.js', (req, res) => {
31
+ res.setHeader('Content-Type', 'application/javascript');
32
+ res.sendFile(path.join(config.sdkDir, 'sdk.js'));
33
+ });
34
+
35
+ app.use('/assets', serveStatic(config.assetsDir));
36
+ app.use('/tts', serveStatic(config.ttsDir));
37
+ app.use('/models', serveStatic(config.modelsDir));
38
+
39
+ app.get(mountPath + '/demo', (req, res) => {
40
+ res.setHeader('Content-Type', 'text/html');
41
+ res.sendFile(path.join(config.sdkDir, 'app.html'));
42
+ });
43
+
44
+ app.use(mountPath, serveStatic(config.sdkDir, {
45
+ dotfiles: 'ignore', index: false,
46
+ extensions: ['html', 'js', 'css', 'png', 'svg', 'ico']
47
+ }));
48
+
49
+ const init = async () => {
50
+ try { patchWorker(config); } catch (e) {}
51
+ const ortWasmFile = path.join(config.assetsDir, 'ort-wasm-simd-threaded.jsep.wasm');
52
+ if (!fs.existsSync(ortWasmFile)) {
53
+ await downloadFile(config.onnxWasmUrl, ortWasmFile);
54
+ }
55
+ await ensureModel(config.defaultWhisperModel, config);
56
+ await ensureTTSModels(config);
57
+ };
58
+
59
+ return { init };
60
+ }
61
+
62
+ module.exports = { webtalk };
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "webtalk",
3
+ "version": "1.0.0",
4
+ "description": "Buildless STT (Whisper WebGPU) + TTS (Pocket TTS ONNX) SDK",
5
+ "main": "middleware.js",
6
+ "type": "commonjs",
7
+ "exports": {
8
+ ".": "./middleware.js",
9
+ "./middleware": "./middleware.js",
10
+ "./sdk": "./sdk.js",
11
+ "./stt": "./stt.js",
12
+ "./tts": "./tts.js",
13
+ "./config": "./config.js",
14
+ "./server": "./server.js"
15
+ },
16
+ "scripts": {
17
+ "start": "node server.js"
18
+ },
19
+ "keywords": ["whisper", "webgpu", "stt", "tts", "speech", "audio"],
20
+ "author": "anEntrypoint",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/AnEntrypoint/realtime-whisper-webgpu.git"
24
+ },
25
+ "bugs": {
26
+ "url": "https://github.com/AnEntrypoint/realtime-whisper-webgpu/issues"
27
+ },
28
+ "homepage": "https://github.com/AnEntrypoint/realtime-whisper-webgpu",
29
+ "license": "MIT",
30
+ "engines": {
31
+ "node": ">=14.0.0"
32
+ }
33
+ }
@@ -0,0 +1,62 @@
1
+ const { createConfig } = require('./config');
2
+
3
+ const state = {
4
+ config: null,
5
+ handlers: { current: null },
6
+ requests: { inFlight: 0, draining: false },
7
+ reload: { count: 0, lastTime: 0, lastError: null },
8
+ debug: { reloadEvents: [], drainEvents: [] }
9
+ };
10
+
11
+ function initState(options = {}) {
12
+ if (!state.config) {
13
+ state.config = createConfig(options);
14
+ }
15
+ return state;
16
+ }
17
+
18
+ function trackRequest() { state.requests.inFlight++; }
19
+ function untrackRequest() { state.requests.inFlight--; }
20
+ function getInFlightCount() { return state.requests.inFlight; }
21
+
22
+ function setCurrentHandlers(handlers) { state.handlers.current = handlers; }
23
+ function getCurrentHandlers() { return state.handlers.current; }
24
+
25
+ function setDraining(draining) { state.requests.draining = draining; }
26
+
27
+ function recordReload(error = null) {
28
+ state.reload.count++;
29
+ state.reload.lastTime = Date.now();
30
+ if (error) state.reload.lastError = error.message;
31
+ state.debug.reloadEvents.push({
32
+ count: state.reload.count,
33
+ time: state.reload.lastTime,
34
+ error: error ? error.message : null
35
+ });
36
+ if (state.debug.reloadEvents.length > 100) state.debug.reloadEvents.shift();
37
+ }
38
+
39
+ function recordDrain() {
40
+ state.debug.drainEvents.push({ time: Date.now(), inFlight: state.requests.inFlight });
41
+ if (state.debug.drainEvents.length > 100) state.debug.drainEvents.shift();
42
+ }
43
+
44
+ function getDebugState() {
45
+ return {
46
+ reloadCount: state.reload.count,
47
+ inFlightRequests: state.requests.inFlight,
48
+ isDraining: state.requests.draining,
49
+ lastReloadTime: state.reload.lastTime,
50
+ lastReloadError: state.reload.lastError,
51
+ recentEvents: {
52
+ reloads: state.debug.reloadEvents.slice(-5),
53
+ drains: state.debug.drainEvents.slice(-5)
54
+ }
55
+ };
56
+ }
57
+
58
+ module.exports = {
59
+ initState, trackRequest, untrackRequest, getInFlightCount,
60
+ setCurrentHandlers, getCurrentHandlers,
61
+ setDraining, recordReload, recordDrain, getDebugState
62
+ };
package/sdk.js ADDED
@@ -0,0 +1,22 @@
1
+ import { STT } from './stt.js';
2
+ import { TTS } from './tts.js';
3
+
4
+ const debug = {
5
+ getSDKVersion() {
6
+ return '1.0.0';
7
+ },
8
+ getLoadedModules() {
9
+ return {
10
+ stt: typeof STT !== 'undefined',
11
+ tts: typeof TTS !== 'undefined'
12
+ };
13
+ },
14
+ getPageInfo() {
15
+ return {
16
+ url: typeof window !== 'undefined' ? window.location.href : 'N/A',
17
+ userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : 'N/A'
18
+ };
19
+ }
20
+ };
21
+
22
+ export { STT, TTS, debug };
@@ -0,0 +1,45 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const MIME_TYPES = {
5
+ '.html': 'text/html',
6
+ '.js': 'application/javascript',
7
+ '.mjs': 'application/javascript',
8
+ '.css': 'text/css',
9
+ '.json': 'application/json',
10
+ '.png': 'image/png',
11
+ '.jpg': 'image/jpeg',
12
+ '.gif': 'image/gif',
13
+ '.svg': 'image/svg+xml',
14
+ '.ico': 'image/x-icon',
15
+ '.woff': 'font/woff',
16
+ '.woff2': 'font/woff2',
17
+ '.ttf': 'font/ttf',
18
+ '.eot': 'application/vnd.ms-fontobject',
19
+ '.otf': 'font/otf',
20
+ '.onnx': 'application/octet-stream',
21
+ '.wasm': 'application/wasm',
22
+ '.bin': 'application/octet-stream',
23
+ '.model': 'application/octet-stream'
24
+ };
25
+
26
+ function serveStatic(root, options = {}) {
27
+ return (req, res, next) => {
28
+ const urlPath = decodeURIComponent(req.path || req.url);
29
+ const filePath = path.join(root, urlPath);
30
+
31
+ if (!filePath.startsWith(root)) {
32
+ return res.status(403).end('Forbidden');
33
+ }
34
+
35
+ fs.stat(filePath, (err, stats) => {
36
+ if (err || !stats.isFile()) return next();
37
+ const ext = path.extname(filePath).toLowerCase();
38
+ const contentType = MIME_TYPES[ext] || 'application/octet-stream';
39
+ res.setHeader('Content-Type', contentType);
40
+ fs.createReadStream(filePath).pipe(res);
41
+ });
42
+ };
43
+ }
44
+
45
+ module.exports = { serveStatic };
package/server.js ADDED
@@ -0,0 +1,177 @@
1
+ const http = require('http');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const url = require('url');
5
+ const { webtalk } = require('./middleware');
6
+ const { initState, trackRequest, untrackRequest, getCurrentHandlers, setCurrentHandlers, getDebugState } = require('./persistent-state');
7
+ const { startFileWatcher, drain, clearRequireCache } = require('./hot-reload');
8
+ const { createDebugAPI } = require('./debug');
9
+
10
+ const MIME_TYPES = {
11
+ '.html': 'text/html', '.js': 'application/javascript',
12
+ '.css': 'text/css', '.json': 'application/json', '.png': 'image/png'
13
+ };
14
+
15
+ const state = initState();
16
+ const config = state.config;
17
+ const debugAPI = createDebugAPI(state);
18
+ let stopWatcher = null;
19
+
20
+ process.webtalk = debugAPI;
21
+
22
+ function createApp() {
23
+ function app(req, res) {
24
+ trackRequest();
25
+ const onEnd = () => {
26
+ untrackRequest();
27
+ res.removeListener('finish', onEnd);
28
+ res.removeListener('close', onEnd);
29
+ };
30
+ res.on('finish', onEnd);
31
+ res.on('close', onEnd);
32
+
33
+ res.json = (data) => {
34
+ res.setHeader('Content-Type', 'application/json');
35
+ res.end(JSON.stringify(data));
36
+ };
37
+ res.status = (code) => { res.statusCode = code; return res; };
38
+ res.sendFile = (filePath) => {
39
+ const ext = path.extname(filePath).toLowerCase();
40
+ res.setHeader('Content-Type', MIME_TYPES[ext] || 'application/octet-stream');
41
+ fs.createReadStream(filePath).pipe(res);
42
+ };
43
+
44
+ const parsed = url.parse(req.url);
45
+ req.path = parsed.pathname;
46
+
47
+ const handlers = getCurrentHandlers();
48
+ if (!handlers) {
49
+ res.statusCode = 503;
50
+ res.end('Server initializing');
51
+ return;
52
+ }
53
+
54
+ let idx = 0;
55
+ const allHandlers = [];
56
+
57
+ for (const item of handlers.USE) {
58
+ allHandlers.push({ prefix: item.prefix, handler: item.handler });
59
+ }
60
+
61
+ if (req.method === 'GET') {
62
+ for (const [routePath, handler] of Object.entries(handlers.GET)) {
63
+ allHandlers.push({ exactPath: routePath, handler });
64
+ }
65
+ }
66
+
67
+ if (req.method === 'OPTIONS') {
68
+ res.writeHead(200);
69
+ res.end();
70
+ return;
71
+ }
72
+
73
+ function next() {
74
+ if (idx >= allHandlers.length) {
75
+ if (req.path === '/' || req.path === '') {
76
+ res.sendFile(path.join(__dirname, 'app.html'));
77
+ return;
78
+ }
79
+ res.statusCode = 404;
80
+ res.end('Not Found');
81
+ return;
82
+ }
83
+ const h = allHandlers[idx++];
84
+ if (h.exactPath) {
85
+ if (req.path === h.exactPath) {
86
+ h.handler(req, res, next);
87
+ } else {
88
+ next();
89
+ }
90
+ } else if (h.prefix) {
91
+ if (req.path.startsWith(h.prefix)) {
92
+ const originalPath = req.path;
93
+ req.path = req.path.slice(h.prefix.length) || '/';
94
+ req.url = req.path;
95
+ h.handler(req, res, () => {
96
+ req.path = originalPath;
97
+ req.url = originalPath;
98
+ next();
99
+ });
100
+ } else {
101
+ next();
102
+ }
103
+ } else {
104
+ h.handler(req, res, next);
105
+ }
106
+ }
107
+
108
+ next();
109
+ }
110
+
111
+ app.get = (p, handler) => {
112
+ const handlers = getCurrentHandlers() || { GET: {}, USE: [] };
113
+ handlers.GET[p] = handler;
114
+ setCurrentHandlers(handlers);
115
+ };
116
+
117
+ app.use = (prefixOrHandler, handler) => {
118
+ const handlers = getCurrentHandlers() || { GET: {}, USE: [] };
119
+ if (typeof prefixOrHandler === 'function') {
120
+ handlers.USE.push({ prefix: null, handler: prefixOrHandler });
121
+ } else {
122
+ handlers.USE.push({ prefix: prefixOrHandler, handler });
123
+ }
124
+ setCurrentHandlers(handlers);
125
+ };
126
+
127
+ return app;
128
+ }
129
+
130
+ const app = createApp();
131
+ const { init } = webtalk(app);
132
+ const server = http.createServer(app);
133
+
134
+ server.on('error', (err) => {
135
+ process.stderr.write(err.message + '\n');
136
+ process.exit(1);
137
+ });
138
+
139
+ async function reloadMiddleware() {
140
+ await drain();
141
+ clearRequireCache(['./middleware.js', './config.js']);
142
+ delete require.cache[require.resolve('./middleware.js')];
143
+ delete require.cache[require.resolve('./config.js')];
144
+ const { webtalk: reloadedWebtalk } = require('./middleware.js');
145
+ const newApp = createApp();
146
+ const { init: newInit } = reloadedWebtalk(newApp);
147
+ await newInit();
148
+ newApp.get('/api/debug', (req, res) => {
149
+ res.json(getDebugState());
150
+ });
151
+ }
152
+
153
+ function shutdown() {
154
+ if (stopWatcher) stopWatcher();
155
+ server.close(() => process.exit(0));
156
+ setTimeout(() => process.exit(0), 2000).unref();
157
+ }
158
+
159
+ async function startServer() {
160
+ await init();
161
+ app.get('/api/debug', (req, res) => {
162
+ res.json({ ...getDebugState(), api: debugAPI.getDebugInfo() });
163
+ });
164
+ server.listen(config.port, '0.0.0.0');
165
+ stopWatcher = startFileWatcher(
166
+ ['./middleware.js', './config.js'],
167
+ reloadMiddleware
168
+ );
169
+ }
170
+
171
+ startServer().catch((err) => {
172
+ process.stderr.write((err.message || 'Startup failed') + '\n');
173
+ process.exit(1);
174
+ });
175
+
176
+ process.on('SIGTERM', shutdown);
177
+ process.on('SIGINT', shutdown);