@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.
- package/NOTICE +111 -82
- package/dist/examples/abot-world.js +3 -3
- package/dist/examples/advisory-model-fit.d.ts +59 -0
- package/dist/examples/advisory-model-fit.js +156 -0
- package/dist/examples/asr/parakeet-nemotron-filesystem.d.ts +2 -0
- package/dist/examples/asr/parakeet-nemotron-filesystem.js +170 -0
- package/dist/examples/assess-model-fit.js +6 -1
- package/dist/examples/audiogen/edit-music.d.ts +2 -0
- package/dist/examples/audiogen/edit-music.js +121 -0
- package/dist/examples/audiogen/understand-music.d.ts +2 -0
- package/dist/examples/audiogen/understand-music.js +73 -0
- package/dist/examples/diffusion-ic-lora-ltx.js +3 -3
- package/dist/examples/diffusion-img2vid.js +1 -1
- package/dist/examples/diffusion-txt2vid-minimax-h3.d.ts +2 -0
- package/dist/examples/diffusion-txt2vid-minimax-h3.js +64 -0
- package/dist/examples/diffusion-txt2vid.js +1 -1
- package/dist/examples/kv-cache-cleanup.js +5 -2
- package/dist/examples/model-fit-ui/server.js +33 -7
- package/dist/examples/rag/rag-turbovec.d.ts +2 -0
- package/dist/examples/rag/rag-turbovec.js +58 -0
- package/dist/examples/tts/audio8.js +5 -5
- package/dist/examples/tts/chatterbox-enhanced.js +4 -5
- package/dist/examples/tts/chatterbox-multilingual.js +5 -5
- package/dist/examples/tts/chatterbox.js +5 -5
- package/dist/examples/tts/cosyvoice3.js +5 -5
- package/dist/examples/tts/llm-to-tts-streaming.js +12 -2
- package/dist/examples/tts/parler.js +5 -5
- package/dist/examples/tts/supertonic-enhanced.js +4 -5
- package/dist/examples/tts/supertonic-multilingual.js +5 -5
- package/dist/examples/tts/supertonic.js +5 -5
- package/dist/scripts/contract/build-contract.d.ts +1 -1
- package/dist/scripts/contract/constants-registry.d.ts +31 -0
- package/dist/scripts/contract/constants-registry.js +13 -2
- package/dist/scripts/contract/method-shapes.d.ts +3 -0
- package/dist/scripts/contract/method-shapes.js +3 -0
- package/dist/src/client/api/assess-model-fit.js +2 -0
- package/dist/src/client/api/audio-edit.d.ts +36 -0
- package/dist/src/client/api/audio-edit.js +38 -0
- package/dist/src/client/api/audio-gen-result.d.ts +5 -1
- package/dist/src/client/api/audio-gen-result.js +105 -52
- package/dist/src/client/api/audio-gen.d.ts +38 -1
- package/dist/src/client/api/audio-gen.js +38 -1
- package/dist/src/client/api/audio-understand.d.ts +25 -0
- package/dist/src/client/api/audio-understand.js +27 -0
- package/dist/src/client/api/delete-cache.d.ts +7 -1
- package/dist/src/client/api/delete-cache.js +11 -1
- package/dist/src/client/api/index.d.ts +3 -0
- package/dist/src/client/api/index.js +3 -0
- package/dist/src/client/api/text-to-speech.d.ts +31 -4
- package/dist/src/client/api/text-to-speech.js +118 -21
- package/dist/src/client/api/vector-index.d.ts +143 -0
- package/dist/src/client/api/vector-index.js +168 -0
- package/dist/src/client/api/world.d.ts +2 -2
- package/dist/src/commands/bundle/manifest.d.ts +2 -0
- package/dist/src/commands/verify/addon-source.d.ts +6 -0
- package/dist/src/commands/verify/bundle-source.d.ts +2 -0
- package/dist/src/commands/verify/bundle-source.js +155 -1
- package/dist/src/commands/verify/index.js +1 -0
- package/dist/src/commands/verify/prebuilds.d.ts +23 -0
- package/dist/src/commands/verify/prebuilds.js +129 -13
- package/dist/src/commands/verify/resource-collectors.js +2 -1
- package/dist/src/expo/plugins/patches/qvac-platform-addons.d.ts +23 -0
- package/dist/src/expo/plugins/patches/qvac-platform-addons.js +94 -0
- package/dist/src/expo/plugins/withMobileBundle.d.ts +12 -2
- package/dist/src/expo/plugins/withMobileBundle.js +62 -31
- package/dist/src/index.d.ts +3 -3
- package/dist/src/index.js +7 -3
- package/dist/src/schemas/sdk-errors-server.d.ts +4 -0
- package/dist/src/schemas/sdk-errors-server.js +22 -0
- package/dist/src/utils/aggregate-events.d.ts +3 -1
- package/dist/src/utils/aggregate-events.js +7 -1
- package/dist/src/utils/errors-server.d.ts +12 -0
- package/dist/src/utils/errors-server.js +21 -0
- package/docs/system-resources-support-matrix.md +19 -19
- package/package.json +22 -15
- package/src/electron-forge/index.cjs +86 -14
- package/src/expo/plugins/patches/android-link.mjs +18 -9
- package/src/expo/plugins/patches/ios-link.mjs +18 -9
- 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,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
|
-
|
|
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,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,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,
|
|
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:
|
|
28
|
-
audioVaeModelSrc:
|
|
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,
|
|
@@ -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);
|