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.
Files changed (2) hide show
  1. package/dist/index.js +101 -25
  2. 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
- // Set up API KEY
18
- let API_KEY = process.env.API_KEY;
19
- if (!API_KEY) {
20
- API_KEY = crypto_1.default.randomBytes(32).toString('hex');
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(`Server started without API_KEY in environment variables.`);
23
- console.log(`Generated a new token for this session.`);
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
- // Ensure models directory exists
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
- // Authentication middleware
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
- // Configure multer to store uploaded files in a temp directory
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 logic
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
- // Queue for pending transcriptions to send them sequentially to the single worker
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
- // Register resolver
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
- // The route
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(`Handy Remote Server is running on port ${port}`);
233
+ console.log(`\nHandy Remote Server is running on port ${port}`);
234
+ console.log(`Waiting for requests...\n`);
159
235
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "handy-remote-server",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Remote Transcription Server for Handy",
5
5
  "main": "dist/index.js",
6
6
  "bin": {