@qvac/sdk 0.15.0 → 0.16.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 (102) hide show
  1. package/NOTICE +31 -45
  2. package/dist/client/api/ocr.d.ts +9 -0
  3. package/dist/client/api/ocr.js +9 -0
  4. package/dist/client/api/translate.js +6 -17
  5. package/dist/client/api/video.d.ts +7 -4
  6. package/dist/client/api/video.js +7 -4
  7. package/dist/client/api/vla.d.ts +14 -10
  8. package/dist/client/api/vla.js +14 -10
  9. package/dist/client/rpc/rpc-error.js +7 -1
  10. package/dist/examples/diffusion-ideogram.d.ts +2 -0
  11. package/dist/examples/diffusion-ideogram.js +108 -0
  12. package/dist/examples/diffusion-img2vid-ltx.d.ts +2 -0
  13. package/dist/examples/diffusion-img2vid-ltx.js +98 -0
  14. package/dist/examples/diffusion-txt2vid-ltx.d.ts +2 -0
  15. package/dist/examples/diffusion-txt2vid-ltx.js +92 -0
  16. package/dist/examples/logging-streaming.js +5 -1
  17. package/dist/examples/transcription/parakeet-tdt-filesystem-chunked.d.ts +2 -0
  18. package/dist/examples/transcription/parakeet-tdt-filesystem-chunked.js +203 -0
  19. package/dist/examples/vla-groot.d.ts +2 -0
  20. package/dist/examples/vla-groot.js +118 -0
  21. package/dist/models/registry/models.d.ts +5711 -9637
  22. package/dist/models/registry/models.js +2747 -6886
  23. package/dist/models/update-models/naming.js +5 -1
  24. package/dist/schemas/batch-completion-stream.d.ts +4 -4
  25. package/dist/schemas/common.d.ts +385 -4
  26. package/dist/schemas/common.js +3 -1
  27. package/dist/schemas/completion-stream.d.ts +186 -2
  28. package/dist/schemas/completion-stream.js +67 -0
  29. package/dist/schemas/constants-registry.d.ts +67 -0
  30. package/dist/schemas/constants-registry.js +51 -0
  31. package/dist/schemas/delete-cache.js +7 -3
  32. package/dist/schemas/engine-addon-map.d.ts +1 -0
  33. package/dist/schemas/engine-addon-map.js +1 -1
  34. package/dist/schemas/finetune.js +6 -3
  35. package/dist/schemas/get-loaded-model-info.d.ts +16 -0
  36. package/dist/schemas/get-loaded-model-info.js +14 -6
  37. package/dist/schemas/llamacpp-config.d.ts +24 -4
  38. package/dist/schemas/llamacpp-config.js +1 -6
  39. package/dist/schemas/load-model.d.ts +1380 -60
  40. package/dist/schemas/load-model.js +6 -3
  41. package/dist/schemas/plugin.d.ts +4 -4
  42. package/dist/schemas/plugin.js +4 -4
  43. package/dist/schemas/reload-config.js +3 -1
  44. package/dist/schemas/sdcpp-config.d.ts +239 -0
  45. package/dist/schemas/sdcpp-config.js +116 -10
  46. package/dist/schemas/sdk-config.d.ts +790 -40
  47. package/dist/schemas/sdk-errors-server.js +3 -1
  48. package/dist/schemas/text-to-speech.d.ts +14 -0
  49. package/dist/schemas/text-to-speech.js +10 -4
  50. package/dist/schemas/tools.d.ts +1 -1
  51. package/dist/schemas/tools.js +2 -1
  52. package/dist/schemas/translate.js +12 -6
  53. package/dist/schemas/vla.d.ts +10 -0
  54. package/dist/schemas/vla.js +31 -11
  55. package/dist/scripts/contract/build-contract.d.ts +38 -0
  56. package/dist/scripts/contract/build-contract.js +620 -0
  57. package/dist/scripts/contract/build-error-codes.d.ts +17 -0
  58. package/dist/scripts/contract/build-error-codes.js +17 -0
  59. package/dist/scripts/contract/build-model-type-maps.d.ts +22 -0
  60. package/dist/scripts/contract/build-model-type-maps.js +16 -0
  61. package/dist/scripts/contract/build-models-registry.d.ts +17 -0
  62. package/dist/scripts/contract/build-models-registry.js +31 -0
  63. package/dist/scripts/contract/plugin-contract.d.ts +23 -0
  64. package/dist/scripts/contract/plugin-contract.js +28 -0
  65. package/dist/scripts/export-contract.d.ts +2 -0
  66. package/dist/scripts/export-contract.js +27 -0
  67. package/dist/scripts/export-plugin-contract.d.ts +2 -0
  68. package/dist/scripts/export-plugin-contract.js +18 -0
  69. package/dist/server/bare/ops/translate.js +16 -3
  70. package/dist/server/bare/plugins/bci-whispercpp-transcription/plugin.d.ts +2 -1
  71. package/dist/server/bare/plugins/ggml-ocr/plugin.js +2 -23
  72. package/dist/server/bare/plugins/ggml-ocr/resolve-config.d.ts +6 -0
  73. package/dist/server/bare/plugins/ggml-ocr/resolve-config.js +73 -0
  74. package/dist/server/bare/plugins/ggml-vla/plugin.d.ts +5 -0
  75. package/dist/server/bare/plugins/ggml-vla/plugin.js +1 -1
  76. package/dist/server/bare/plugins/llamacpp-completion/ops/completion-stream.js +11 -22
  77. package/dist/server/bare/plugins/llamacpp-completion/ops/kv-cache-state.d.ts +8 -0
  78. package/dist/server/bare/plugins/llamacpp-completion/ops/kv-cache-state.js +8 -0
  79. package/dist/server/bare/plugins/llamacpp-completion/plugin.d.ts +8 -3
  80. package/dist/server/bare/plugins/llamacpp-embedding/plugin.d.ts +6 -1
  81. package/dist/server/bare/plugins/sdcpp-generation/ops/video.d.ts +3 -1
  82. package/dist/server/bare/plugins/sdcpp-generation/ops/video.js +31 -2
  83. package/dist/server/bare/plugins/sdcpp-generation/plugin.d.ts +129 -1
  84. package/dist/server/bare/plugins/sdcpp-generation/plugin.js +94 -22
  85. package/dist/server/bare/plugins/tts-ggml/ops/text-to-speech.js +2 -16
  86. package/dist/server/bare/plugins/tts-ggml/plugin.d.ts +6 -0
  87. package/dist/server/bare/plugins/tts-ggml/plugin.js +3 -1
  88. package/dist/server/bare/types/addon-responses.d.ts +2 -0
  89. package/dist/server/bare/utils/tts-stats.d.ts +8 -4
  90. package/dist/server/bare/utils/tts-stats.js +6 -0
  91. package/dist/server/rpc/create-server.js +23 -1
  92. package/dist/server/rpc/handler-registry.js +5 -0
  93. package/dist/server/rpc/handlers/completion-orchestrate.d.ts +42 -0
  94. package/dist/server/rpc/handlers/completion-orchestrate.js +190 -0
  95. package/dist/server/rpc/handlers/get-loaded-model-info.js +8 -2
  96. package/dist/server/rpc/handlers/index.d.ts +2 -0
  97. package/dist/server/rpc/handlers/index.js +2 -0
  98. package/dist/server/rpc/method-shapes.d.ts +51 -0
  99. package/dist/server/rpc/method-shapes.js +48 -0
  100. package/dist/server/rpc/profiling/operation-metrics.js +20 -0
  101. package/dist/server/utils/cache/auto-history.js +13 -9
  102. package/package.json +16 -13
@@ -0,0 +1,92 @@
1
+ import { loadModel, unloadModel, video } from '@qvac/sdk';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ // Text-to-video (+ synchronized audio) with LTX-2. Unlike Wan, LTX-2 uses a
5
+ // Gemma text encoder (llmModelSrc), a video VAE (vaeModelSrc), a set of
6
+ // text-embedding connectors (embeddingsConnectorsModelSrc — this is what
7
+ // selects the LTX-2 layout), and an optional audio VAE (audioVaeModelSrc)
8
+ // that produces a 48 kHz audio track muxed into the output AVI.
9
+ //
10
+ // This example needs powerful hardware: LTX-2.3 is a ~21B model, budget at
11
+ // least 20+ GB of video / unified memory.
12
+ //
13
+ // No LTX-2 model constants ship in the SDK registry, so the model sources
14
+ // default to public HuggingFace URLs (the SDK downloads http(s) sources
15
+ // directly). Pass your own paths / URLs via argv to override.
16
+ const HF = 'https://huggingface.co';
17
+ const diffusionModelSrc = process.argv[2] ||
18
+ `${HF}/QuantStack/LTX-2.3-GGUF/resolve/main/LTX-2.3-distilled-1.1/LTX-2.3-22B-distilled-1.1-Q5_K_M.gguf`;
19
+ const llmModelSrc = process.argv[3] || `${HF}/unsloth/gemma-3-12b-it-GGUF/resolve/main/gemma-3-12b-it-UD-Q4_K_XL.gguf`;
20
+ const vaeModelSrc = process.argv[4] ||
21
+ `${HF}/unsloth/LTX-2.3-GGUF/resolve/main/vae/ltx-2.3-22b-distilled_video_vae.safetensors`;
22
+ const audioVaeModelSrc = process.argv[5] ||
23
+ `${HF}/unsloth/LTX-2.3-GGUF/resolve/main/vae/ltx-2.3-22b-distilled_audio_vae.safetensors`;
24
+ const embeddingsConnectorsModelSrc = process.argv[6] ||
25
+ `${HF}/unsloth/LTX-2.3-GGUF/resolve/main/text_encoders/ltx-2.3-22b-distilled_embeddings_connectors.safetensors`;
26
+ const prompt = process.argv[7] || 'a claymation cat playing jazz on a piano';
27
+ const outputDir = process.argv[8] || '.';
28
+ try {
29
+ console.log('▸ Loading LTX-2 model (diffusion + Gemma + video VAE + audio VAE + connectors)...');
30
+ const modelId = await loadModel({
31
+ modelSrc: diffusionModelSrc,
32
+ modelType: 'sdcpp-generation',
33
+ modelConfig: {
34
+ mode: 'video',
35
+ device: 'gpu',
36
+ threads: 4,
37
+ // Supplying embeddingsConnectorsModelSrc selects the LTX-2 layout.
38
+ llmModelSrc,
39
+ vaeModelSrc,
40
+ audioVaeModelSrc,
41
+ embeddingsConnectorsModelSrc,
42
+ diffusion_fa: true,
43
+ vae_tiling: true
44
+ },
45
+ onProgress: (p) => {
46
+ const mb = (n) => (n / 1e6).toFixed(1);
47
+ const line = `▸ Downloading ${p.percentage.toFixed(0)}% (${mb(p.downloaded)}/${mb(p.total)} MB)`;
48
+ process.stderr.write(process.stderr.isTTY ? `\r${line}` : `${line}\n`);
49
+ if (p.percentage >= 100)
50
+ process.stderr.write('\n');
51
+ }
52
+ });
53
+ console.log(`▸ Model loaded: ${modelId}`);
54
+ console.log(`\n▸ Generating video for: "${prompt}"`);
55
+ const { progressStream, outputs, stats } = video({
56
+ modelId,
57
+ mode: 'txt2vid',
58
+ prompt,
59
+ negative_prompt: 'blurry, low quality, static, jittery, watermark, distorted audio',
60
+ // LTX-2 requires width/height as multiples of 32.
61
+ width: 512,
62
+ height: 320,
63
+ // LTX-2 requires (8*k + 1) frames, max 257. At 24 fps: 25≈1s, 121≈5s, 241≈10s.
64
+ video_frames: 121,
65
+ fps: 24,
66
+ // Distilled variants run in 4-8 steps with cfg ~1.0; the full --dev model
67
+ // wants steps >= 20 and cfg ~7.0.
68
+ steps: 10,
69
+ cfg_scale: 1.0,
70
+ seed: 42,
71
+ // Tile the video VAE decode along the time axis to cap peak VRAM on long
72
+ // / HD clips (LTX-2 only).
73
+ temporal_tiling: true
74
+ });
75
+ for await (const { step, totalSteps } of progressStream) {
76
+ console.log(`▸ step ${step}/${totalSteps}`);
77
+ }
78
+ const buffers = await outputs;
79
+ for (let i = 0; i < buffers.length; i++) {
80
+ const outputPath = path.join(outputDir, `ltx_t2v_${i}.avi`);
81
+ fs.writeFileSync(outputPath, buffers[i]);
82
+ console.log(`▸ Saved ${outputPath} (play in VLC to hear the muxed audio track)`);
83
+ }
84
+ console.log('\n▸ Stats:', await stats);
85
+ await unloadModel({ modelId, clearStorage: false });
86
+ console.log('▸ Done.');
87
+ process.exit(0);
88
+ }
89
+ catch (error) {
90
+ console.error('✖', error);
91
+ process.exit(1);
92
+ }
@@ -46,7 +46,11 @@ try {
46
46
  const result = completion({
47
47
  modelId: llmModelId,
48
48
  history: messages,
49
- stream: true
49
+ stream: true,
50
+ // Bound the generation: at temp 0.7 a 1B model can ignore "to 5" and ramble
51
+ // past ctx_size (2048) into a context overflow. This demo only needs enough
52
+ // tokens to show logs streaming.
53
+ generationParams: { predict: 512 }
50
54
  });
51
55
  const { embedding } = await embed({
52
56
  modelId: embedModelId,
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=parakeet-tdt-filesystem-chunked.d.ts.map
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Parakeet TDT transcription for long audio files.
3
+ *
4
+ * Usage:
5
+ * bun run examples/transcription/parakeet-tdt-filesystem-chunked.ts <audio-file> [parakeet-tdt-gguf]
6
+ *
7
+ * FFmpeg first detects silence, then this example selects boundaries near
8
+ * 45 seconds and transcribes one segment at a time. Segments never exceed
9
+ * 60 seconds, which keeps audio and inference memory bounded. Any audio
10
+ * format supported by FFmpeg can be used.
11
+ */
12
+ import { loadModel, unloadModel, transcribe, PARAKEET_TDT_0_6B_V3_Q8_0 } from '@qvac/sdk';
13
+ import { spawn } from 'child_process';
14
+ const SAMPLE_RATE = 16000;
15
+ const SILENCE_NOISE_DB = -35;
16
+ const SILENCE_DURATION_S = 0.5;
17
+ const MIN_SEGMENT_S = 10;
18
+ const TARGET_SEGMENT_S = 45;
19
+ const MAX_SEGMENT_S = 60;
20
+ function runCommand(command, args) {
21
+ return new Promise((resolve, reject) => {
22
+ const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'] });
23
+ const stdout = [];
24
+ const stderr = [];
25
+ child.stdout.on('data', (chunk) => stdout.push(chunk));
26
+ child.stderr.on('data', (chunk) => stderr.push(chunk));
27
+ child.on('error', reject);
28
+ child.on('close', (code) => {
29
+ const stderrText = Buffer.concat(stderr).toString();
30
+ if (code !== 0) {
31
+ reject(new Error(`${command} exited with code ${code}: ${stderrText.trim()}`));
32
+ return;
33
+ }
34
+ resolve({ stdout: Buffer.concat(stdout), stderr: stderrText });
35
+ });
36
+ });
37
+ }
38
+ async function getAudioDuration(audioPath) {
39
+ const result = await runCommand('ffprobe', [
40
+ '-v',
41
+ 'error',
42
+ '-show_entries',
43
+ 'format=duration',
44
+ '-of',
45
+ 'default=noprint_wrappers=1:nokey=1',
46
+ audioPath
47
+ ]);
48
+ const duration = Number.parseFloat(result.stdout.toString().trim());
49
+ if (!Number.isFinite(duration) || duration <= 0) {
50
+ throw new Error(`Could not determine audio duration for ${audioPath}`);
51
+ }
52
+ return duration;
53
+ }
54
+ async function detectSilences(audioPath, duration) {
55
+ const result = await runCommand('ffmpeg', [
56
+ '-hide_banner',
57
+ '-nostats',
58
+ '-i',
59
+ audioPath,
60
+ '-vn',
61
+ '-af',
62
+ `silencedetect=noise=${SILENCE_NOISE_DB}dB:d=${SILENCE_DURATION_S}`,
63
+ '-f',
64
+ 'null',
65
+ '-'
66
+ ]);
67
+ const intervals = [];
68
+ let currentStart = null;
69
+ for (const line of result.stderr.split('\n')) {
70
+ const startMatch = line.match(/silence_start:\s*([0-9.]+)/);
71
+ if (startMatch?.[1]) {
72
+ const start = Number.parseFloat(startMatch[1]);
73
+ if (Number.isFinite(start))
74
+ currentStart = start;
75
+ }
76
+ const endMatch = line.match(/silence_end:\s*([0-9.]+)/);
77
+ if (endMatch?.[1] && currentStart !== null) {
78
+ const end = Number.parseFloat(endMatch[1]);
79
+ if (Number.isFinite(end) && end > currentStart) {
80
+ intervals.push({ start: currentStart, end });
81
+ }
82
+ currentStart = null;
83
+ }
84
+ }
85
+ if (currentStart !== null && currentStart < duration) {
86
+ intervals.push({ start: currentStart, end: duration });
87
+ }
88
+ return intervals;
89
+ }
90
+ function createAudioSegments(duration, silences) {
91
+ const silenceMidpoints = silences.map(({ start, end }) => start + (end - start) / 2);
92
+ const segments = [];
93
+ let start = 0;
94
+ while (duration - start > MAX_SEGMENT_S) {
95
+ const target = start + TARGET_SEGMENT_S;
96
+ const minimumCut = start + MIN_SEGMENT_S;
97
+ const maximumCut = start + MAX_SEGMENT_S;
98
+ const candidates = silenceMidpoints.filter((candidate) => candidate >= minimumCut && candidate <= maximumCut);
99
+ const firstCandidate = candidates[0];
100
+ const cut = firstCandidate === undefined
101
+ ? Math.min(target, duration - MIN_SEGMENT_S)
102
+ : candidates.slice(1).reduce((best, candidate) => {
103
+ return Math.abs(candidate - target) < Math.abs(best - target) ? candidate : best;
104
+ }, firstCandidate);
105
+ segments.push({ start, end: cut });
106
+ start = cut;
107
+ }
108
+ if (duration > start) {
109
+ segments.push({ start, end: duration });
110
+ }
111
+ return segments;
112
+ }
113
+ async function decodeSegment(audioPath, segment) {
114
+ const result = await runCommand('ffmpeg', [
115
+ '-hide_banner',
116
+ '-loglevel',
117
+ 'error',
118
+ '-ss',
119
+ segment.start.toFixed(3),
120
+ '-i',
121
+ audioPath,
122
+ '-t',
123
+ (segment.end - segment.start).toFixed(3),
124
+ '-vn',
125
+ '-ar',
126
+ String(SAMPLE_RATE),
127
+ '-ac',
128
+ '1',
129
+ '-sample_fmt',
130
+ 's16',
131
+ '-f',
132
+ 's16le',
133
+ 'pipe:1'
134
+ ]);
135
+ return result.stdout;
136
+ }
137
+ function formatTimestamp(seconds) {
138
+ const totalTenths = Math.round(seconds * 10);
139
+ const hours = Math.floor(totalTenths / 36000);
140
+ const minutes = Math.floor((totalTenths % 36000) / 600);
141
+ const remainingSeconds = ((totalTenths % 600) / 10).toFixed(1).padStart(4, '0');
142
+ return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${remainingSeconds}`;
143
+ }
144
+ const args = process.argv.slice(2);
145
+ if (!args[0]) {
146
+ console.error('Usage: bun run examples/transcription/parakeet-tdt-filesystem-chunked.ts ' +
147
+ '<audio-file> [parakeet-tdt-gguf]');
148
+ process.exit(1);
149
+ }
150
+ const audioFilePath = args[0];
151
+ const parakeetModelSrc = args[1] ?? PARAKEET_TDT_0_6B_V3_Q8_0;
152
+ let modelId = null;
153
+ try {
154
+ await runCommand('ffmpeg', ['-version']);
155
+ await runCommand('ffprobe', ['-version']);
156
+ console.log('▸ Detecting silence and planning bounded segments...');
157
+ const duration = await getAudioDuration(audioFilePath);
158
+ const silences = await detectSilences(audioFilePath, duration);
159
+ const segments = createAudioSegments(duration, silences);
160
+ console.log(`▸ Audio duration: ${formatTimestamp(duration)}; detected ${silences.length} silence interval(s); planned ${segments.length} segment(s)`);
161
+ console.log('▸ Loading Parakeet TDT model...');
162
+ modelId = await loadModel({
163
+ modelSrc: parakeetModelSrc,
164
+ modelType: 'parakeet-transcription',
165
+ onProgress: (p) => {
166
+ const mb = (n) => (n / 1e6).toFixed(1);
167
+ const line = `▸ Downloading ${p.percentage.toFixed(0)}% (${mb(p.downloaded)}/${mb(p.total)} MB)`;
168
+ process.stderr.write(process.stderr.isTTY ? `\r${line}` : `${line}\n`);
169
+ if (p.percentage >= 100)
170
+ process.stderr.write('\n');
171
+ }
172
+ });
173
+ console.log(`▸ Model loaded: ${modelId}`);
174
+ const transcript = [];
175
+ for (const [index, segment] of segments.entries()) {
176
+ console.log(`▸ Segment ${index + 1}/${segments.length}: ${formatTimestamp(segment.start)} - ${formatTimestamp(segment.end)}`);
177
+ const audioChunk = await decodeSegment(audioFilePath, segment);
178
+ const text = await transcribe({ modelId, audioChunk });
179
+ const normalizedText = text.trim();
180
+ if (normalizedText && !normalizedText.includes('[No speech detected]')) {
181
+ transcript.push(normalizedText);
182
+ console.log(normalizedText);
183
+ }
184
+ }
185
+ console.log('\n▸ Complete transcript');
186
+ console.log(transcript.join(' '));
187
+ await unloadModel({ modelId });
188
+ modelId = null;
189
+ console.log('▸ Done');
190
+ process.exit(0);
191
+ }
192
+ catch (error) {
193
+ console.error('✖', error);
194
+ if (modelId) {
195
+ try {
196
+ await unloadModel({ modelId });
197
+ }
198
+ catch (unloadError) {
199
+ console.error('✖ Failed to unload model:', unloadError);
200
+ }
201
+ }
202
+ process.exit(1);
203
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=vla-groot.d.ts.map
@@ -0,0 +1,118 @@
1
+ /**
2
+ * NVIDIA GR00T N1.7-3B (vision-language-action) example using the QVAC SDK.
3
+ *
4
+ * Loads the GR00T-N1.7-3B LIBERO GGUF model, runs a single inference pass with
5
+ * synthetic inputs, and prints the produced action chunk + per-stage timings.
6
+ *
7
+ * GR00T differs from SmolVLA / π₀.₅ in the ways the SDK surfaces via
8
+ * `vlaHparams()`:
9
+ * - `imageInputMode: 'patches'` — each `images` entry is a pre-patchified
10
+ * buffer of `hparams.imagePatchElems` floats, NOT a `3·w·h` pixel plane.
11
+ * A real consumer runs the model's own vision patch-embed over each camera
12
+ * frame; here we feed synthetic patch buffers of the right length.
13
+ * - `stateInputMode: 'continuous'` — the robot state is projected by an
14
+ * in-model linear layer; pad it to `hparams.maxStateDim` with `vlaPadState`.
15
+ * - `noise` is REQUIRED. GR00T is a flow-matching model that does not sample
16
+ * its own prior; a missing `noise` is rejected as INVALID_INPUT.
17
+ *
18
+ * The prompt must place one contiguous run of `MERGED_TOKENS_PER_IMAGE`
19
+ * image-placeholder tokens per camera. A real consumer produces this by
20
+ * tokenising the instruction with the Qwen3-VL tokenizer, which inserts the
21
+ * image tokens; we lay it out by hand for the smoke.
22
+ *
23
+ * Usage:
24
+ * bun examples/vla-groot.ts [path-to-groot.gguf]
25
+ *
26
+ * By default the example pulls the registry-baked GR00T-LIBERO GGUF (~3.76 GB)
27
+ * on first run and caches it locally. Pass an absolute path on the command line
28
+ * to override and load a local GGUF instead.
29
+ */
30
+ import { close, loadModel, GROOT_Q8_VF16, unloadModel, vla, vlaHparams, vlaPadState } from '@qvac/sdk';
31
+ // LIBERO GR00T prompt layout (Qwen3-VL tokenizer). GR00T reports
32
+ // `hparams.tokenizerMaxLength === 0` — it does not surface a tokenizer length,
33
+ // so the prompt length is fixed by the model: 2 cameras × 64 merged image
34
+ // tokens (256 patches, 2×2 merge) = 128 image tokens, plus text, = 148 total.
35
+ // A real consumer gets these from the tokenizer; the smoke hard-codes them.
36
+ const IMAGE_TOKEN_ID = 151655;
37
+ const MERGED_TOKENS_PER_IMAGE = 64;
38
+ const PROMPT_LENGTH = 148;
39
+ const TEXT_TOKEN_ID = 1000;
40
+ const modelSrcOverride = process.argv[2];
41
+ const modelSrc = modelSrcOverride ?? GROOT_Q8_VF16;
42
+ try {
43
+ console.log('▸ Loading GR00T (N1.7-3B LIBERO) model...');
44
+ const modelId = await loadModel({
45
+ modelSrc,
46
+ modelType: 'ggml-vla',
47
+ modelConfig: { backend: 'cpu' },
48
+ onProgress: (p) => {
49
+ const mb = (n) => (n / 1e6).toFixed(1);
50
+ const line = `▸ Downloading ${p.percentage.toFixed(0)}% (${mb(p.downloaded)}/${mb(p.total)} MB)`;
51
+ process.stderr.write(process.stderr.isTTY ? `\r${line}` : `${line}\n`);
52
+ if (p.percentage >= 100)
53
+ process.stderr.write('\n');
54
+ }
55
+ });
56
+ if (typeof modelSrc !== 'string')
57
+ process.stderr.write('\n');
58
+ console.log(`▸ Model loaded: ${modelId}`);
59
+ const { hparams, backendName } = await vlaHparams({ modelId });
60
+ console.log(`▸ Backend: ${backendName ?? '(unknown)'}`);
61
+ console.log('▸ Hparams:', hparams);
62
+ const patchElems = hparams.imagePatchElems;
63
+ if (hparams.imageInputMode !== 'patches' || patchElems == null) {
64
+ throw new Error(`expected a patch-input model (imageInputMode 'patches' + imagePatchElems); ` +
65
+ `got imageInputMode=${hparams.imageInputMode}`);
66
+ }
67
+ const numCameras = hparams.numCameras ?? 2;
68
+ // Patch-input model: each camera is a pre-patchified buffer of exactly
69
+ // `imagePatchElems` floats. A real consumer fills these from the model's
70
+ // vision patch-embed over the camera frame; we use small synthetic values.
71
+ const images = Array.from({ length: numCameras }, () => new Float32Array(patchElems).fill(0.02));
72
+ // Continuous-state model: pad the robot state to `maxStateDim`.
73
+ const state = vlaPadState([0, 0, 0, 0, 0, 0], hparams.maxStateDim);
74
+ // GR00T requires the noise prior (flow-matching; it is not sampled in-model).
75
+ const noise = new Float32Array(hparams.chunkSize * hparams.maxActionDim);
76
+ // Prompt: one run of `MERGED_TOKENS_PER_IMAGE` image tokens per camera, each
77
+ // followed by a text separator, padded out to the fixed `PROMPT_LENGTH`.
78
+ const tokens = new Int32Array(PROMPT_LENGTH);
79
+ let w = 0;
80
+ for (let cam = 0; cam < numCameras; cam++) {
81
+ for (let k = 0; k < MERGED_TOKENS_PER_IMAGE && w < tokens.length; k++) {
82
+ tokens[w++] = IMAGE_TOKEN_ID;
83
+ }
84
+ if (w < tokens.length)
85
+ tokens[w++] = TEXT_TOKEN_ID + cam;
86
+ }
87
+ for (; w < tokens.length; w++)
88
+ tokens[w] = TEXT_TOKEN_ID + w;
89
+ const mask = new Uint8Array(PROMPT_LENGTH).fill(1);
90
+ console.log('▸ Running VLA inference...');
91
+ const { actions, actionDim, chunkSize, stats } = await vla({
92
+ modelId,
93
+ images,
94
+ // Patch inputs ignore imgWidth/imgHeight, but the request schema requires
95
+ // positive integers; pass the model's vision image size.
96
+ imgWidth: hparams.visionImageSize,
97
+ imgHeight: hparams.visionImageSize,
98
+ state,
99
+ tokens,
100
+ mask,
101
+ noise
102
+ });
103
+ console.log(`▸ Got ${chunkSize} action steps of dim ${actionDim}.`);
104
+ console.log(Array.from(actions.subarray(0, actionDim)));
105
+ if (stats) {
106
+ console.log(`▸ Timing: vision=${stats.vision_ms?.toFixed(0)}ms ` +
107
+ `ode=${stats.ode_ms?.toFixed(0)}ms ` +
108
+ `total=${stats.total_ms?.toFixed(0)}ms`);
109
+ }
110
+ await unloadModel({ modelId, clearStorage: false });
111
+ console.log('▸ Model unloaded.');
112
+ process.exit(0);
113
+ }
114
+ catch (error) {
115
+ console.error('✖', error);
116
+ await close();
117
+ process.exit(1);
118
+ }