escribano 0.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/LICENSE +21 -0
- package/README.md +297 -0
- package/dist/0_types.js +279 -0
- package/dist/actions/classify-session.js +77 -0
- package/dist/actions/create-contexts.js +44 -0
- package/dist/actions/create-topic-blocks.js +68 -0
- package/dist/actions/extract-metadata.js +24 -0
- package/dist/actions/generate-artifact-v3.js +296 -0
- package/dist/actions/generate-artifact.js +61 -0
- package/dist/actions/generate-summary-v3.js +260 -0
- package/dist/actions/outline-index.js +204 -0
- package/dist/actions/process-recording-v2.js +494 -0
- package/dist/actions/process-recording-v3.js +412 -0
- package/dist/actions/process-session.js +183 -0
- package/dist/actions/publish-summary-v3.js +303 -0
- package/dist/actions/sync-to-outline.js +196 -0
- package/dist/adapters/audio.silero.adapter.js +69 -0
- package/dist/adapters/cap.adapter.js +94 -0
- package/dist/adapters/capture.cap.adapter.js +107 -0
- package/dist/adapters/capture.filesystem.adapter.js +124 -0
- package/dist/adapters/embedding.ollama.adapter.js +141 -0
- package/dist/adapters/intelligence.adapter.js +202 -0
- package/dist/adapters/intelligence.mlx.adapter.js +395 -0
- package/dist/adapters/intelligence.ollama.adapter.js +741 -0
- package/dist/adapters/publishing.outline.adapter.js +75 -0
- package/dist/adapters/storage.adapter.js +81 -0
- package/dist/adapters/storage.fs.adapter.js +83 -0
- package/dist/adapters/transcription.whisper.adapter.js +206 -0
- package/dist/adapters/video.ffmpeg.adapter.js +405 -0
- package/dist/adapters/whisper.adapter.js +168 -0
- package/dist/batch-context.js +329 -0
- package/dist/db/helpers.js +50 -0
- package/dist/db/index.js +95 -0
- package/dist/db/migrate.js +80 -0
- package/dist/db/repositories/artifact.sqlite.js +77 -0
- package/dist/db/repositories/cluster.sqlite.js +92 -0
- package/dist/db/repositories/context.sqlite.js +75 -0
- package/dist/db/repositories/index.js +10 -0
- package/dist/db/repositories/observation.sqlite.js +70 -0
- package/dist/db/repositories/recording.sqlite.js +56 -0
- package/dist/db/repositories/subject.sqlite.js +64 -0
- package/dist/db/repositories/topic-block.sqlite.js +45 -0
- package/dist/db/types.js +4 -0
- package/dist/domain/classification.js +60 -0
- package/dist/domain/context.js +97 -0
- package/dist/domain/index.js +2 -0
- package/dist/domain/observation.js +17 -0
- package/dist/domain/recording.js +41 -0
- package/dist/domain/segment.js +93 -0
- package/dist/domain/session.js +93 -0
- package/dist/domain/time-range.js +38 -0
- package/dist/domain/transcript.js +79 -0
- package/dist/index.js +173 -0
- package/dist/pipeline/context.js +162 -0
- package/dist/pipeline/events.js +2 -0
- package/dist/prerequisites.js +226 -0
- package/dist/scripts/rebuild-index.js +53 -0
- package/dist/scripts/seed-fixtures.js +290 -0
- package/dist/services/activity-segmentation.js +333 -0
- package/dist/services/activity-segmentation.test.js +191 -0
- package/dist/services/app-normalization.js +212 -0
- package/dist/services/cluster-merge.js +69 -0
- package/dist/services/clustering.js +237 -0
- package/dist/services/debug.js +58 -0
- package/dist/services/frame-sampling.js +318 -0
- package/dist/services/signal-extraction.js +106 -0
- package/dist/services/subject-grouping.js +342 -0
- package/dist/services/temporal-alignment.js +99 -0
- package/dist/services/vlm-enrichment.js +84 -0
- package/dist/services/vlm-service.js +130 -0
- package/dist/stats/index.js +3 -0
- package/dist/stats/observer.js +65 -0
- package/dist/stats/repository.js +36 -0
- package/dist/stats/resource-tracker.js +86 -0
- package/dist/stats/types.js +1 -0
- package/dist/test-classification-prompts.js +181 -0
- package/dist/tests/cap.adapter.test.js +75 -0
- package/dist/tests/capture.cap.adapter.test.js +69 -0
- package/dist/tests/classify-session.test.js +140 -0
- package/dist/tests/db/repositories.test.js +243 -0
- package/dist/tests/domain/time-range.test.js +31 -0
- package/dist/tests/integration.test.js +84 -0
- package/dist/tests/intelligence.adapter.test.js +102 -0
- package/dist/tests/intelligence.ollama.adapter.test.js +178 -0
- package/dist/tests/process-v2.test.js +90 -0
- package/dist/tests/services/clustering.test.js +112 -0
- package/dist/tests/services/frame-sampling.test.js +152 -0
- package/dist/tests/utils/ocr.test.js +76 -0
- package/dist/tests/utils/parallel.test.js +57 -0
- package/dist/tests/visual-observer.test.js +175 -0
- package/dist/utils/id-normalization.js +15 -0
- package/dist/utils/index.js +9 -0
- package/dist/utils/model-detector.js +154 -0
- package/dist/utils/ocr.js +80 -0
- package/dist/utils/parallel.js +32 -0
- package/migrations/001_initial.sql +109 -0
- package/migrations/002_clusters.sql +41 -0
- package/migrations/003_observations_vlm_fields.sql +14 -0
- package/migrations/004_observations_unique.sql +18 -0
- package/migrations/005_processing_stats.sql +29 -0
- package/migrations/006_vlm_raw_response.sql +6 -0
- package/migrations/007_subjects.sql +23 -0
- package/migrations/008_artifacts_recording.sql +6 -0
- package/migrations/009_artifact_subjects.sql +10 -0
- package/package.json +82 -0
- package/prompts/action-items.md +55 -0
- package/prompts/blog-draft.md +54 -0
- package/prompts/blog-research.md +87 -0
- package/prompts/card.md +54 -0
- package/prompts/classify-segment.md +38 -0
- package/prompts/classify.md +37 -0
- package/prompts/code-snippets.md +163 -0
- package/prompts/extract-metadata.md +149 -0
- package/prompts/notes.md +83 -0
- package/prompts/runbook.md +123 -0
- package/prompts/standup.md +50 -0
- package/prompts/step-by-step.md +125 -0
- package/prompts/subject-grouping.md +31 -0
- package/prompts/summary-v3.md +89 -0
- package/prompts/summary.md +77 -0
- package/prompts/topic-classifier.md +24 -0
- package/prompts/topic-extract.md +13 -0
- package/prompts/vlm-batch.md +21 -0
- package/prompts/vlm-single.md +19 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escribano - Intelligence Adapter (MLX-VLM)
|
|
3
|
+
*
|
|
4
|
+
* Implements IntelligenceService using MLX-VLM via Unix domain socket.
|
|
5
|
+
* Uses interleaved batching for 4.7x speedup over Ollama sequential processing.
|
|
6
|
+
*
|
|
7
|
+
* Architecture:
|
|
8
|
+
* TypeScript (this file) <--Unix Socket--> Python (mlx_bridge.py)
|
|
9
|
+
*
|
|
10
|
+
* See docs/adr/006-mlx-vlm-adapter.md for full design.
|
|
11
|
+
*/
|
|
12
|
+
import { spawn } from 'node:child_process';
|
|
13
|
+
import { existsSync, unlinkSync } from 'node:fs';
|
|
14
|
+
import { createConnection } from 'node:net';
|
|
15
|
+
import { homedir } from 'node:os';
|
|
16
|
+
import { resolve } from 'node:path';
|
|
17
|
+
const DEBUG_MLX = process.env.ESCRIBANO_VERBOSE === 'true';
|
|
18
|
+
function debugLog(...args) {
|
|
19
|
+
if (DEBUG_MLX) {
|
|
20
|
+
console.log('[VLM] [MLX]', ...args);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const DEFAULT_CONFIG = {
|
|
24
|
+
model: process.env.ESCRIBANO_VLM_MODEL ??
|
|
25
|
+
'mlx-community/Qwen3-VL-2B-Instruct-bf16',
|
|
26
|
+
batchSize: Number(process.env.ESCRIBANO_VLM_BATCH_SIZE) || 4,
|
|
27
|
+
maxTokens: Number(process.env.ESCRIBANO_VLM_MAX_TOKENS) || 2000,
|
|
28
|
+
socketPath: process.env.ESCRIBANO_MLX_SOCKET_PATH ?? '/tmp/escribano-mlx.sock',
|
|
29
|
+
bridgeScript: resolve(process.cwd(), 'scripts/mlx_bridge.py'),
|
|
30
|
+
startupTimeout: Number(process.env.ESCRIBANO_MLX_STARTUP_TIMEOUT) || 60000,
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Get Python executable path.
|
|
34
|
+
* Priority:
|
|
35
|
+
* 1. ESCRIBANO_PYTHON_PATH env var (explicit override)
|
|
36
|
+
* 2. Active virtual environment (VIRTUAL_ENV)
|
|
37
|
+
* 3. System python3 (fallback)
|
|
38
|
+
*/
|
|
39
|
+
function getPythonPath() {
|
|
40
|
+
if (process.env.ESCRIBANO_PYTHON_PATH) {
|
|
41
|
+
return process.env.ESCRIBANO_PYTHON_PATH;
|
|
42
|
+
}
|
|
43
|
+
if (process.env.VIRTUAL_ENV) {
|
|
44
|
+
return resolve(process.env.VIRTUAL_ENV, 'bin', 'python3');
|
|
45
|
+
}
|
|
46
|
+
// Check common uv venv location (typically ~/.venv)
|
|
47
|
+
const uvHomeVenv = resolve(homedir(), '.venv', 'bin', 'python3');
|
|
48
|
+
if (existsSync(uvHomeVenv)) {
|
|
49
|
+
return uvHomeVenv;
|
|
50
|
+
}
|
|
51
|
+
return 'python3';
|
|
52
|
+
}
|
|
53
|
+
// Global cleanup function to track the current bridge instance
|
|
54
|
+
let globalCleanup = null;
|
|
55
|
+
/**
|
|
56
|
+
* Cleanup the MLX bridge process.
|
|
57
|
+
* Should be called explicitly before process exit.
|
|
58
|
+
*/
|
|
59
|
+
export function cleanupMlxBridge() {
|
|
60
|
+
if (globalCleanup) {
|
|
61
|
+
debugLog('Explicit cleanup called');
|
|
62
|
+
globalCleanup();
|
|
63
|
+
globalCleanup = null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Create MLX-VLM intelligence service.
|
|
68
|
+
*
|
|
69
|
+
* Note: This adapter only implements describeImages() for VLM processing.
|
|
70
|
+
* Other methods (classify, generate, etc.) are not implemented and will throw.
|
|
71
|
+
*/
|
|
72
|
+
export function createMlxIntelligenceService(_config = {}) {
|
|
73
|
+
const mlxConfig = { ...DEFAULT_CONFIG };
|
|
74
|
+
const bridge = {
|
|
75
|
+
process: null,
|
|
76
|
+
socket: null,
|
|
77
|
+
ready: false,
|
|
78
|
+
connecting: false,
|
|
79
|
+
};
|
|
80
|
+
// Cleanup on process exit
|
|
81
|
+
const cleanup = () => {
|
|
82
|
+
if (bridge.socket) {
|
|
83
|
+
try {
|
|
84
|
+
bridge.socket.destroy();
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Ignore
|
|
88
|
+
}
|
|
89
|
+
bridge.socket = null;
|
|
90
|
+
}
|
|
91
|
+
if (bridge.process) {
|
|
92
|
+
try {
|
|
93
|
+
bridge.process.kill('SIGTERM');
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Ignore
|
|
97
|
+
}
|
|
98
|
+
bridge.process = null;
|
|
99
|
+
}
|
|
100
|
+
// Clean up socket file if it exists
|
|
101
|
+
if (existsSync(mlxConfig.socketPath)) {
|
|
102
|
+
try {
|
|
103
|
+
unlinkSync(mlxConfig.socketPath);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Ignore
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
bridge.ready = false;
|
|
110
|
+
};
|
|
111
|
+
// Register global cleanup
|
|
112
|
+
globalCleanup = cleanup;
|
|
113
|
+
// Also cleanup on process signals
|
|
114
|
+
process.on('SIGTERM', cleanup);
|
|
115
|
+
process.on('SIGINT', cleanup);
|
|
116
|
+
// Cleanup on beforeExit to ensure it runs before process.exit
|
|
117
|
+
process.on('beforeExit', cleanup);
|
|
118
|
+
/**
|
|
119
|
+
* Start the Python bridge process.
|
|
120
|
+
*/
|
|
121
|
+
const startBridge = async () => {
|
|
122
|
+
if (bridge.process && bridge.ready) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
debugLog('Starting MLX bridge...');
|
|
127
|
+
const pythonPath = getPythonPath();
|
|
128
|
+
debugLog(`Using Python: ${pythonPath}`);
|
|
129
|
+
bridge.process = spawn(pythonPath, [mlxConfig.bridgeScript], {
|
|
130
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
131
|
+
env: {
|
|
132
|
+
...process.env,
|
|
133
|
+
ESCRIBANO_VLM_MODEL: mlxConfig.model,
|
|
134
|
+
ESCRIBANO_VLM_BATCH_SIZE: String(mlxConfig.batchSize),
|
|
135
|
+
ESCRIBANO_VLM_MAX_TOKENS: String(mlxConfig.maxTokens),
|
|
136
|
+
ESCRIBANO_MLX_SOCKET_PATH: mlxConfig.socketPath,
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
if (!bridge.process.stdout || !bridge.process.stderr) {
|
|
140
|
+
reject(new Error('Failed to create bridge process streams'));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
// Handle stdout (ready signal is JSON on first line)
|
|
144
|
+
let readyReceived = false;
|
|
145
|
+
bridge.process.stdout.on('data', (data) => {
|
|
146
|
+
const lines = data.toString().trim().split('\n');
|
|
147
|
+
for (const line of lines) {
|
|
148
|
+
if (!readyReceived && line.startsWith('{')) {
|
|
149
|
+
try {
|
|
150
|
+
const msg = JSON.parse(line);
|
|
151
|
+
if (msg.status === 'ready') {
|
|
152
|
+
readyReceived = true;
|
|
153
|
+
bridge.ready = true;
|
|
154
|
+
debugLog(`Bridge ready: ${msg.model}`);
|
|
155
|
+
resolve();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// Not JSON, ignore
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
// Handle stderr (logs from Python)
|
|
165
|
+
bridge.process.stderr.on('data', (data) => {
|
|
166
|
+
const text = data.toString().trim();
|
|
167
|
+
if (text) {
|
|
168
|
+
console.log(text);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
// Handle process exit
|
|
172
|
+
bridge.process.on('exit', (code, signal) => {
|
|
173
|
+
debugLog(`Bridge exited: code=${code} signal=${signal}`);
|
|
174
|
+
bridge.process = null;
|
|
175
|
+
bridge.ready = false;
|
|
176
|
+
if (!readyReceived) {
|
|
177
|
+
reject(new Error(`Bridge failed to start: exit code ${code}`));
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
bridge.process.on('error', (err) => {
|
|
181
|
+
debugLog(`Bridge error: ${err.message}`);
|
|
182
|
+
if (!readyReceived) {
|
|
183
|
+
reject(new Error(`Failed to start bridge: ${err.message}`));
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
// Timeout for ready signal
|
|
187
|
+
setTimeout(() => {
|
|
188
|
+
if (!readyReceived) {
|
|
189
|
+
reject(new Error(`Bridge startup timeout (${mlxConfig.startupTimeout / 1000}s)`));
|
|
190
|
+
}
|
|
191
|
+
}, mlxConfig.startupTimeout);
|
|
192
|
+
});
|
|
193
|
+
};
|
|
194
|
+
/**
|
|
195
|
+
* Connect to the Unix socket.
|
|
196
|
+
*/
|
|
197
|
+
const connect = () => {
|
|
198
|
+
return new Promise((resolve, reject) => {
|
|
199
|
+
if (bridge.socket && !bridge.socket.destroyed) {
|
|
200
|
+
resolve(bridge.socket);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
debugLog(`Connecting to socket: ${mlxConfig.socketPath}`);
|
|
204
|
+
const client = createConnection(mlxConfig.socketPath);
|
|
205
|
+
client.on('connect', () => {
|
|
206
|
+
debugLog('Socket connected');
|
|
207
|
+
bridge.socket = client;
|
|
208
|
+
resolve(client);
|
|
209
|
+
});
|
|
210
|
+
client.on('error', (err) => {
|
|
211
|
+
debugLog(`Socket error: ${err.message}`);
|
|
212
|
+
bridge.socket = null;
|
|
213
|
+
reject(new Error(`Socket connection failed: ${err.message}`));
|
|
214
|
+
});
|
|
215
|
+
client.on('close', () => {
|
|
216
|
+
debugLog('Socket closed');
|
|
217
|
+
bridge.socket = null;
|
|
218
|
+
});
|
|
219
|
+
// Timeout
|
|
220
|
+
setTimeout(() => {
|
|
221
|
+
if (!bridge.socket) {
|
|
222
|
+
client.destroy();
|
|
223
|
+
reject(new Error('Socket connection timeout'));
|
|
224
|
+
}
|
|
225
|
+
}, 5000);
|
|
226
|
+
});
|
|
227
|
+
};
|
|
228
|
+
/**
|
|
229
|
+
* Send request and receive streaming NDJSON responses.
|
|
230
|
+
*/
|
|
231
|
+
const sendRequest = async (request, onBatch) => {
|
|
232
|
+
// Ensure bridge is running
|
|
233
|
+
if (!bridge.ready) {
|
|
234
|
+
await startBridge();
|
|
235
|
+
}
|
|
236
|
+
// Connect to socket
|
|
237
|
+
const socket = await connect();
|
|
238
|
+
return new Promise((resolve, reject) => {
|
|
239
|
+
const responses = [];
|
|
240
|
+
let buffer = '';
|
|
241
|
+
const onData = (chunk) => {
|
|
242
|
+
buffer += chunk.toString();
|
|
243
|
+
while (buffer.includes('\n')) {
|
|
244
|
+
const newlineIndex = buffer.indexOf('\n');
|
|
245
|
+
const line = buffer.slice(0, newlineIndex);
|
|
246
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
247
|
+
if (!line.trim())
|
|
248
|
+
continue;
|
|
249
|
+
try {
|
|
250
|
+
const response = JSON.parse(line);
|
|
251
|
+
if ('error' in response && response.error) {
|
|
252
|
+
// Error response
|
|
253
|
+
reject(new Error(response.error));
|
|
254
|
+
socket.off('data', onData);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if ('done' in response && response.done) {
|
|
258
|
+
// Final response
|
|
259
|
+
socket.off('data', onData);
|
|
260
|
+
resolve(responses);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
// Batch response
|
|
264
|
+
responses.push(response);
|
|
265
|
+
if (onBatch && 'progress' in response) {
|
|
266
|
+
const resp = response;
|
|
267
|
+
onBatch(response, resp.progress);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
debugLog(`Failed to parse response: ${line}`);
|
|
272
|
+
// Continue processing, might be partial
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
socket.on('data', onData);
|
|
277
|
+
socket.on('error', (err) => {
|
|
278
|
+
socket.off('data', onData);
|
|
279
|
+
reject(new Error(`Socket error: ${err.message}`));
|
|
280
|
+
});
|
|
281
|
+
// Send request
|
|
282
|
+
const requestJson = `${JSON.stringify(request)}\n`;
|
|
283
|
+
debugLog(`Sending request: id=${request.id} method=${request.method}`);
|
|
284
|
+
socket.write(requestJson);
|
|
285
|
+
});
|
|
286
|
+
};
|
|
287
|
+
// Return IntelligenceService implementation
|
|
288
|
+
return {
|
|
289
|
+
/**
|
|
290
|
+
* Classify transcript - NOT IMPLEMENTED for MLX backend.
|
|
291
|
+
*/
|
|
292
|
+
async classify(_transcript, _visualLogs) {
|
|
293
|
+
throw new Error('MLX adapter does not support classify(). Use Ollama backend for this operation.');
|
|
294
|
+
},
|
|
295
|
+
/**
|
|
296
|
+
* Classify segment - NOT IMPLEMENTED for MLX backend.
|
|
297
|
+
*/
|
|
298
|
+
async classifySegment(_segment, _transcript) {
|
|
299
|
+
throw new Error('MLX adapter does not support classifySegment(). Use Ollama backend for this operation.');
|
|
300
|
+
},
|
|
301
|
+
/**
|
|
302
|
+
* Extract metadata - NOT IMPLEMENTED for MLX backend.
|
|
303
|
+
*/
|
|
304
|
+
async extractMetadata(_transcript, _classification, _visualLogs) {
|
|
305
|
+
throw new Error('MLX adapter does not support extractMetadata(). Use Ollama backend for this operation.');
|
|
306
|
+
},
|
|
307
|
+
/**
|
|
308
|
+
* Generate artifact - NOT IMPLEMENTED for MLX backend.
|
|
309
|
+
*/
|
|
310
|
+
async generate(_artifactType, _context) {
|
|
311
|
+
throw new Error('MLX adapter does not support generate(). Use Ollama backend for this operation.');
|
|
312
|
+
},
|
|
313
|
+
/**
|
|
314
|
+
* Describe images using MLX-VLM with interleaved batching.
|
|
315
|
+
*
|
|
316
|
+
* This is the primary method for VLM frame processing.
|
|
317
|
+
*/
|
|
318
|
+
async describeImages(images, options = {}) {
|
|
319
|
+
const total = images.length;
|
|
320
|
+
if (total === 0) {
|
|
321
|
+
debugLog('No images to process');
|
|
322
|
+
return [];
|
|
323
|
+
}
|
|
324
|
+
console.log(`[VLM] Processing ${total} images with MLX...`);
|
|
325
|
+
console.log(`[VLM] Model: ${mlxConfig.model}, batch size: ${mlxConfig.batchSize}`);
|
|
326
|
+
const startTime = Date.now();
|
|
327
|
+
const allResults = [];
|
|
328
|
+
const requestId = Date.now();
|
|
329
|
+
const handleBatch = (response, progress) => {
|
|
330
|
+
if (response.results) {
|
|
331
|
+
for (const result of response.results) {
|
|
332
|
+
allResults.push(result);
|
|
333
|
+
// Fire callback for each frame
|
|
334
|
+
if (options.onImageProcessed) {
|
|
335
|
+
options.onImageProcessed(result, progress);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// Log progress every 10 frames
|
|
339
|
+
if (progress.current % 10 === 0 ||
|
|
340
|
+
progress.current === progress.total) {
|
|
341
|
+
console.log(`[VLM] [${progress.current}/${progress.total}] frames processed`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
try {
|
|
346
|
+
await sendRequest({
|
|
347
|
+
id: requestId,
|
|
348
|
+
method: 'describe_images',
|
|
349
|
+
params: {
|
|
350
|
+
images: images.map((img, idx) => ({
|
|
351
|
+
index: idx,
|
|
352
|
+
imagePath: img.imagePath,
|
|
353
|
+
timestamp: img.timestamp,
|
|
354
|
+
})),
|
|
355
|
+
batchSize: mlxConfig.batchSize,
|
|
356
|
+
maxTokens: mlxConfig.maxTokens,
|
|
357
|
+
},
|
|
358
|
+
}, handleBatch);
|
|
359
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
360
|
+
const fps = total / ((Date.now() - startTime) / 1000);
|
|
361
|
+
console.log(`\n[VLM] Complete: ${allResults.length}/${total} frames in ${duration}s (${fps.toFixed(2)} fps)`);
|
|
362
|
+
return allResults;
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
const message = error.message;
|
|
366
|
+
console.error(`[VLM] ERROR: ${message}`);
|
|
367
|
+
throw new Error(`MLX VLM processing failed: ${message}`);
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
/**
|
|
371
|
+
* Embed text - NOT IMPLEMENTED for MLX backend.
|
|
372
|
+
*/
|
|
373
|
+
async embedText(_texts, _options) {
|
|
374
|
+
throw new Error('MLX adapter does not support embedText(). Use Ollama backend for this operation.');
|
|
375
|
+
},
|
|
376
|
+
/**
|
|
377
|
+
* Extract topics - NOT IMPLEMENTED for MLX backend.
|
|
378
|
+
*/
|
|
379
|
+
async extractTopics(_observations) {
|
|
380
|
+
throw new Error('MLX adapter does not support extractTopics(). Use Ollama backend for this operation.');
|
|
381
|
+
},
|
|
382
|
+
/**
|
|
383
|
+
* Generate text - NOT IMPLEMENTED for MLX backend.
|
|
384
|
+
*/
|
|
385
|
+
async generateText(_prompt, _options) {
|
|
386
|
+
throw new Error('MLX adapter does not support generateText(). Use Ollama backend for this operation.');
|
|
387
|
+
},
|
|
388
|
+
getResourceName() {
|
|
389
|
+
return 'mlx-python';
|
|
390
|
+
},
|
|
391
|
+
getPid() {
|
|
392
|
+
return bridge.process?.pid ?? null;
|
|
393
|
+
},
|
|
394
|
+
};
|
|
395
|
+
}
|