arcane-os 0.6.2 → 0.7.1
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/CHANGELOG.md +28 -0
- package/NOTICE +6 -11
- package/README.md +25 -10
- package/browser-runtime/ai/browser-device-settings.mjs +24 -20
- package/browser-runtime/ai/browser-speech-artifacts.mjs +86 -0
- package/browser-runtime/ai/browser-speech-providers.mjs +71 -7
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +15 -12
- package/browser-runtime/ai/browser-wllama-runtime.mjs +41 -2
- package/browser-runtime/ai/speech-worker-client.mjs +10 -2
- package/browser-runtime/ai/speech-worker-runtime.mjs +67 -3
- package/browser-runtime/ai/wllama/index.mjs +9 -9
- package/docs/architecture.md +2 -2
- package/docs/reference/ai/browser-speech.md +32 -15
- package/docs/reference/ai/browser-wasm.md +36 -28
- package/docs/reference/cli.md +22 -6
- package/docs/reference/inventory/runtime-components.json +4 -4
- package/docs/reference/protocols.md +12 -5
- package/docs/reference/runtime-components.md +32 -22
- package/docs/reference/runtime-modules.md +7 -1
- package/docs/reference/sdk-api.md +21 -15
- package/package.json +1 -1
- package/runtime/arcane/components/browser-ai-setup.html +199 -179
- package/runtime/arcane/components/speech.html +34 -3
- package/runtime/arcane/components/voice-transcription.html +30 -3
- package/runtime/arcane/modules/ComponentContracts.js +86 -15
- package/src/cli/main.mjs +14 -3
- package/src/dev-server.mjs +32 -3
|
@@ -42,15 +42,40 @@
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
.description,
|
|
45
|
-
.status{
|
|
46
|
-
margin:0;
|
|
45
|
+
.status {
|
|
46
|
+
margin: 0;
|
|
47
|
+
overflow-wrap: anywhere;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.transcription-feedback {
|
|
51
|
+
min-width: 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.stt-activation-progress {
|
|
55
|
+
display: block;
|
|
56
|
+
width: 100%;
|
|
57
|
+
height: .6rem;
|
|
58
|
+
margin-block-start: .7rem;
|
|
59
|
+
accent-color: var(--arcane-action, var(--primary-color));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.stt-activation-progress[hidden] {
|
|
63
|
+
display: none;
|
|
47
64
|
}
|
|
48
65
|
</style>
|
|
49
66
|
|
|
50
67
|
<section class="voice-transcription">
|
|
51
68
|
<p id="description" class="description"></p>
|
|
52
69
|
<section id="transcript" class="transcript"></section>
|
|
53
|
-
<
|
|
70
|
+
<div class="transcription-feedback">
|
|
71
|
+
<p id="status" class="status" role="status" aria-live="polite"></p>
|
|
72
|
+
<progress
|
|
73
|
+
id="sttActivationProgress"
|
|
74
|
+
class="stt-activation-progress"
|
|
75
|
+
aria-label="Transcription model activation progress"
|
|
76
|
+
hidden
|
|
77
|
+
></progress>
|
|
78
|
+
</div>
|
|
54
79
|
<section class="controls">
|
|
55
80
|
<button id="start" type="button" disabled></button>
|
|
56
81
|
<button id="sttActivation" type="button" hidden disabled>Start transcription</button>
|
|
@@ -104,6 +129,7 @@
|
|
|
104
129
|
const status=host.shadowRoot.querySelector('#status');
|
|
105
130
|
const startButton=host.shadowRoot.querySelector('#start');
|
|
106
131
|
const sttActivationButton=host.shadowRoot.querySelector('#sttActivation');
|
|
132
|
+
const sttActivationProgress = host.shadowRoot.querySelector('#sttActivationProgress');
|
|
107
133
|
const stopButton=host.shadowRoot.querySelector('#stop');
|
|
108
134
|
const completeButton=host.shadowRoot.querySelector('#complete');
|
|
109
135
|
const runtimeStateAbortController=new AbortController();
|
|
@@ -225,6 +251,7 @@
|
|
|
225
251
|
{
|
|
226
252
|
host,
|
|
227
253
|
button:sttActivationButton,
|
|
254
|
+
progress: sttActivationProgress,
|
|
228
255
|
onChange:renderState,
|
|
229
256
|
eventSource:events
|
|
230
257
|
}
|
|
@@ -407,27 +407,60 @@ function effectiveDashboardVisibility(definitions=[],visibility={}){
|
|
|
407
407
|
);
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
-
function formatAIRuntimeProgress(progress,fallback){
|
|
411
|
-
const phase=is.string(progress?.phase)&&progress.phase
|
|
412
|
-
?progress.phase
|
|
413
|
-
:fallback;
|
|
414
|
-
const
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
||!unit){
|
|
423
|
-
return phase;
|
|
410
|
+
function formatAIRuntimeProgress(progress, fallback) {
|
|
411
|
+
const phase = is.string(progress?.phase) && progress.phase
|
|
412
|
+
? progress.phase
|
|
413
|
+
: fallback;
|
|
414
|
+
const parts = [phase];
|
|
415
|
+
const message = is.string(progress?.message) && progress.message
|
|
416
|
+
? progress.message
|
|
417
|
+
: is.string(progress?.stage) && progress.stage
|
|
418
|
+
? progress.stage
|
|
419
|
+
: '';
|
|
420
|
+
if (message && message !== phase) {
|
|
421
|
+
parts.push(message);
|
|
424
422
|
}
|
|
425
|
-
|
|
423
|
+
const item = is.string(progress?.item) && progress.item
|
|
424
|
+
? progress.item
|
|
425
|
+
: is.string(progress?.file) && progress.file
|
|
426
|
+
? progress.file
|
|
427
|
+
: '';
|
|
428
|
+
if (item && item !== message) {
|
|
429
|
+
parts.push(item);
|
|
430
|
+
}
|
|
431
|
+
const completed = progress?.completed;
|
|
432
|
+
const total = progress?.total;
|
|
433
|
+
const unit = is.string(progress?.unit)
|
|
434
|
+
? progress.unit
|
|
435
|
+
: '';
|
|
436
|
+
if (is.finite(completed) && is.finite(total) && total > 0 && unit) {
|
|
437
|
+
parts.push(`${completed} of ${total} ${unit}`);
|
|
438
|
+
} else if (is.finite(completed) && completed >= 0 && unit === 'files') {
|
|
439
|
+
parts.push(`${completed} files complete`);
|
|
440
|
+
}
|
|
441
|
+
if (is.finite(progress?.elapsedMs) && progress.elapsedMs >= 0) {
|
|
442
|
+
const elapsedSeconds = Math.floor(progress.elapsedMs / 1000);
|
|
443
|
+
const hours = Math.floor(elapsedSeconds / 3600);
|
|
444
|
+
const minutes = Math.floor(elapsedSeconds % 3600 / 60);
|
|
445
|
+
const seconds = elapsedSeconds % 60;
|
|
446
|
+
parts.push(
|
|
447
|
+
hours > 0
|
|
448
|
+
? `${hours}h ${minutes}m elapsed`
|
|
449
|
+
: minutes > 0
|
|
450
|
+
? `${minutes}m ${String(seconds).padStart(2, '0')}s elapsed`
|
|
451
|
+
: `${seconds}s elapsed`
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
if (progress?.heartbeat === true) {
|
|
455
|
+
parts.push('Still working');
|
|
456
|
+
}
|
|
457
|
+
return parts.join(' · ');
|
|
426
458
|
}
|
|
427
459
|
|
|
428
460
|
function createSTTActivationController({
|
|
429
461
|
host,
|
|
430
462
|
button,
|
|
463
|
+
progress = null,
|
|
431
464
|
onChange,
|
|
432
465
|
EventClass=globalThis.CustomEvent,
|
|
433
466
|
eventSource=null
|
|
@@ -595,6 +628,40 @@ function createSTTActivationController({
|
|
|
595
628
|
&&['unloaded','loading','unloading','error'].includes(role.state);
|
|
596
629
|
}
|
|
597
630
|
|
|
631
|
+
function renderProgress() {
|
|
632
|
+
if (!progress) {
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
const active = !destroyed
|
|
636
|
+
&& (requestPending || ['loading', 'unloading'].includes(role?.state));
|
|
637
|
+
progress.hidden = !active;
|
|
638
|
+
if (!active) {
|
|
639
|
+
progress.removeAttribute('value');
|
|
640
|
+
progress.removeAttribute('max');
|
|
641
|
+
progress.removeAttribute('aria-valuetext');
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
const completed = role?.progress?.completed;
|
|
645
|
+
const total = role?.progress?.total;
|
|
646
|
+
const unit = role?.progress?.unit;
|
|
647
|
+
if (!is.finite(completed)
|
|
648
|
+
|| completed < 0
|
|
649
|
+
|| !is.finite(total)
|
|
650
|
+
|| total <= 0
|
|
651
|
+
|| !is.string(unit)
|
|
652
|
+
|| !unit) {
|
|
653
|
+
progress.removeAttribute('value');
|
|
654
|
+
progress.removeAttribute('max');
|
|
655
|
+
} else {
|
|
656
|
+
progress.max = total;
|
|
657
|
+
progress.value = completed;
|
|
658
|
+
}
|
|
659
|
+
progress.setAttribute(
|
|
660
|
+
'aria-valuetext',
|
|
661
|
+
status()
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
598
665
|
async function request(nextAction){
|
|
599
666
|
if(destroyed||requestPending||action()!==nextAction){
|
|
600
667
|
return false;
|
|
@@ -647,6 +714,7 @@ function createSTTActivationController({
|
|
|
647
714
|
||action()!==nextAction){
|
|
648
715
|
return false;
|
|
649
716
|
}
|
|
717
|
+
renderProgress();
|
|
650
718
|
onChange();
|
|
651
719
|
if(destroyed
|
|
652
720
|
||generation!==requestGeneration
|
|
@@ -715,6 +783,7 @@ function createSTTActivationController({
|
|
|
715
783
|
if(!destroyed&&generation===requestGeneration){
|
|
716
784
|
requestPending=false;
|
|
717
785
|
try{
|
|
786
|
+
renderProgress();
|
|
718
787
|
onChange();
|
|
719
788
|
}catch(error){
|
|
720
789
|
arcaneLogging.error('Unable to render STT activation state:',error);
|
|
@@ -749,6 +818,7 @@ function createSTTActivationController({
|
|
|
749
818
|
requestPending=false;
|
|
750
819
|
}
|
|
751
820
|
role=nextRole;
|
|
821
|
+
renderProgress();
|
|
752
822
|
}
|
|
753
823
|
|
|
754
824
|
function destroy(){
|
|
@@ -758,6 +828,7 @@ function createSTTActivationController({
|
|
|
758
828
|
destroyed=true;
|
|
759
829
|
requestGeneration+=1;
|
|
760
830
|
requestPending=false;
|
|
831
|
+
renderProgress();
|
|
761
832
|
try{
|
|
762
833
|
button.removeEventListener('click',activateSelectedSTT);
|
|
763
834
|
}catch(error){
|
package/src/cli/main.mjs
CHANGED
|
@@ -35,6 +35,7 @@ const VALUE_OPTIONS=new Set([
|
|
|
35
35
|
]);
|
|
36
36
|
const FLAG_OPTIONS=new Set([
|
|
37
37
|
'git',
|
|
38
|
+
'public',
|
|
38
39
|
'skip-tests',
|
|
39
40
|
'dry-run',
|
|
40
41
|
'require-local-ai',
|
|
@@ -60,7 +61,7 @@ Usage:
|
|
|
60
61
|
${CLI_NAME} upgrade [--workspace <directory>] [--app <id>]
|
|
61
62
|
${CLI_NAME} doctor [--workspace <directory>] [--arcane-root <directory>]
|
|
62
63
|
${CLI_NAME} import-map [--workspace <directory>] [--app <id>]
|
|
63
|
-
${CLI_NAME} dev [--app <id>] [--host
|
|
64
|
+
${CLI_NAME} dev [--app <id>] [--public] [--host <address>] [--port 8000] [--sdk-runtime-source <sdk-root>]
|
|
64
65
|
${CLI_NAME} test [--app <id>] [--scope app]
|
|
65
66
|
${CLI_NAME} test --scope shared --test-file <repo-relative.test.mjs>
|
|
66
67
|
${CLI_NAME} check [--app <id>] [--scope app] [--skip-tests]
|
|
@@ -83,6 +84,8 @@ Usage:
|
|
|
83
84
|
${CLI_NAME} mail serve --profile <profile> --from <address> --app <id> --origin <origin> [--allow-to <addresses>] [--app-key-stdin] [--host 127.0.0.1] [--port 8025] [--request-timeout <ms>]
|
|
84
85
|
|
|
85
86
|
Development:
|
|
87
|
+
--public Bind all IPv4 interfaces (0.0.0.0) and print network URLs.
|
|
88
|
+
--host <address> Override the bind address; takes precedence over --public.
|
|
86
89
|
--sdk-runtime-source <sdk-root> Dev-only live SDK checkout; omitted preserves the workspace runtime mode.
|
|
87
90
|
|
|
88
91
|
Global:
|
|
@@ -445,6 +448,9 @@ function operationOptions(command,parsed,cwd){
|
|
|
445
448
|
if(values['sdk-runtime-source']!==undefined&&command!=='dev'){
|
|
446
449
|
usage('--sdk-runtime-source is supported only by dev.');
|
|
447
450
|
}
|
|
451
|
+
if(flags.has('public')&&command!=='dev'){
|
|
452
|
+
usage('--public is supported only by dev.');
|
|
453
|
+
}
|
|
448
454
|
if(flags.has('overwrite')&&command!=='bundle'){
|
|
449
455
|
usage('--overwrite is supported only by bundle.');
|
|
450
456
|
}
|
|
@@ -506,7 +512,7 @@ function operationOptions(command,parsed,cwd){
|
|
|
506
512
|
noExtraPositionals(command,positionals);
|
|
507
513
|
return {
|
|
508
514
|
...common,
|
|
509
|
-
host:values.host??'127.0.0.1',
|
|
515
|
+
host:values.host??(flags.has('public')?'0.0.0.0':'127.0.0.1'),
|
|
510
516
|
port:readPort(values.port,8000),
|
|
511
517
|
...(values['sdk-runtime-source']===undefined?{}:{
|
|
512
518
|
sdkRuntimeSourceRoot:path.resolve(cwd,values['sdk-runtime-source'])
|
|
@@ -831,6 +837,7 @@ function serverSummary(result){
|
|
|
831
837
|
host:result.host,
|
|
832
838
|
port:result.port,
|
|
833
839
|
url:result.url,
|
|
840
|
+
...(result.networkUrls===undefined?{}:{networkUrls:result.networkUrls}),
|
|
834
841
|
...(result.callerAuthentication
|
|
835
842
|
?{callerAuthentication:result.callerAuthentication}
|
|
836
843
|
:{}),
|
|
@@ -841,7 +848,11 @@ function serverSummary(result){
|
|
|
841
848
|
}
|
|
842
849
|
|
|
843
850
|
async function waitForServer(result,signal,reporter){
|
|
844
|
-
|
|
851
|
+
const readyMessage=[
|
|
852
|
+
`Development server ready at ${result.url}`,
|
|
853
|
+
...(result.networkUrls??[]).map(function networkAddress(url){return `Network: ${url}`;})
|
|
854
|
+
].join('\n');
|
|
855
|
+
reporter.emit('server.ready',serverSummary(result),readyMessage);
|
|
845
856
|
if(result.lifecycle&&is.function(result.lifecycle.then)){
|
|
846
857
|
const abort=()=>{
|
|
847
858
|
void Promise.resolve().then(()=>result.close?.()).catch(()=>{});
|
package/src/dev-server.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import Is from 'strong-type';
|
|
|
2
2
|
import {constants as FS_CONSTANTS} from 'node:fs';
|
|
3
3
|
import {lstat,open,readFile,realpath} from 'node:fs/promises';
|
|
4
4
|
import http from 'node:http';
|
|
5
|
+
import os from 'node:os';
|
|
5
6
|
import path from 'node:path';
|
|
6
7
|
import {resolveWorkspace} from './workspace.mjs';
|
|
7
8
|
import {createEventQueue} from './event-queue.mjs';
|
|
@@ -489,6 +490,29 @@ function listen(server,{host,port,signal}){
|
|
|
489
490
|
});
|
|
490
491
|
}
|
|
491
492
|
|
|
493
|
+
function browserHostname(host){
|
|
494
|
+
return host.includes(':')?`[${host}]`:host;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function networkUrlsForAddress(address,startPath){
|
|
498
|
+
if(address.address==='127.0.0.1'||address.address==='::1')return [];
|
|
499
|
+
const allIPv4=address.address==='0.0.0.0';
|
|
500
|
+
const allInterfaces=address.address==='::';
|
|
501
|
+
const urls=new Set();
|
|
502
|
+
for(const entries of Object.values(os.networkInterfaces())){
|
|
503
|
+
for(const entry of entries??[]){
|
|
504
|
+
if(entry.internal)continue;
|
|
505
|
+
// Scoped IPv6 addresses need the receiving device's interface,
|
|
506
|
+
// so they cannot provide a portable browser link for another device.
|
|
507
|
+
if(entry.family==='IPv6'&&entry.scopeid)continue;
|
|
508
|
+
if(allIPv4&&entry.family!=='IPv4')continue;
|
|
509
|
+
if(!allIPv4&&!allInterfaces&&entry.address!==address.address)continue;
|
|
510
|
+
urls.add(`http://${browserHostname(entry.address)}:${address.port}${startPath}`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return [...urls];
|
|
514
|
+
}
|
|
515
|
+
|
|
492
516
|
async function startOwnedDevServer({
|
|
493
517
|
workspaceRoot=process.cwd(),
|
|
494
518
|
appId,
|
|
@@ -504,8 +528,8 @@ async function startOwnedDevServer({
|
|
|
504
528
|
if(sdkRuntimeSourceRoot!==undefined&&mode!=='source'){
|
|
505
529
|
fail('sdkRuntimeSourceRoot is supported only in source development mode.','ARCANE_USAGE');
|
|
506
530
|
}
|
|
507
|
-
if(
|
|
508
|
-
fail('Development server host must be a
|
|
531
|
+
if(!is.string(host)||!host.trim()){
|
|
532
|
+
fail('Development server host must be a nonempty string.','ARCANE_USAGE');
|
|
509
533
|
}
|
|
510
534
|
if(!is.integer(port)||port<0||port>65535)fail('port must be an integer from 0 through 65535.','ARCANE_USAGE');
|
|
511
535
|
const requestedRuntimeMode=mode==='source'&&sdkRuntimeSourceRoot!==undefined
|
|
@@ -631,7 +655,9 @@ async function startOwnedDevServer({
|
|
|
631
655
|
server.close();
|
|
632
656
|
fail('Development server did not expose a TCP address.');
|
|
633
657
|
}
|
|
634
|
-
const visibleHost=address.
|
|
658
|
+
const visibleHost=address.address==='0.0.0.0'||address.address==='::'
|
|
659
|
+
?'localhost'
|
|
660
|
+
:browserHostname(address.address);
|
|
635
661
|
const endpoint=new URL(`http://${visibleHost}:${address.port}`);
|
|
636
662
|
const origin=endpoint.origin;
|
|
637
663
|
const cleanUrl=`${origin}${routeSet.startPath}`;
|
|
@@ -728,17 +754,20 @@ async function startOwnedDevServer({
|
|
|
728
754
|
origin,
|
|
729
755
|
cleanUrl,
|
|
730
756
|
url,
|
|
757
|
+
networkUrls:[],
|
|
731
758
|
close,
|
|
732
759
|
closed:lifecycle,
|
|
733
760
|
lifecycle
|
|
734
761
|
};
|
|
735
762
|
try{
|
|
763
|
+
result.networkUrls=networkUrlsForAddress(address,routeSet.startPath);
|
|
736
764
|
await events.send({
|
|
737
765
|
type:'server.started',
|
|
738
766
|
mode,
|
|
739
767
|
host:result.host,
|
|
740
768
|
port:result.port,
|
|
741
769
|
url,
|
|
770
|
+
networkUrls:result.networkUrls,
|
|
742
771
|
appId:result.appId,
|
|
743
772
|
...(routeSet.runtime?{
|
|
744
773
|
runtimeMode:routeSet.runtime.mode,
|