arcane-os 0.62.0 → 0.64.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 (44) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +1 -1
  3. package/browser-runtime/core/client.mjs +7 -2
  4. package/docs/reference/README.md +7 -0
  5. package/docs/reference/cli.md +5 -1
  6. package/docs/reference/core-client.md +11 -2
  7. package/docs/reference/core-native-packaging.md +57 -8
  8. package/docs/reference/core-repositories.md +149 -0
  9. package/docs/reference/inventory/package-api.json +163 -9
  10. package/docs/reference/local-ai.md +51 -7
  11. package/docs/reference/local-image-flux.md +154 -0
  12. package/docs/reference/local-image-generation.md +12 -0
  13. package/docs/reference/local-image-sdxl.md +139 -0
  14. package/docs/reference/local-whisper.md +219 -0
  15. package/docs/reference/native-speech.md +163 -0
  16. package/docs/reference/runtime-modules.md +16 -2
  17. package/docs/reference/sdk-api.md +335 -6
  18. package/package.json +5 -1
  19. package/runtime/arcane/components/speech.html +15 -3
  20. package/runtime/arcane/components/voice-transcription.html +19 -3
  21. package/runtime/arcane/modules/AI.js +250 -27
  22. package/runtime/arcane/modules/AIProviderRuntime.js +20 -3
  23. package/runtime/arcane/modules/AIRuntimeState.js +15 -2
  24. package/runtime/arcane/modules/ComponentContracts.js +3 -0
  25. package/schemas/arcane-app.schema.json +53 -2
  26. package/schemas/arcane-package.schema.json +53 -2
  27. package/src/core/repositories.mjs +210 -0
  28. package/src/core/services/speech.mjs +361 -0
  29. package/src/local-ai/config.mjs +9 -3
  30. package/src/local-ai/dev.mjs +32 -2
  31. package/src/local-ai/image-library.mjs +9 -0
  32. package/src/local-ai/image-models/flux.mjs +31 -0
  33. package/src/local-ai/image-models/sdxl.mjs +18 -0
  34. package/src/local-ai/install.mjs +26 -7
  35. package/src/local-ai/whisper/build.mjs +48 -0
  36. package/src/local-ai/whisper/config.mjs +61 -0
  37. package/src/local-ai/whisper/index.mjs +515 -0
  38. package/src/local-ai/whisper/install.mjs +274 -0
  39. package/src/local-ai/whisper/native/CMakeLists.txt +98 -0
  40. package/src/local-ai/whisper/native/NOTICE +18 -0
  41. package/src/local-ai/whisper/native/README.md +104 -0
  42. package/src/local-ai/whisper/native/main.cpp +489 -0
  43. package/src/native/portable-layout.mjs +37 -7
  44. package/src/native/windows-provider.mjs +39 -15
@@ -18,6 +18,15 @@ export function createImageLibrary(
18
18
  ) {
19
19
  const requireBinding = createRequire(bindingModulePath);
20
20
  const koffi = requireBinding(bindingModulePath);
21
+ const configuration = koffi.config();
22
+ // Async inference needs the normal native stack before Koffi creates its pools.
23
+ if (configuration.async_stack_size < configuration.sync_stack_size) {
24
+ koffi.config(
25
+ {
26
+ async_stack_size: configuration.sync_stack_size
27
+ }
28
+ );
29
+ }
21
30
  const library = loadLibrary(libraryPath);
22
31
  const backendDirectory = dirname(libraryPath);
23
32
  let backendLibrary;
@@ -0,0 +1,31 @@
1
+ /** Distilled Klein model resources; acquisition belongs to the model asset owner. */
2
+ export const FLUX2_KLEIN_4B_MODEL = {
3
+ id: 'flux2-klein-4b',
4
+ name: 'FLUX.2 Klein 4B',
5
+ family: 'flux',
6
+ resources: {
7
+ diffusion_model: {
8
+ url: 'https://huggingface.co/black-forest-labs/FLUX.2-klein-4B/resolve/e7b7dc27f91deacad38e78976d1f2b499d76a294/flux-2-klein-4b.safetensors',
9
+ filename: 'flux-2-klein-4b.safetensors'
10
+ },
11
+ vae: {
12
+ url: 'https://huggingface.co/black-forest-labs/FLUX.2-small-decoder/resolve/a3efc24f613ef42d9428af62fdbd6f5fd8856c4a/full_encoder_small_decoder.safetensors',
13
+ filename: 'full_encoder_small_decoder.safetensors'
14
+ },
15
+ llm: {
16
+ url: 'https://huggingface.co/Qwen/Qwen3-4B-GGUF/resolve/bc640142c66e1fdd12af0bd68f40445458f3869b/Qwen3-4B-Q8_0.gguf',
17
+ filename: 'Qwen3-4B-Q8_0.gguf'
18
+ }
19
+ },
20
+ context: {},
21
+ defaults: {
22
+ sample_params: {
23
+ sample_steps: 4,
24
+ sample_method: 'euler',
25
+ guidance: {
26
+ txt_cfg: 1
27
+ }
28
+ }
29
+ },
30
+ operations: ['txt2img']
31
+ };
@@ -0,0 +1,18 @@
1
+ /** Official complete SDXL checkpoint; acquisition belongs to the model asset owner. */
2
+ export const SDXL_BASE_1_0_MODEL = {
3
+ id: 'sdxl-base-1.0',
4
+ name: 'SDXL Base 1.0',
5
+ family: 'sdxl',
6
+ resources: {
7
+ model: {
8
+ url: 'https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/resolve/462165984030d82259a11f4367a4eed129e94a7b/sd_xl_base_1.0.safetensors',
9
+ filename: 'sd_xl_base_1.0.safetensors'
10
+ }
11
+ },
12
+ context: {},
13
+ defaults: {
14
+ width: 1024,
15
+ height: 1024
16
+ },
17
+ operations: ['txt2img']
18
+ };
@@ -10,6 +10,8 @@ import {createEventQueue} from '../event-queue.mjs';
10
10
  import {runProcess} from '../process.mjs';
11
11
  import {extractLocalAIArchive} from './archive.mjs';
12
12
  import {normalizeImageRuntimeRequirement} from './config.mjs';
13
+ import {normalizeWhisperRuntimeRequirement} from './whisper/config.mjs';
14
+ import {bundledWhisperRuntime, installedWhisperRuntime, installWhisperDistribution, whisperRuntimeSelection} from './whisper/install.mjs';
13
15
 
14
16
  const is=new Is(false);
15
17
  const installations=new Map();
@@ -21,11 +23,15 @@ function selectRuntimes(runtimes){
21
23
  const selected=new Map();
22
24
  for(const item of runtimes){
23
25
  const requirement=is.string(item)?{id:item}:item;
24
- const runtime=requirement?.id==='stable-diffusion.cpp'?normalizeImageRuntimeRequirement(requirement):requirement;
25
- if(!runtime||!['llama.cpp','ollama','nemo-speech','onnx','stable-diffusion.cpp'].includes(runtime.id)){
26
+ const runtime=requirement?.id==='stable-diffusion.cpp'?normalizeImageRuntimeRequirement(requirement)
27
+ :requirement?.id==='whisper.cpp'?normalizeWhisperRuntimeRequirement(requirement):requirement;
28
+ if(!runtime||!['llama.cpp','ollama','nemo-speech','onnx','stable-diffusion.cpp','whisper.cpp'].includes(runtime.id)){
26
29
  throw new ArcaneError(ERROR_CODES.targetUnavailable,`Local AI runtime installation is unavailable for ${String(runtime?.id)}.`);
27
30
  }
28
31
  const prior=selected.get(runtime.id);
32
+ if(prior&&runtime.id==='whisper.cpp'&&whisperRuntimeSelection(prior)!==whisperRuntimeSelection(runtime)){
33
+ throw new ArcaneError(ERROR_CODES.usage,'Select one complete whisper.cpp runtime and model requirement for this operation.');
34
+ }
29
35
  if(prior&&(prior.version!==runtime.version||prior.url!==runtime.url||(runtime.id==='stable-diffusion.cpp'&&prior.backend!==runtime.backend))){
30
36
  throw new ArcaneError(ERROR_CODES.usage,`Select one ${runtime.id} runtime version and URL for this operation.`);
31
37
  }
@@ -351,7 +357,7 @@ function bundledImageModelResource(resource, descriptor, model, role, runtimeRoo
351
357
  return bundled;
352
358
  }
353
359
 
354
- async function installedRuntime(base,runtime,platform,architecture,signal){
360
+ async function installedRuntime(base,runtime,platform,architecture,signal,reuseWhisper=false){
355
361
  let entries;
356
362
  try{
357
363
  entries=await readdir(base,{withFileTypes:true});
@@ -374,9 +380,19 @@ async function installedRuntime(base,runtime,platform,architecture,signal){
374
380
  }
375
381
  if(installation.requestVersion!==(runtime.version??null)||installation.requestUrl!==(runtime.url??null))continue;
376
382
  if (runtime.id === 'stable-diffusion.cpp' && installation.requestBackend !== runtime.backend) continue;
383
+ let whisperRequirement;
384
+ if (runtime.id === 'whisper.cpp') {
385
+ if (!installation.requestWhisper) continue;
386
+ whisperRequirement = JSON.parse(installation.requestWhisper);
387
+ if (whisperRuntimeSelection(whisperRequirement, !reuseWhisper) !== whisperRuntimeSelection(runtime, !reuseWhisper)) continue;
388
+ }
377
389
  const record=installation.runtime;
378
390
  if(record?.id!==runtime.id||record.platform!==platform||record.architecture!==architecture)continue;
379
391
  try{
392
+ if(runtime.id==='whisper.cpp'){
393
+ if(await installedWhisperRuntime(record,signal,!reuseWhisper))return reuseWhisper?{runtime:record,requirement:whisperRequirement}:record;
394
+ continue;
395
+ }
380
396
  if (runtime.id === 'stable-diffusion.cpp') {
381
397
  const locations = [record.libraryPath, record.bindingModulePath];
382
398
  if (!Array.isArray(record.variants) || record.variants.length === 0) continue;
@@ -524,10 +540,11 @@ async function installRuntime(runtime,{directory,platform,architecture,signal,on
524
540
  await onEvent({type:'local-ai.install.available',message:`${runtime.id} ${installed.version} is already installed.`,data:installed});
525
541
  return installed;
526
542
  }
543
+ const reusableWhisper=runtime.id==='whisper.cpp'?await installedRuntime(base,runtime,platform,architecture,signal,true):null;
527
544
  if(runtime.id==='onnx'&&(!['win32','linux','darwin'].includes(platform)||!['x64','arm64'].includes(architecture))){
528
545
  throw new ArcaneError(ERROR_CODES.targetUnavailable,`onnxruntime-node has no supported ${platform}/${architecture} CPU runtime.`);
529
546
  }
530
- const release=runtime.id==='onnx'?null:await selectRelease(runtime,platform,architecture,signal);
547
+ const release=['onnx','whisper.cpp'].includes(runtime.id)?null:await selectRelease(runtime,platform,architecture,signal);
531
548
  throwIfAborted(signal);
532
549
  await mkdir(base,{recursive:true});
533
550
  const attempt=await mkdtemp(path.join(base,'install-'));
@@ -538,6 +555,8 @@ async function installRuntime(runtime,{directory,platform,architecture,signal,on
538
555
  let locations;
539
556
  if(runtime.id==='onnx'){
540
557
  locations=await installONNXPackage(runtime,{root,directory,platform,architecture,signal,onEvent});
558
+ } else if (runtime.id === 'whisper.cpp') {
559
+ locations = await installWhisperDistribution(runtime, {root, directory, platform, architecture, signal, onEvent, upstreamResponse, reusable: reusableWhisper});
541
560
  } else if (runtime.id === 'stable-diffusion.cpp') {
542
561
  locations = await installImageDistribution(
543
562
  release,
@@ -556,7 +575,7 @@ async function installRuntime(runtime,{directory,platform,architecture,signal,on
556
575
  const record={id:runtime.id,platform,architecture,root,...locations,...(runtime.version?{requestedVersion:runtime.version}:{})};
557
576
  throwIfAborted(signal);
558
577
  if(archive)await unlink(archive);
559
- await writeFile(path.join(attempt,'installation.json'),`${JSON.stringify({requestVersion:runtime.version??null,requestUrl:runtime.url??null,...(runtime.id==='stable-diffusion.cpp'?{requestBackend:runtime.backend}:{}),runtime:record},null,2)}\n`,{flag:'wx',signal});
578
+ await writeFile(path.join(attempt,'installation.json'),`${JSON.stringify({requestVersion:runtime.version??null,requestUrl:runtime.url??null,...(runtime.id==='stable-diffusion.cpp'?{requestBackend:runtime.backend}:{}),...(runtime.id==='whisper.cpp'?{requestWhisper:whisperRuntimeSelection(runtime)}:{}),runtime:record},null,2)}\n`,{flag:'wx',signal});
560
579
  completed=true;
561
580
  await onEvent({type:'local-ai.install.completed',message:`${runtime.id} ${record.version} is available.`,data:record});
562
581
  return record;
@@ -576,7 +595,7 @@ async function installRuntime(runtime,{directory,platform,architecture,signal,on
576
595
 
577
596
  function shareInstallation(runtime,options){
578
597
  throwIfAborted(options.signal);
579
- const key=JSON.stringify([options.directory,options.platform,options.architecture,runtime.id,runtime.version??null,runtime.url??null,runtime.id==='stable-diffusion.cpp'?runtime.backend:null]);
598
+ const key=JSON.stringify([options.directory,options.platform,options.architecture,runtime.id,runtime.version??null,runtime.url??null,runtime.id==='stable-diffusion.cpp'?runtime.backend:null,runtime.id==='whisper.cpp'?whisperRuntimeSelection(runtime):null]);
580
599
  let entry=installations.get(key);
581
600
  if(entry?.controller.signal.aborted){
582
601
  // The prior cancelled attempt owns its cleanup. A later request waits
@@ -725,7 +744,7 @@ export async function bundleLocalAIRuntimes({runtimes=[],directory,outputRoot,pl
725
744
  const files=[];
726
745
  const relative=`runtime/local-ai/${runtime.id}`;
727
746
  const destination=path.join(root,relative);
728
- const bundled={...runtime,root:relative};
747
+ const bundled=runtime.id==='whisper.cpp'?bundledWhisperRuntime(runtime,relative):{...runtime,root:relative};
729
748
  for(const field of ['executable','modulePath','includeDirectory','libraryDirectory','binaryDirectory','cmakeDirectory','libraryPath','bindingModulePath']){
730
749
  if(runtime[field]===undefined)continue;
731
750
  bundled[field]=path.posix.join(relative,path.relative(runtime.root,runtime[field]).split(path.sep).join('/'));
@@ -0,0 +1,48 @@
1
+ import path from 'node:path';
2
+ import {fileURLToPath} from 'node:url';
3
+ import {mkdir} from 'node:fs/promises';
4
+ import Is from 'strong-type';
5
+ import {runProcess} from '../../process.mjs';
6
+
7
+ const is = new Is(false);
8
+
9
+ /** Build only the persistent first-party helper against an installed runtime. */
10
+ export async function buildWhisperHelper({
11
+ runtime,
12
+ outputRoot,
13
+ cmake = 'cmake',
14
+ generator,
15
+ cmakeArgs = [],
16
+ env,
17
+ signal,
18
+ onEvent
19
+ } = {}) {
20
+ if (!is.string(runtime?.directory) || !is.string(runtime?.sourceDirectory) || !is.string(outputRoot)) {
21
+ throw new TypeError('The selected Whisper runtime directory, matching sourceDirectory, and helper outputRoot are required.');
22
+ }
23
+ const root = path.resolve(outputRoot);
24
+ const build = path.join(root, 'build');
25
+ const source = fileURLToPath(new URL('./native/', import.meta.url));
26
+ await mkdir(root, {recursive: true});
27
+ const options = {env, signal, onEvent};
28
+ const configure = await runProcess(cmake, [
29
+ '-S', source,
30
+ '-B', build,
31
+ ...(generator ? ['-G', generator] : []),
32
+ `-DWHISPER_SOURCE_DIRECTORY=${path.resolve(runtime.sourceDirectory)}`,
33
+ `-DWHISPER_RUNTIME_DIRECTORY=${path.resolve(runtime.directory)}`,
34
+ `-DCMAKE_INSTALL_PREFIX=${root}`,
35
+ '-DCMAKE_BUILD_TYPE=Release',
36
+ ...cmakeArgs
37
+ ], options);
38
+ const compile = await runProcess(cmake, ['--build', build, '--config', 'Release'], options);
39
+ const install = await runProcess(cmake, ['--install', build, '--config', 'Release'], options);
40
+ return {
41
+ platform: process.platform,
42
+ architecture: process.arch,
43
+ root,
44
+ executable: path.join(root, 'bin', process.platform === 'win32' ? 'arcane-whisper.exe' : 'arcane-whisper'),
45
+ runtimeDirectory: path.resolve(runtime.directory),
46
+ diagnostics: {configure, compile, install}
47
+ };
48
+ }
@@ -0,0 +1,61 @@
1
+ import Is from 'strong-type';
2
+ import path from 'node:path';
3
+ import {ArcaneError, ERROR_CODES} from '../../errors.mjs';
4
+
5
+ const is = new Is(false);
6
+
7
+ export const WHISPER_SMALL_MODEL = {
8
+ id: 'whisper-small',
9
+ url: 'https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-small.bin',
10
+ filename: 'ggml-small.bin'
11
+ };
12
+
13
+ /** Describe a selected native runtime and models without acquiring them. */
14
+ export function normalizeWhisperRuntimeRequirement(record) {
15
+ if (record.helperRoot !== undefined && (!is.string(record.helperRoot) || !record.helperRoot)) {
16
+ throw new ArcaneError(ERROR_CODES.usage, 'Whisper helperRoot must name the complete selected helper directory.');
17
+ }
18
+ const helperRoot = record.helperRoot ?? (is.string(record.helperExecutable) ? path.dirname(record.helperExecutable) : undefined);
19
+ const backend = record.backend ?? 'auto';
20
+ if (!['auto', 'cuda', 'metal', 'cpu'].includes(backend)) {
21
+ throw new ArcaneError(ERROR_CODES.usage, `Unknown whisper.cpp backend: ${String(backend)}.`);
22
+ }
23
+ if (record.models !== undefined && !is.array(record.models)) {
24
+ throw new ArcaneError(ERROR_CODES.usage, 'whisper.cpp models must be an array of selected model IDs or descriptors.');
25
+ }
26
+ const models = (record.models ?? []).map(function selectedWhisperModel(value) {
27
+ const model = value === 'whisper-small' ? WHISPER_SMALL_MODEL : value;
28
+ if (!model || !is.object(model) || is.array(model) || !is.string(model.id) || !model.id) {
29
+ throw new ArcaneError(ERROR_CODES.usage, 'A Whisper model needs a descriptor with an id, or the built-in whisper-small model ID.');
30
+ }
31
+ if ((!is.string(model.path) || !model.path) && (!is.string(model.url) || !model.url)) {
32
+ throw new ArcaneError(ERROR_CODES.usage, `Whisper model ${model.id} needs its selected path or URL.`);
33
+ }
34
+ return {...model};
35
+ });
36
+ if (record.variants !== undefined && (!is.array(record.variants) || record.variants.length === 0)) {
37
+ throw new ArcaneError(ERROR_CODES.usage, 'whisper.cpp variants must be a nonempty array when supplied.');
38
+ }
39
+ const variants = record.variants?.map(function selectedWhisperVariant(variant) {
40
+ if (!variant || !is.object(variant) || is.array(variant) || !['cuda', 'metal', 'cpu'].includes(variant.backend)) {
41
+ throw new ArcaneError(ERROR_CODES.usage, 'Each Whisper runtime variant needs its cuda, metal, or cpu backend.');
42
+ }
43
+ if ((!is.string(variant.root) || !variant.root) && (!is.string(variant.url) || !variant.url)) {
44
+ throw new ArcaneError(ERROR_CODES.usage, `Whisper ${variant.backend} needs its complete runtime root or archive URL.`);
45
+ }
46
+ return {...variant};
47
+ });
48
+ if (record.modelId !== undefined && (!is.string(record.modelId) || !models.some(function matchingModel(model) {
49
+ return model.id === record.modelId;
50
+ }))) {
51
+ throw new ArcaneError(ERROR_CODES.usage, 'Whisper modelId must name one of the explicitly selected models.');
52
+ }
53
+ return {
54
+ ...record,
55
+ version: record.version ?? '1.9.4',
56
+ backend,
57
+ models,
58
+ ...(helperRoot === undefined ? {} : {helperRoot}),
59
+ ...(variants ? {variants} : {})
60
+ };
61
+ }