@qvac/sdk 0.19.0 → 0.20.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 (79) hide show
  1. package/NOTICE +111 -82
  2. package/dist/examples/abot-world.js +3 -3
  3. package/dist/examples/advisory-model-fit.d.ts +59 -0
  4. package/dist/examples/advisory-model-fit.js +156 -0
  5. package/dist/examples/asr/parakeet-nemotron-filesystem.d.ts +2 -0
  6. package/dist/examples/asr/parakeet-nemotron-filesystem.js +170 -0
  7. package/dist/examples/assess-model-fit.js +6 -1
  8. package/dist/examples/audiogen/edit-music.d.ts +2 -0
  9. package/dist/examples/audiogen/edit-music.js +121 -0
  10. package/dist/examples/audiogen/understand-music.d.ts +2 -0
  11. package/dist/examples/audiogen/understand-music.js +73 -0
  12. package/dist/examples/diffusion-ic-lora-ltx.js +3 -3
  13. package/dist/examples/diffusion-img2vid.js +1 -1
  14. package/dist/examples/diffusion-txt2vid-minimax-h3.d.ts +2 -0
  15. package/dist/examples/diffusion-txt2vid-minimax-h3.js +64 -0
  16. package/dist/examples/diffusion-txt2vid.js +1 -1
  17. package/dist/examples/kv-cache-cleanup.js +5 -2
  18. package/dist/examples/model-fit-ui/server.js +33 -7
  19. package/dist/examples/rag/rag-turbovec.d.ts +2 -0
  20. package/dist/examples/rag/rag-turbovec.js +58 -0
  21. package/dist/examples/tts/audio8.js +5 -5
  22. package/dist/examples/tts/chatterbox-enhanced.js +4 -5
  23. package/dist/examples/tts/chatterbox-multilingual.js +5 -5
  24. package/dist/examples/tts/chatterbox.js +5 -5
  25. package/dist/examples/tts/cosyvoice3.js +5 -5
  26. package/dist/examples/tts/llm-to-tts-streaming.js +12 -2
  27. package/dist/examples/tts/parler.js +5 -5
  28. package/dist/examples/tts/supertonic-enhanced.js +4 -5
  29. package/dist/examples/tts/supertonic-multilingual.js +5 -5
  30. package/dist/examples/tts/supertonic.js +5 -5
  31. package/dist/scripts/contract/build-contract.d.ts +1 -1
  32. package/dist/scripts/contract/constants-registry.d.ts +31 -0
  33. package/dist/scripts/contract/constants-registry.js +13 -2
  34. package/dist/scripts/contract/method-shapes.d.ts +3 -0
  35. package/dist/scripts/contract/method-shapes.js +3 -0
  36. package/dist/src/client/api/assess-model-fit.js +2 -0
  37. package/dist/src/client/api/audio-edit.d.ts +36 -0
  38. package/dist/src/client/api/audio-edit.js +38 -0
  39. package/dist/src/client/api/audio-gen-result.d.ts +5 -1
  40. package/dist/src/client/api/audio-gen-result.js +105 -52
  41. package/dist/src/client/api/audio-gen.d.ts +38 -1
  42. package/dist/src/client/api/audio-gen.js +38 -1
  43. package/dist/src/client/api/audio-understand.d.ts +25 -0
  44. package/dist/src/client/api/audio-understand.js +27 -0
  45. package/dist/src/client/api/delete-cache.d.ts +7 -1
  46. package/dist/src/client/api/delete-cache.js +11 -1
  47. package/dist/src/client/api/index.d.ts +3 -0
  48. package/dist/src/client/api/index.js +3 -0
  49. package/dist/src/client/api/text-to-speech.d.ts +31 -4
  50. package/dist/src/client/api/text-to-speech.js +118 -21
  51. package/dist/src/client/api/vector-index.d.ts +143 -0
  52. package/dist/src/client/api/vector-index.js +168 -0
  53. package/dist/src/client/api/world.d.ts +2 -2
  54. package/dist/src/commands/bundle/manifest.d.ts +2 -0
  55. package/dist/src/commands/verify/addon-source.d.ts +6 -0
  56. package/dist/src/commands/verify/bundle-source.d.ts +2 -0
  57. package/dist/src/commands/verify/bundle-source.js +155 -1
  58. package/dist/src/commands/verify/index.js +1 -0
  59. package/dist/src/commands/verify/prebuilds.d.ts +23 -0
  60. package/dist/src/commands/verify/prebuilds.js +129 -13
  61. package/dist/src/commands/verify/resource-collectors.js +2 -1
  62. package/dist/src/expo/plugins/patches/qvac-platform-addons.d.ts +23 -0
  63. package/dist/src/expo/plugins/patches/qvac-platform-addons.js +94 -0
  64. package/dist/src/expo/plugins/withMobileBundle.d.ts +12 -2
  65. package/dist/src/expo/plugins/withMobileBundle.js +62 -31
  66. package/dist/src/index.d.ts +3 -3
  67. package/dist/src/index.js +7 -3
  68. package/dist/src/schemas/sdk-errors-server.d.ts +4 -0
  69. package/dist/src/schemas/sdk-errors-server.js +22 -0
  70. package/dist/src/utils/aggregate-events.d.ts +3 -1
  71. package/dist/src/utils/aggregate-events.js +7 -1
  72. package/dist/src/utils/errors-server.d.ts +12 -0
  73. package/dist/src/utils/errors-server.js +21 -0
  74. package/docs/system-resources-support-matrix.md +19 -19
  75. package/package.json +22 -15
  76. package/src/electron-forge/index.cjs +86 -14
  77. package/src/expo/plugins/patches/android-link.mjs +18 -9
  78. package/src/expo/plugins/patches/ios-link.mjs +18 -9
  79. package/src/expo/plugins/patches/qvac-platform-addons.ts +124 -0
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Advisory llama.cpp fit check (QVAC-22629).
3
+ *
4
+ * Before a completion or embedding load, the SDK runs `@qvac/model-fit` in one
5
+ * disposable Bare child and projects whether the exact configuration it is
6
+ * about to load will fit in device memory.
7
+ *
8
+ * The result is ADVISORY. It never blocks a load. `does-not-fit` is logged and
9
+ * the ordinary load path runs unchanged. Crashes, timeouts, malformed
10
+ * responses, unsupported configurations, and internal errors all resolve to
11
+ * "no evidence" and are equally non-blocking. Nothing consumes the verdict
12
+ * yet — this PR only produces it.
13
+ *
14
+ * The verdict is emitted on the SDK server log stream, not to stdout, so this
15
+ * example subscribes to `loggingStream({ id: SDK_LOG_ID })` and reprints the
16
+ * `[advisory-fit:…]` lines.
17
+ *
18
+ * ---------------------------------------------------------------------------
19
+ * What fits on this machine (Apple M4 Pro, 24 GiB unified memory)
20
+ * ---------------------------------------------------------------------------
21
+ *
22
+ * Measured with `@qvac/model-fit@0.8.0` (the first release carrying the
23
+ * qvac-fabric#214 memory-reporting fix) at the default 1024 MiB margin. The
24
+ * fitter budgets against what the machine can actually keep resident
25
+ * (total − wired − compressor: 17.4 GiB on this machine at idle), not the raw
26
+ * RAM figure.
27
+ *
28
+ * PROJECTED TO FIT — all layers on GPU
29
+ * Qwen3.5 0.8B Q4_K_M 0.5 GiB @ 4k ctx
30
+ * gpt-oss-20B Q4_K_M 10.8 GiB @ 32k ctx
31
+ * gte-large fp16 0.6 GiB (embedding, context pinned to 512)
32
+ *
33
+ * PROJECTED NOT TO FIT — try any of these to see a `does-not-fit` verdict
34
+ * gpt-oss-20B Q4_K_M 10.8 GiB @ 128k ctx with an f32 KV cache ← below
35
+ * Gemma 4 31B Q4_K_M 18.3 GiB @ 1k ctx ← the weights alone do not
36
+ * fit with gpu_layers: 99
37
+ *
38
+ * gpt-oss-20B at 128k context FITS with the default KV cache and DOES NOT FIT
39
+ * once `cache-type-k`/`cache-type-v` are set to `f32`. Same model, same
40
+ * context, same machine — the verdict tracks the configuration, not the file
41
+ * size.
42
+ *
43
+ * Two boundaries worth understanding when reading verdicts:
44
+ *
45
+ * 1. The verdict answers for a PLACEMENT, not a model. The SDK's schema
46
+ * default is `gpu_layers: 99` ("everything on the GPU"), so `does-not-fit`
47
+ * means "not at this placement". Omit `gpu_layers` and the fitter is free
48
+ * to move layers to the CPU side instead.
49
+ * 2. A `does-not-fit` configuration can still RUN on macOS when the OS
50
+ * compresses and pages hard enough — and a `fits` configuration right at
51
+ * the boundary can still fail at first decode under memory pressure.
52
+ * Prediction cannot separate those cases from a snapshot; the addon-side
53
+ * probe decode (QVAC-24114) is the runtime check that catches the
54
+ * remainder. The verdict here is the honest working-set budget, and the
55
+ * measured failure modes punish over-committing, so treat `does-not-fit`
56
+ * as "expect degradation or decode failure", not "the load will error".
57
+ */
58
+ import { completion, loadModel, unloadModel, loggingStream, SDK_LOG_ID, QWEN3_5_0_8B_MULTIMODAL_Q4_K_M, GPT_OSS_20B_INST_Q4_K_M } from '@qvac/sdk';
59
+ // The oversized load below is expected to be reported as `does-not-fit` and
60
+ // then attempted anyway, because the check is advisory. It really does try to
61
+ // allocate ~11 GiB, so it stays opt-in.
62
+ const ATTEMPT_OVERSIZED = process.env['QVAC_FIT_DEMO_ATTEMPT_OVERSIZED'] === '1';
63
+ // Reprint the worker's advisory verdicts. They arrive on the SDK server log
64
+ // stream; everything else on that stream is filtered out to keep this readable.
65
+ //
66
+ // Called once per phase rather than once for the process: a subscription
67
+ // currently stops delivering after any `unloadModel`, so a single one would go
68
+ // silent before the second verdict. Resubscribing after the unload works.
69
+ function watchVerdicts() {
70
+ void (async () => {
71
+ for await (const log of loggingStream({ id: SDK_LOG_ID })) {
72
+ if (log.message.includes('[advisory-fit:')) {
73
+ console.log(`▸ [${log.level.toUpperCase()}] ${log.message}`);
74
+ }
75
+ }
76
+ })().catch(() => {
77
+ // Stream terminated — normal on shutdown.
78
+ });
79
+ }
80
+ watchVerdicts();
81
+ try {
82
+ // 1. A load the fitter projects to fit. The verdict carries the plan it
83
+ // projected: resolved context, offloaded layers, and GPU device count.
84
+ console.log('▸ Loading Qwen3.5 0.8B @ 4k — expected verdict: projected to fit');
85
+ const smallModelId = await loadModel({
86
+ modelSrc: QWEN3_5_0_8B_MULTIMODAL_Q4_K_M,
87
+ modelConfig: { ctx_size: 4096 }
88
+ });
89
+ console.log(`▸ Loaded ${smallModelId}\n`);
90
+ const result = completion({
91
+ modelId: smallModelId,
92
+ history: [{ role: 'user', content: 'Say hello in five words.' }],
93
+ stream: false,
94
+ generationParams: { predict: 48 }
95
+ });
96
+ const final = await result.final;
97
+ console.log(`▸ Completion still works normally: ${final.contentText.trim().slice(0, 120)}\n`);
98
+ // Unloaded before the next phase, so the second verdict is measured on an
99
+ // idle machine and stays comparable to the fixture tables above. Leaving it
100
+ // loaded would shift the verdict: the check reserves resident weight bytes
101
+ // through the fit margin, and since model-fit 0.8.0 the fit child also sees
102
+ // system-wide wired memory.
103
+ await unloadModel({ modelId: smallModelId, clearStorage: false });
104
+ watchVerdicts();
105
+ // 2. A load the fitter projects NOT to fit. The point of this example is that
106
+ // the SDK reports the verdict and then loads anyway — the check is
107
+ // evidence, not admission control.
108
+ if (!ATTEMPT_OVERSIZED) {
109
+ console.log('▸ Skipping the oversized gpt-oss-20B load.');
110
+ console.log('▸ Set QVAC_FIT_DEMO_ATTEMPT_OVERSIZED=1 to let it run and watch the');
111
+ console.log(' load proceed past a `does-not-fit` verdict (allocates ~11 GiB).');
112
+ }
113
+ else {
114
+ console.log('▸ Loading gpt-oss-20B @ 128k with an f32 KV cache');
115
+ console.log('▸ Expected verdict: projected NOT to fit');
116
+ console.log('▸ The load is attempted regardless. That is the fail-open contract:');
117
+ console.log(' the verdict is evidence, not admission control.\n');
118
+ const bigModelId = await loadModel({
119
+ modelSrc: GPT_OSS_20B_INST_Q4_K_M,
120
+ modelConfig: {
121
+ ctx_size: 131072,
122
+ 'cache-type-k': 'f32',
123
+ 'cache-type-v': 'f32'
124
+ }
125
+ });
126
+ console.log(`▸ Load returned ${bigModelId} — the advisory verdict did not block it`);
127
+ // Loading is not the same as being usable: a model can load and then fail
128
+ // at decode time. Gemma 4 31B does exactly that on this machine. So run a
129
+ // real completion and report throughput rather than trusting the load.
130
+ try {
131
+ const check = completion({
132
+ modelId: bigModelId,
133
+ history: [{ role: 'user', content: 'Name three colours. Answer briefly.' }],
134
+ stream: false,
135
+ generationParams: { predict: 40 }
136
+ });
137
+ const checkFinal = await check.final;
138
+ console.log(`▸ ...and it actually runs: ${checkFinal.stats?.tokensPerSecond?.toFixed(1) ?? '?'} tok/s ` +
139
+ `— the OS compressed and paged its way past the working-set budget`);
140
+ }
141
+ catch (inferenceError) {
142
+ console.log(`▸ ...but it cannot run: ${inferenceError instanceof Error ? inferenceError.message : String(inferenceError)}`);
143
+ console.log('▸ The verdict was right, and `loadModel` succeeding did not mean usable.');
144
+ }
145
+ await unloadModel({ modelId: bigModelId, clearStorage: false });
146
+ }
147
+ }
148
+ catch (error) {
149
+ // A failing load here is the native loader's own error, not the fit check.
150
+ // The check never throws and never converts a verdict into a load failure.
151
+ console.error('✖', error instanceof Error ? error.message : error);
152
+ process.exitCode = 1;
153
+ }
154
+ // The log subscription is an open stream and would otherwise keep the process
155
+ // alive after the work is done.
156
+ process.exit(process.exitCode ?? 0);
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=parakeet-nemotron-filesystem.d.ts.map
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Parakeet Nemotron transcription from an audio file.
3
+ *
4
+ * Usage:
5
+ * bun run examples/asr/parakeet-nemotron-filesystem.ts \
6
+ * <audio-file> [locale] [nemotron-gguf] [--streaming]
7
+ * bun run examples/asr/parakeet-nemotron-filesystem.ts \
8
+ * <audio-file> [nemotron-gguf] [--streaming]
9
+ *
10
+ * Uses `PARAKEET_NEMOTRON_0_6B_Q4_0` when the model is omitted and `auto` when
11
+ * the locale is omitted. Pass `--streaming` to stream decoded 16 kHz mono PCM;
12
+ * otherwise, the input is transcribed in batch mode.
13
+ */
14
+ import { loadModel, PARAKEET_NEMOTRON_0_6B_Q4_0, transcribe, transcribeStream, unloadModel } from '@qvac/sdk';
15
+ import { spawn } from 'child_process';
16
+ const SAMPLE_RATE = 16000;
17
+ const BYTES_PER_S16_SAMPLE = 2;
18
+ const INPUT_CHUNK_MS = 160;
19
+ const MILLISECONDS_PER_SECOND = 1000;
20
+ const BYTES_PER_MEGABYTE = 1e6;
21
+ const DEFAULT_LOCALE = 'auto';
22
+ const FFMPEG_PROTOCOLS = 'file,pipe';
23
+ const STREAMING_FLAG = '--streaming';
24
+ const EARLY_OUTPUT_END_MESSAGE = 'The transcription stream ended before the decoder input completed';
25
+ const USAGE = 'Usage: bun run examples/asr/parakeet-nemotron-filesystem.ts ' +
26
+ '<audio-file> [locale] [nemotron-gguf] [--streaming]';
27
+ function parseArguments(args) {
28
+ const streaming = args.includes(STREAMING_FLAG);
29
+ const positional = args.filter((argument) => argument !== STREAMING_FLAG);
30
+ const [audioFilePath, second, third] = positional;
31
+ const secondIsModel = second !== undefined &&
32
+ (second.endsWith('.gguf') || second.includes('/') || second.includes('\\'));
33
+ const locale = secondIsModel ? DEFAULT_LOCALE : (second ?? DEFAULT_LOCALE);
34
+ const modelSource = secondIsModel ? second : third;
35
+ return { audioFilePath, locale, modelSource, streaming };
36
+ }
37
+ function requireAudioFilePath(audioFilePath) {
38
+ if (audioFilePath)
39
+ return audioFilePath;
40
+ throw new Error(USAGE);
41
+ }
42
+ function delay(ms) {
43
+ return new Promise((resolve) => setTimeout(resolve, ms));
44
+ }
45
+ function calculateChunkBytes() {
46
+ const samplesPerChunk = Math.floor((INPUT_CHUNK_MS / MILLISECONDS_PER_SECOND) * SAMPLE_RATE);
47
+ return samplesPerChunk * BYTES_PER_S16_SAMPLE;
48
+ }
49
+ function createDecoder(inputPath) {
50
+ return spawn('ffmpeg', [
51
+ '-protocol_whitelist',
52
+ FFMPEG_PROTOCOLS,
53
+ '-i',
54
+ inputPath,
55
+ '-ar',
56
+ String(SAMPLE_RATE),
57
+ '-ac',
58
+ '1',
59
+ '-f',
60
+ 's16le',
61
+ 'pipe:1'
62
+ ], { stdio: ['ignore', 'pipe', 'inherit'] });
63
+ }
64
+ function waitForDecoder(decoder) {
65
+ return new Promise((resolve) => {
66
+ decoder.once('error', resolve);
67
+ decoder.once('close', (code) => {
68
+ if (code === 0)
69
+ resolve(null);
70
+ else
71
+ resolve(new Error(`ffmpeg exited with code ${code}`));
72
+ });
73
+ });
74
+ }
75
+ function stopDecoder(decoder) {
76
+ if (decoder?.exitCode === null && !decoder.killed)
77
+ decoder.kill();
78
+ }
79
+ async function printTranscription(session) {
80
+ for await (const event of session) {
81
+ if (event.type === 'text' && event.text)
82
+ process.stdout.write(event.text);
83
+ }
84
+ }
85
+ async function rejectWhenOutputEnds(output) {
86
+ await output;
87
+ throw new Error(EARLY_OUTPUT_END_MESSAGE);
88
+ }
89
+ async function writeAvailableChunks(session, pcm, chunkBytes) {
90
+ let offset = 0;
91
+ while (pcm.length - offset >= chunkBytes) {
92
+ session.write(pcm.subarray(offset, offset + chunkBytes));
93
+ offset += chunkBytes;
94
+ await delay(INPUT_CHUNK_MS);
95
+ }
96
+ return pcm.subarray(offset);
97
+ }
98
+ async function feedDecoderOutput(session, decoder, chunkBytes) {
99
+ let incomplete = Buffer.alloc(0);
100
+ for await (const decoded of decoder.stdout) {
101
+ const pcm = incomplete.length === 0 ? Buffer.from(decoded) : Buffer.concat([incomplete, decoded]);
102
+ incomplete = await writeAvailableChunks(session, pcm, chunkBytes);
103
+ }
104
+ return incomplete;
105
+ }
106
+ async function runStreaming(modelId, inputPath) {
107
+ const session = await transcribeStream({ modelId, parakeetStreamingConfig: {} });
108
+ const output = printTranscription(session);
109
+ let decoder;
110
+ let exited;
111
+ let feeding;
112
+ try {
113
+ decoder = createDecoder(inputPath);
114
+ exited = waitForDecoder(decoder);
115
+ feeding = feedDecoderOutput(session, decoder, calculateChunkBytes());
116
+ const incomplete = await Promise.race([feeding, rejectWhenOutputEnds(output)]);
117
+ const decoderError = await exited;
118
+ if (decoderError)
119
+ throw decoderError;
120
+ if (incomplete.length > 0)
121
+ session.write(incomplete);
122
+ session.end();
123
+ await output;
124
+ process.stdout.write('\n');
125
+ }
126
+ finally {
127
+ stopDecoder(decoder);
128
+ session.destroy();
129
+ await Promise.allSettled([exited, feeding, output]);
130
+ }
131
+ }
132
+ function printDownloadProgress(progress) {
133
+ const downloaded = (progress.downloaded / BYTES_PER_MEGABYTE).toFixed(1);
134
+ const total = (progress.total / BYTES_PER_MEGABYTE).toFixed(1);
135
+ const percentage = progress.percentage.toFixed(0);
136
+ const line = `Downloading ${percentage}% (${downloaded}/${total} MB)`;
137
+ process.stderr.write(process.stderr.isTTY ? `\r${line}` : `${line}\n`);
138
+ if (progress.percentage >= 100)
139
+ process.stderr.write('\n');
140
+ }
141
+ async function run() {
142
+ const { audioFilePath, locale, modelSource, streaming } = parseArguments(process.argv.slice(2));
143
+ const inputPath = requireAudioFilePath(audioFilePath);
144
+ let modelId = null;
145
+ try {
146
+ console.log(`Loading Nemotron with locale ${locale}...`);
147
+ modelId = await loadModel({
148
+ modelSrc: modelSource ?? PARAKEET_NEMOTRON_0_6B_Q4_0,
149
+ modelType: 'parakeet-transcription',
150
+ modelConfig: { language: locale, streaming },
151
+ onProgress: printDownloadProgress
152
+ });
153
+ if (streaming) {
154
+ console.log('Streaming with the Nemotron native 320 ms operating point...');
155
+ await runStreaming(modelId, inputPath);
156
+ }
157
+ else {
158
+ console.log('Transcribing in batch mode...');
159
+ console.log(await transcribe({ modelId, audioChunk: inputPath }));
160
+ }
161
+ }
162
+ finally {
163
+ if (modelId)
164
+ await unloadModel({ modelId });
165
+ }
166
+ }
167
+ run().catch((error) => {
168
+ console.error(error);
169
+ process.exitCode = 1;
170
+ });
@@ -53,7 +53,12 @@ try {
53
53
  for (const [index, model] of result.models.entries()) {
54
54
  const mark = VERDICT_MARK[model.verdict];
55
55
  const size = gib(CANDIDATES[index].expectedSize).padStart(9);
56
- const needs = model.estimate ? `needs ${gib(model.estimate.upperBoundBytes)}` : '';
56
+ // A calibrated row has a two-sided estimate; a computed-only row has the floor.
57
+ const needs = model.estimate
58
+ ? `needs ${gib(model.estimate.upperBoundBytes)}`
59
+ : model.evidence === 'computed-only' && model.floorBytes
60
+ ? `at least ${gib(model.floorBytes)}`
61
+ : '';
57
62
  console.log(` ${mark} ${model.name.padEnd(46)} ${size} on disk ${needs}`);
58
63
  // Present on every `unknown`, and worth surfacing: it names what is missing.
59
64
  if (model.verdict === 'unknown') {
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=edit-music.d.ts.map
@@ -0,0 +1,121 @@
1
+ import { writeFileSync } from 'node:fs';
2
+ import { AUDIOGEN_ACESTEP_5HZ_LM_0_6B_Q8_0, AUDIOGEN_ACESTEP_V15_TURBO_Q4_K_M, AUDIOGEN_QWEN3_EMBEDDING_0_6B_Q8_0, AUDIOGEN_VAE_BF16, audioEdit, loadModel, unloadModel } from '@qvac/sdk';
3
+ // Edit an existing recording with ACE-Step: a Flow-Edit that re-conditions the
4
+ // whole clip from a source prompt to a target prompt, then a Repaint that
5
+ // regenerates one time range against a new prompt. Operations run in order.
6
+ //
7
+ // Usage:
8
+ // bun examples/audiogen/edit-music.ts <source.wav|mp3|...> "original pop song" "guitar pop-rock" [output.wav]
9
+ //
10
+ // The source must run at least REPAINT_END seconds: the Repaint below asks for
11
+ // a fixed window, and a range past the end of the source is rejected with
12
+ // `repaint.start must be within the source duration`.
13
+ //
14
+ // The source is a file path: the SDK decodes it (any FFmpeg-decodable format)
15
+ // to the 48 kHz stereo float PCM the engine expects. Pass raw interleaved
16
+ // stereo 48 kHz Float32 LE PCM in [-1, 1] as a Buffer instead when the audio
17
+ // is already in memory — for example the `pcm` of an earlier `audioGen()` run,
18
+ // converted from Int16 to Float32.
19
+ // Repaint window, in seconds from the start of the source.
20
+ const REPAINT_START = 10;
21
+ const REPAINT_END = 20;
22
+ const sourcePath = process.argv[2];
23
+ const fromCaption = process.argv[3] ?? 'Original pop song';
24
+ const toCaption = process.argv[4] ?? 'Guitar pop-rock';
25
+ const outputPath = process.argv[5] ?? 'audiogen-edit.wav';
26
+ if (!sourcePath) {
27
+ console.error('Usage: bun examples/audiogen/edit-music.ts <source-audio> "<from caption>" "<to caption>" [output.wav]');
28
+ process.exit(1);
29
+ }
30
+ let modelId;
31
+ try {
32
+ console.log('▸ Loading ACE-Step AudioGen models...');
33
+ modelId = await loadModel({
34
+ modelType: 'audiogen',
35
+ modelConfig: {
36
+ textEncModelSrc: AUDIOGEN_QWEN3_EMBEDDING_0_6B_Q8_0,
37
+ lmModelSrc: AUDIOGEN_ACESTEP_5HZ_LM_0_6B_Q8_0,
38
+ // Flow-Edit needs a turbo DiT variant; Repaint works on every variant.
39
+ ditModelSrc: AUDIOGEN_ACESTEP_V15_TURBO_Q4_K_M,
40
+ vaeModelSrc: AUDIOGEN_VAE_BF16,
41
+ useGPU: true
42
+ }
43
+ });
44
+ console.log(`▸ Model loaded: ${modelId}`);
45
+ console.log(`▸ Editing ${sourcePath}: "${fromCaption}" -> "${toCaption}"`);
46
+ const run = audioEdit({
47
+ modelId,
48
+ sourceAudio: sourcePath,
49
+ operations: [
50
+ {
51
+ type: 'flow-edit',
52
+ from: { caption: fromCaption },
53
+ to: { caption: toCaption }
54
+ },
55
+ {
56
+ // Regenerate the repaint window as a synth solo; the rest is kept.
57
+ type: 'repaint',
58
+ caption: 'analog synth solo',
59
+ lyrics: '[Instrumental]',
60
+ start: REPAINT_START,
61
+ end: REPAINT_END,
62
+ mode: 'balanced',
63
+ strength: 0.5
64
+ }
65
+ ],
66
+ // Seeds the first operation; each following operation uses seed + index.
67
+ seed: 22883
68
+ });
69
+ console.log(`▸ requestId: ${run.requestId}`);
70
+ for await (const progress of run.progressStream) {
71
+ const value = progress.total > 0 ? `${progress.step}/${progress.total}` : `${progress.step} (indeterminate)`;
72
+ console.log(`▸ ${progress.stage}: ${value}`);
73
+ }
74
+ const [audio, stats] = await Promise.all([run.audio, run.stats]);
75
+ writeFileSync(outputPath, createWav(audio.pcm, audio.sampleRate, audio.channels, audio.bitsPerSample));
76
+ if (stats)
77
+ console.log(`▸ Stats: ${JSON.stringify(stats)}`);
78
+ console.log(`▸ Saved ${outputPath}`);
79
+ await unloadModel({ modelId });
80
+ modelId = undefined;
81
+ process.exit(0);
82
+ }
83
+ catch (error) {
84
+ if (modelId !== undefined) {
85
+ try {
86
+ await unloadModel({ modelId });
87
+ }
88
+ catch {
89
+ // Preserve the edit error as the primary failure.
90
+ }
91
+ }
92
+ console.error('✖', error);
93
+ process.exit(1);
94
+ }
95
+ function createWav(pcm, sampleRate, channels, bitsPerSample) {
96
+ const header = new ArrayBuffer(44);
97
+ const view = new DataView(header);
98
+ const blockAlign = channels * (bitsPerSample / 8);
99
+ writeAscii(view, 0, 'RIFF');
100
+ view.setUint32(4, 36 + pcm.byteLength, true);
101
+ writeAscii(view, 8, 'WAVE');
102
+ writeAscii(view, 12, 'fmt ');
103
+ view.setUint32(16, 16, true);
104
+ view.setUint16(20, 1, true);
105
+ view.setUint16(22, channels, true);
106
+ view.setUint32(24, sampleRate, true);
107
+ view.setUint32(28, sampleRate * blockAlign, true);
108
+ view.setUint16(32, blockAlign, true);
109
+ view.setUint16(34, bitsPerSample, true);
110
+ writeAscii(view, 36, 'data');
111
+ view.setUint32(40, pcm.byteLength, true);
112
+ const wav = new Uint8Array(44 + pcm.byteLength);
113
+ wav.set(new Uint8Array(header));
114
+ wav.set(pcm, 44);
115
+ return wav;
116
+ }
117
+ function writeAscii(view, offset, value) {
118
+ for (let index = 0; index < value.length; index++) {
119
+ view.setUint8(offset + index, value.charCodeAt(index));
120
+ }
121
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=understand-music.d.ts.map
@@ -0,0 +1,73 @@
1
+ import { AUDIOGEN_ACESTEP_5HZ_LM_0_6B_Q8_0, AUDIOGEN_ACESTEP_V15_TURBO_Q4_K_M, AUDIOGEN_QWEN3_EMBEDDING_0_6B_Q8_0, AUDIOGEN_VAE_BF16, audioUnderstand, loadModel, unloadModel } from '@qvac/sdk';
2
+ // Describe an existing recording with ACE-Step's reverse pipeline: the engine
3
+ // encodes the audio, recovers its FSQ semantic codes, and the LM reports a
4
+ // caption and the clip's musical metadata.
5
+ //
6
+ // Usage:
7
+ // bun examples/audiogen/understand-music.ts <source.wav|mp3|...> [language hint]
8
+ //
9
+ // The source is a file path: the SDK decodes it (any FFmpeg-decodable format)
10
+ // to the 48 kHz stereo float PCM the engine expects. Pass raw interleaved
11
+ // stereo 48 kHz Float32 LE PCM in [-1, 1] as a Buffer instead when the audio
12
+ // is already in memory.
13
+ const sourcePath = process.argv[2];
14
+ const vocalLanguage = process.argv[3];
15
+ if (!sourcePath) {
16
+ console.error('Usage: bun examples/audiogen/understand-music.ts <source-audio> [language]');
17
+ process.exit(1);
18
+ }
19
+ let modelId;
20
+ try {
21
+ console.log('▸ Loading ACE-Step AudioGen models...');
22
+ modelId = await loadModel({
23
+ modelType: 'audiogen',
24
+ modelConfig: {
25
+ textEncModelSrc: AUDIOGEN_QWEN3_EMBEDDING_0_6B_Q8_0,
26
+ lmModelSrc: AUDIOGEN_ACESTEP_5HZ_LM_0_6B_Q8_0,
27
+ ditModelSrc: AUDIOGEN_ACESTEP_V15_TURBO_Q4_K_M,
28
+ vaeModelSrc: AUDIOGEN_VAE_BF16,
29
+ useGPU: true
30
+ }
31
+ });
32
+ console.log(`▸ Model loaded: ${modelId}`);
33
+ console.log(`▸ Analysing ${sourcePath}`);
34
+ const run = audioUnderstand({
35
+ modelId,
36
+ sourceAudio: sourcePath,
37
+ // Forced into the result instead of the LM's guess; omit to let it decide.
38
+ ...(vocalLanguage !== undefined && { vocalLanguage }),
39
+ seed: 11
40
+ });
41
+ console.log(`▸ requestId: ${run.requestId}`);
42
+ for await (const progress of run.progressStream) {
43
+ const value = progress.total > 0 ? `${progress.step}/${progress.total}` : `${progress.step} (indeterminate)`;
44
+ console.log(`▸ ${progress.stage}: ${value}`);
45
+ }
46
+ const [description, stats] = await Promise.all([run.description, run.stats]);
47
+ console.log(`▸ Caption: ${description.caption}`);
48
+ console.log(`▸ ${description.bpm} BPM, ${description.keyscale}, ${description.timesignature}`);
49
+ console.log(`▸ Vocal language: ${description.vocalLanguage}`);
50
+ // The LM's duration is an estimate; the recovered codes fix the true length.
51
+ console.log(`▸ Estimated duration: ${description.duration.toFixed(1)} s`);
52
+ console.log(`▸ Recovered ${description.audioCodes.length} semantic codes`);
53
+ if (stats)
54
+ console.log(`▸ Stats: ${JSON.stringify({ ...stats, understand: undefined })}`);
55
+ // `audioCodes` feeds straight back into audioGen() to re-synthesize the piece
56
+ // without re-running the LM:
57
+ // audioGen({ modelId, caption: description.caption, audioCodes: description.audioCodes })
58
+ await unloadModel({ modelId });
59
+ modelId = undefined;
60
+ process.exit(0);
61
+ }
62
+ catch (error) {
63
+ if (modelId !== undefined) {
64
+ try {
65
+ await unloadModel({ modelId });
66
+ }
67
+ catch {
68
+ // Preserve the understand error as the primary failure.
69
+ }
70
+ }
71
+ console.error('✖', error);
72
+ process.exit(1);
73
+ }
@@ -1,4 +1,4 @@
1
- import { GEMMA_3_12B_Q4_K_XL, loadModel, LTX_2_3_22B_DISTILLED_EMBEDDINGS_CONNECTORS, LTX_2_3_22B_Q5_K_M, LTX_2_3_VAE, LTX_2_3_VAE_1, unloadModel, video } from '@qvac/sdk';
1
+ import { GEMMA_3_12B_Q4_K_XL, loadModel, LTX_2_3_22B_DISTILLED_EMBEDDINGS_CONNECTORS, LTX_2_3_22B_Q5_K_M, LTX_2_3_AUDIO_VAE, LTX_2_3_VIDEO_VAE, unloadModel, video } from '@qvac/sdk';
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
4
  const loraPath = process.argv[2];
@@ -24,8 +24,8 @@ try {
24
24
  device: 'gpu',
25
25
  threads: 4,
26
26
  llmModelSrc: GEMMA_3_12B_Q4_K_XL,
27
- vaeModelSrc: LTX_2_3_VAE_1,
28
- audioVaeModelSrc: LTX_2_3_VAE,
27
+ vaeModelSrc: LTX_2_3_VIDEO_VAE,
28
+ audioVaeModelSrc: LTX_2_3_AUDIO_VAE,
29
29
  embeddingsConnectorsModelSrc: LTX_2_3_22B_DISTILLED_EMBEDDINGS_CONNECTORS,
30
30
  diffusion_fa: true,
31
31
  offload_to_cpu: true,
@@ -33,7 +33,7 @@ try {
33
33
  clipVisionModelSrc,
34
34
  diffusion_fa: true,
35
35
  offload_to_cpu: true,
36
- vae_on_cpu: true,
36
+ params_backend: 'vae=cpu',
37
37
  vae_tiling: true
38
38
  },
39
39
  onProgress: (p) => {
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=diffusion-txt2vid-minimax-h3.d.ts.map
@@ -0,0 +1,64 @@
1
+ import { loadModel, unloadModel, video } from '@qvac/sdk';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ // Pass a directory containing the four MiniMax-H3 files, followed by an output path.
5
+ const modelsDir = process.argv[2];
6
+ if (!modelsDir) {
7
+ throw new Error('Usage: diffusion-txt2vid-minimax-h3.ts <models-directory> [output.avi]');
8
+ }
9
+ const outputPath = process.argv[3] || 'minimax-h3.avi';
10
+ const modelSrc = path.resolve(modelsDir, process.env['H3_MODEL'] || 'minimax_h3_fl2va_pruned-Q4_K.gguf');
11
+ const llmModelSrc = path.resolve(modelsDir, process.env['H3_LLM'] || 'qwen3vl_32b_minimax_h3-Q4_K_M.gguf');
12
+ const vaeModelSrc = path.resolve(modelsDir, 'vae/minimax_h3_video_vae_fp16.safetensors');
13
+ const audioVaeModelSrc = path.resolve(modelsDir, 'vae/minimax_h3_audio_vae_fp32.safetensors');
14
+ for (const file of [modelSrc, llmModelSrc, vaeModelSrc, audioVaeModelSrc])
15
+ fs.accessSync(file);
16
+ let modelId;
17
+ try {
18
+ modelId = await loadModel({
19
+ modelType: 'sdcpp-generation',
20
+ modelSrc,
21
+ modelConfig: {
22
+ mode: 'video',
23
+ llmModelSrc,
24
+ vaeModelSrc,
25
+ audioVaeModelSrc,
26
+ device: 'gpu',
27
+ diffusion_fa: true,
28
+ offload_to_cpu: process.env['H3_OFFLOAD_TO_CPU'] !== '0',
29
+ stream_layers: process.env['H3_STREAM_LAYERS'] === '1',
30
+ ...(process.env['H3_BACKEND'] && { backend: process.env['H3_BACKEND'] }),
31
+ ...(process.env['H3_PARAMS_BACKEND'] && { params_backend: process.env['H3_PARAMS_BACKEND'] }),
32
+ ...(process.env['H3_MAX_VRAM'] && { max_vram: process.env['H3_MAX_VRAM'] })
33
+ }
34
+ });
35
+ const { progressStream, outputs, stats } = video({
36
+ modelId,
37
+ mode: 'txt2vid',
38
+ prompt: process.env['PROMPT'] ||
39
+ 'Premium cinematic coffee commercial. A confident adult sits at a small café table at sunrise, slowly lifts one matte black coffee cup, takes a relaxed sip, and smiles. Warm golden rim light, drifting steam, realistic skin, natural hands, shallow depth of field, subtle slow camera push-in, restrained natural motion, polished live-action advertising, no dialogue, no text overlay.',
40
+ negative_prompt: 'extra people, duplicate cup, malformed hands, cup fused to hand, text, subtitles, watermark, cartoon, CGI, blur, flicker, jitter, camera shake',
41
+ width: 960,
42
+ height: 544,
43
+ // H3 uses 17*k+5 frames; 124 frames at 24 FPS is approximately 5.17 seconds.
44
+ video_frames: Number(process.env['FRAMES'] || 124),
45
+ fps: 24,
46
+ steps: 8,
47
+ cfg_scale: 1,
48
+ seed: 11
49
+ });
50
+ const progress = (async () => {
51
+ for await (const tick of progressStream)
52
+ console.log(`Step ${tick.step}/${tick.totalSteps}`);
53
+ })();
54
+ const [buffers, runtimeStats] = await Promise.all([outputs, stats, progress]);
55
+ if (!buffers[0])
56
+ throw new Error('Generation finished without an AVI result');
57
+ fs.writeFileSync(outputPath, buffers[0]);
58
+ console.log(`Saved ${outputPath}`, runtimeStats);
59
+ }
60
+ finally {
61
+ if (modelId)
62
+ await unloadModel({ modelId, clearStorage: false });
63
+ }
64
+ process.exit(0);
@@ -26,7 +26,7 @@ try {
26
26
  vaeModelSrc,
27
27
  diffusion_fa: true,
28
28
  offload_to_cpu: true,
29
- vae_on_cpu: true,
29
+ params_backend: 'vae=cpu',
30
30
  vae_tiling: true
31
31
  },
32
32
  onProgress: (p) => {