handy-remote-server 1.0.0 → 1.1.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/dist/index.js +101 -25
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10,42 +10,115 @@ const child_process_1 = require("child_process");
|
|
|
10
10
|
const crypto_1 = __importDefault(require("crypto"));
|
|
11
11
|
const path_1 = __importDefault(require("path"));
|
|
12
12
|
const fs_1 = __importDefault(require("fs"));
|
|
13
|
+
const os_1 = __importDefault(require("os"));
|
|
13
14
|
const dotenv_1 = __importDefault(require("dotenv"));
|
|
14
15
|
dotenv_1.default.config();
|
|
15
16
|
const app = (0, express_1.default)();
|
|
16
17
|
const port = process.env.PORT || 3000;
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
// ── Persistent API Key ────────────────────────────────────────────────
|
|
19
|
+
const handyDir = path_1.default.join(os_1.default.homedir(), '.handy');
|
|
20
|
+
const keyFilePath = path_1.default.join(handyDir, 'api_key');
|
|
21
|
+
function loadOrCreateApiKey() {
|
|
22
|
+
// 1. Env var takes priority
|
|
23
|
+
if (process.env.API_KEY) {
|
|
24
|
+
return process.env.API_KEY;
|
|
25
|
+
}
|
|
26
|
+
// 2. Try to load from cached file
|
|
27
|
+
if (fs_1.default.existsSync(keyFilePath)) {
|
|
28
|
+
const cached = fs_1.default.readFileSync(keyFilePath, 'utf-8').trim();
|
|
29
|
+
if (cached.length > 0) {
|
|
30
|
+
console.log(`\n======================================================`);
|
|
31
|
+
console.log(`Loaded API KEY from ${keyFilePath}`);
|
|
32
|
+
console.log(`Your API KEY is: ${cached}`);
|
|
33
|
+
console.log(`======================================================\n`);
|
|
34
|
+
return cached;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// 3. Generate a new one and persist it
|
|
38
|
+
const newKey = crypto_1.default.randomBytes(32).toString('hex');
|
|
39
|
+
fs_1.default.mkdirSync(handyDir, { recursive: true });
|
|
40
|
+
fs_1.default.writeFileSync(keyFilePath, newKey + '\n', { mode: 0o600 });
|
|
21
41
|
console.log(`\n======================================================`);
|
|
22
|
-
console.log(`
|
|
23
|
-
console.log(`
|
|
24
|
-
console.log(`Your API KEY is: ${API_KEY}`);
|
|
42
|
+
console.log(`Generated a new API KEY (saved to ${keyFilePath})`);
|
|
43
|
+
console.log(`Your API KEY is: ${newKey}`);
|
|
25
44
|
console.log(`======================================================\n`);
|
|
45
|
+
return newKey;
|
|
46
|
+
}
|
|
47
|
+
const API_KEY = loadOrCreateApiKey();
|
|
48
|
+
// ── Logging helpers ───────────────────────────────────────────────────
|
|
49
|
+
function timestamp() {
|
|
50
|
+
return new Date().toISOString();
|
|
51
|
+
}
|
|
52
|
+
function formatBytes(bytes) {
|
|
53
|
+
if (bytes < 1024)
|
|
54
|
+
return `${bytes} B`;
|
|
55
|
+
if (bytes < 1024 * 1024)
|
|
56
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
57
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
26
58
|
}
|
|
27
|
-
|
|
59
|
+
function formatDuration(ms) {
|
|
60
|
+
if (ms < 1000)
|
|
61
|
+
return `${ms}ms`;
|
|
62
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
63
|
+
}
|
|
64
|
+
// ── Ensure directories ────────────────────────────────────────────────
|
|
28
65
|
const modelsDir = path_1.default.join(__dirname, '..', 'models');
|
|
29
66
|
if (!fs_1.default.existsSync(modelsDir)) {
|
|
30
67
|
fs_1.default.mkdirSync(modelsDir, { recursive: true });
|
|
31
68
|
}
|
|
32
|
-
|
|
69
|
+
const uploadDir = path_1.default.join(__dirname, '..', 'uploads');
|
|
70
|
+
if (!fs_1.default.existsSync(uploadDir)) {
|
|
71
|
+
fs_1.default.mkdirSync(uploadDir, { recursive: true });
|
|
72
|
+
}
|
|
73
|
+
// ── Request logging middleware ────────────────────────────────────────
|
|
74
|
+
let requestCounter = 0;
|
|
75
|
+
app.use((req, res, next) => {
|
|
76
|
+
const reqId = ++requestCounter;
|
|
77
|
+
const start = Date.now();
|
|
78
|
+
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
79
|
+
console.log(`\n[${timestamp()}] ── REQUEST #${reqId} ──────────────────────`);
|
|
80
|
+
console.log(` Method: ${req.method} ${req.path}`);
|
|
81
|
+
console.log(` From: ${ip}`);
|
|
82
|
+
console.log(` Headers: Content-Type=${req.headers['content-type'] || 'N/A'}, Content-Length=${req.headers['content-length'] || 'N/A'}`);
|
|
83
|
+
// Store metadata on request for later use
|
|
84
|
+
req._reqId = reqId;
|
|
85
|
+
req._startTime = start;
|
|
86
|
+
req._ip = ip;
|
|
87
|
+
const originalJson = res.json.bind(res);
|
|
88
|
+
res.json = function (body) {
|
|
89
|
+
const duration = Date.now() - start;
|
|
90
|
+
const status = res.statusCode;
|
|
91
|
+
console.log(`[${timestamp()}] ── RESPONSE #${reqId} ─────────────────────`);
|
|
92
|
+
console.log(` Status: ${status}`);
|
|
93
|
+
console.log(` Duration: ${formatDuration(duration)}`);
|
|
94
|
+
if (body?.text) {
|
|
95
|
+
const preview = body.text.length > 100 ? body.text.substring(0, 100) + '...' : body.text;
|
|
96
|
+
console.log(` Result: "${preview}"`);
|
|
97
|
+
}
|
|
98
|
+
else if (body?.error) {
|
|
99
|
+
console.log(` Error: ${body.error}`);
|
|
100
|
+
}
|
|
101
|
+
console.log(` ────────────────────────────────────────────────`);
|
|
102
|
+
return originalJson(body);
|
|
103
|
+
};
|
|
104
|
+
next();
|
|
105
|
+
});
|
|
106
|
+
// ── Authentication middleware ─────────────────────────────────────────
|
|
33
107
|
app.use((req, res, next) => {
|
|
34
108
|
const authHeader = req.headers.authorization;
|
|
35
109
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
110
|
+
console.log(` Auth: REJECTED (missing/invalid Authorization header)`);
|
|
36
111
|
return res.status(401).json({ error: 'Missing or invalid Authorization header' });
|
|
37
112
|
}
|
|
38
113
|
const token = authHeader.split(' ')[1];
|
|
39
114
|
if (token !== API_KEY) {
|
|
115
|
+
console.log(` Auth: REJECTED (invalid key)`);
|
|
40
116
|
return res.status(403).json({ error: 'Invalid API Key' });
|
|
41
117
|
}
|
|
118
|
+
console.log(` Auth: OK`);
|
|
42
119
|
next();
|
|
43
120
|
});
|
|
44
|
-
//
|
|
45
|
-
const uploadDir = path_1.default.join(__dirname, '..', 'uploads');
|
|
46
|
-
if (!fs_1.default.existsSync(uploadDir)) {
|
|
47
|
-
fs_1.default.mkdirSync(uploadDir, { recursive: true });
|
|
48
|
-
}
|
|
121
|
+
// ── Multer storage ────────────────────────────────────────────────────
|
|
49
122
|
const storage = multer_1.default.diskStorage({
|
|
50
123
|
destination: function (req, file, cb) {
|
|
51
124
|
cb(null, uploadDir);
|
|
@@ -56,7 +129,7 @@ const storage = multer_1.default.diskStorage({
|
|
|
56
129
|
}
|
|
57
130
|
});
|
|
58
131
|
const upload = (0, multer_1.default)({ storage: storage });
|
|
59
|
-
// Model download
|
|
132
|
+
// ── Model download ───────────────────────────────────────────────────
|
|
60
133
|
const GIGAAM_MODEL_URL = 'https://blob.handy.computer/giga-am-v3.int8.onnx';
|
|
61
134
|
const gigaamModelPath = path_1.default.join(modelsDir, 'gigaam.onnx');
|
|
62
135
|
async function downloadFile(url, dest) {
|
|
@@ -78,7 +151,6 @@ let inferProcess = null;
|
|
|
78
151
|
let isReady = false;
|
|
79
152
|
let resolvers = {};
|
|
80
153
|
ensureModels().then(() => {
|
|
81
|
-
// Spawn the rust background process
|
|
82
154
|
let inferProcessPath = process.env.INFER_CLI_PATH || path_1.default.join(__dirname, '..', 'rust-infer', 'target', 'release', 'rust-infer');
|
|
83
155
|
if (!fs_1.default.existsSync(inferProcessPath)) {
|
|
84
156
|
inferProcessPath = path_1.default.join(__dirname, '..', 'rust-infer', 'target', 'debug', 'rust-infer');
|
|
@@ -114,7 +186,7 @@ ensureModels().then(() => {
|
|
|
114
186
|
console.error('Failed to download models:', e);
|
|
115
187
|
process.exit(1);
|
|
116
188
|
});
|
|
117
|
-
//
|
|
189
|
+
// ── Request queue ─────────────────────────────────────────────────────
|
|
118
190
|
const requestQueue = [];
|
|
119
191
|
let isProcessing = false;
|
|
120
192
|
function processQueue() {
|
|
@@ -122,38 +194,42 @@ function processQueue() {
|
|
|
122
194
|
return;
|
|
123
195
|
isProcessing = true;
|
|
124
196
|
const req = requestQueue.shift();
|
|
125
|
-
|
|
197
|
+
console.log(` [Queue] Processing request #${req.reqId} (queue length: ${requestQueue.length})`);
|
|
126
198
|
resolvers[req.file] = (result) => {
|
|
127
199
|
delete resolvers[req.file];
|
|
128
200
|
isProcessing = false;
|
|
129
|
-
// Clean up temp file
|
|
130
201
|
if (fs_1.default.existsSync(req.file)) {
|
|
131
202
|
fs_1.default.unlinkSync(req.file);
|
|
132
203
|
}
|
|
133
204
|
req.resolve(result);
|
|
134
|
-
// Process next
|
|
135
205
|
process.nextTick(processQueue);
|
|
136
206
|
};
|
|
137
|
-
// Send path to worker via stdin
|
|
138
207
|
inferProcess.stdin.write(req.file + '\n');
|
|
139
208
|
}
|
|
140
|
-
//
|
|
209
|
+
// ── Transcription endpoint ────────────────────────────────────────────
|
|
141
210
|
app.post('/transcribe', express_1.default.raw({ type: 'audio/wav', limit: '50mb' }), async (req, res) => {
|
|
211
|
+
const reqId = req._reqId || 0;
|
|
142
212
|
if (!isReady) {
|
|
213
|
+
console.log(` [#${reqId}] Rejected: models still loading`);
|
|
143
214
|
return res.status(503).json({ error: 'Models are still loading' });
|
|
144
215
|
}
|
|
145
|
-
// If using express.raw, the body is a Buffer
|
|
146
216
|
if (!req.body || !Buffer.isBuffer(req.body)) {
|
|
217
|
+
console.log(` [#${reqId}] Rejected: invalid audio body`);
|
|
147
218
|
return res.status(400).json({ error: 'Invalid audio body. Send raw WAV bytes with Content-Type: audio/wav' });
|
|
148
219
|
}
|
|
220
|
+
const audioSize = req.body.length;
|
|
221
|
+
console.log(` [#${reqId}] Audio received: ${formatBytes(audioSize)}`);
|
|
149
222
|
const tempFilePath = path_1.default.join(uploadDir, `upload-${Date.now()}-${Math.random().toString(36).substring(7)}.wav`);
|
|
150
223
|
fs_1.default.writeFileSync(tempFilePath, req.body);
|
|
224
|
+
console.log(` [#${reqId}] Queued for inference (queue length: ${requestQueue.length})`);
|
|
151
225
|
const result = await new Promise((resolve) => {
|
|
152
|
-
requestQueue.push({ file: tempFilePath, resolve });
|
|
226
|
+
requestQueue.push({ file: tempFilePath, resolve, reqId });
|
|
153
227
|
processQueue();
|
|
154
228
|
});
|
|
155
229
|
res.json(result);
|
|
156
230
|
});
|
|
231
|
+
// ── Start server ──────────────────────────────────────────────────────
|
|
157
232
|
app.listen(port, () => {
|
|
158
|
-
console.log(
|
|
233
|
+
console.log(`\nHandy Remote Server is running on port ${port}`);
|
|
234
|
+
console.log(`Waiting for requests...\n`);
|
|
159
235
|
});
|