arcane-os 0.1.0-dev.5
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 +154 -0
- package/COMMERCIAL-LICENSE.md +13 -0
- package/LICENSE +661 -0
- package/NOTICE +70 -0
- package/README.md +448 -0
- package/bin/arcane-test.mjs +741 -0
- package/bin/arcane.mjs +5 -0
- package/docs/architecture.md +234 -0
- package/docs/compatibility.md +36 -0
- package/docs/event-manager.md +166 -0
- package/docs/platform-targets.md +108 -0
- package/docs/publishing.md +203 -0
- package/docs/reference/README.md +117 -0
- package/docs/reference/arcane-ollama.md +288 -0
- package/docs/reference/availability-and-normalization.md +123 -0
- package/docs/reference/behavioral-testing.md +86 -0
- package/docs/reference/cli.md +569 -0
- package/docs/reference/core/README.md +62 -0
- package/docs/reference/core/arcane-ai-contracts.md +873 -0
- package/docs/reference/core/arcane-api.md +601 -0
- package/docs/reference/core/arcane-entities.md +65 -0
- package/docs/reference/core/arcane-events.md +134 -0
- package/docs/reference/core/ollama-module.md +181 -0
- package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +1909 -0
- package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +1057 -0
- package/docs/reference/core/reference/arcane-api/core-and-events.md +320 -0
- package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +610 -0
- package/docs/reference/core/reference/arcane-api/namespaces.md +1157 -0
- package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +1423 -0
- package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +315 -0
- package/docs/reference/event-manager.md +957 -0
- package/docs/reference/inventory/package-api.json +2632 -0
- package/docs/reference/inventory/runtime-components.json +934 -0
- package/docs/reference/inventory/runtime-entities.json +26 -0
- package/docs/reference/inventory/runtime-modules.json +1249 -0
- package/docs/reference/protocols.md +242 -0
- package/docs/reference/runtime-components.md +1098 -0
- package/docs/reference/runtime-entities.md +303 -0
- package/docs/reference/runtime-modules.md +2010 -0
- package/docs/reference/sdk-api.md +4901 -0
- package/docs/roadmap.md +79 -0
- package/docs/work-amplification.md +124 -0
- package/node_modules/event-pubsub/CHANGELOG.md +55 -0
- package/node_modules/event-pubsub/MIGRATION.md +70 -0
- package/node_modules/event-pubsub/README.md +363 -0
- package/node_modules/event-pubsub/SECURITY.md +37 -0
- package/node_modules/event-pubsub/index.js +141 -0
- package/node_modules/event-pubsub/licence +21 -0
- package/node_modules/event-pubsub/package.json +59 -0
- package/node_modules/strong-type/README.md +408 -0
- package/node_modules/strong-type/assets/strong-type-header.png +0 -0
- package/node_modules/strong-type/index.js +1151 -0
- package/node_modules/strong-type/licence +21 -0
- package/node_modules/strong-type/node.js +125 -0
- package/node_modules/strong-type/package.json +61 -0
- package/package.json +95 -0
- package/runtime/ARCANE_RUNTIME_RELEASE.json +791 -0
- package/runtime/arcane/components/app-bar.html +468 -0
- package/runtime/arcane/components/assistant-panel.html +715 -0
- package/runtime/arcane/components/calculator.html +8 -0
- package/runtime/arcane/components/chart.html +655 -0
- package/runtime/arcane/components/chat.html +1225 -0
- package/runtime/arcane/components/conversation-view.html +13 -0
- package/runtime/arcane/components/dashboard-config.html +341 -0
- package/runtime/arcane/components/data-maintenance.html +112 -0
- package/runtime/arcane/components/data-view.html +92 -0
- package/runtime/arcane/components/directory-picker.html +197 -0
- package/runtime/arcane/components/document-inspector.html +252 -0
- package/runtime/arcane/components/file-drop.html +264 -0
- package/runtime/arcane/components/file-inspector.html +293 -0
- package/runtime/arcane/components/file-manager.html +1715 -0
- package/runtime/arcane/components/header.html +142 -0
- package/runtime/arcane/components/integration-settings.html +14 -0
- package/runtime/arcane/components/local-ai-status.html +360 -0
- package/runtime/arcane/components/markdown-document.html +1048 -0
- package/runtime/arcane/components/markdown-editor.html +360 -0
- package/runtime/arcane/components/media-embed.html +8 -0
- package/runtime/arcane/components/modal.html +402 -0
- package/runtime/arcane/components/output-panel.html +259 -0
- package/runtime/arcane/components/preferences-form.html +135 -0
- package/runtime/arcane/components/record-timeline.html +105 -0
- package/runtime/arcane/components/relationship-board.html +116 -0
- package/runtime/arcane/components/screen-capture.html +8 -0
- package/runtime/arcane/components/source-code-viewer.html +441 -0
- package/runtime/arcane/components/source-explanation.html +124 -0
- package/runtime/arcane/components/speech.html +365 -0
- package/runtime/arcane/components/summary-strip.html +177 -0
- package/runtime/arcane/components/table.html +77 -0
- package/runtime/arcane/components/task-progress.html +282 -0
- package/runtime/arcane/components/terminal-workspace.html +65 -0
- package/runtime/arcane/components/theme-editor.html +41 -0
- package/runtime/arcane/components/theme-switcher.html +46 -0
- package/runtime/arcane/components/unified-inbox.html +20 -0
- package/runtime/arcane/components/voice-transcription.html +476 -0
- package/runtime/arcane/components/weather-widget.html +8 -0
- package/runtime/arcane/components/web-navigator.html +239 -0
- package/runtime/arcane/css/communications.css +1 -0
- package/runtime/arcane/css/dashboard-config.css +45 -0
- package/runtime/arcane/css/document-site.css +981 -0
- package/runtime/arcane/css/layout.css +438 -0
- package/runtime/arcane/css/primitives.css +321 -0
- package/runtime/arcane/css/theme.css +112 -0
- package/runtime/arcane/css/utility-workspace.css +1 -0
- package/runtime/arcane/entities/ApiModelRecord.js +20 -0
- package/runtime/arcane/entities/Calculation.js +13 -0
- package/runtime/arcane/entities/Chat.js +581 -0
- package/runtime/arcane/entities/CommunicationMessage.js +29 -0
- package/runtime/arcane/entities/CommunicationThread.js +21 -0
- package/runtime/arcane/entities/Document.js +10 -0
- package/runtime/arcane/entities/File.js +143 -0
- package/runtime/arcane/entities/Image.js +135 -0
- package/runtime/arcane/entities/IntentEnvelope.js +834 -0
- package/runtime/arcane/entities/Preference.js +83 -0
- package/runtime/arcane/entities/TWiNPolicyDecision.js +1092 -0
- package/runtime/arcane/entities/TerminalSession.js +46 -0
- package/runtime/arcane/entities/Theme.js +107 -0
- package/runtime/arcane/entities/User.js +1046 -0
- package/runtime/arcane/entities/Weather.js +23 -0
- package/runtime/arcane/img/arcane-os-everywhere.png +0 -0
- package/runtime/arcane/img/arrow-left.png +0 -0
- package/runtime/arcane/img/arrow-right.png +0 -0
- package/runtime/arcane/img/doc.svg +5 -0
- package/runtime/arcane/img/folder.svg +4 -0
- package/runtime/arcane/img/image.svg +5 -0
- package/runtime/arcane/img/refresh.png +0 -0
- package/runtime/arcane/img/send.svg +9 -0
- package/runtime/arcane/img/trash.svg +5 -0
- package/runtime/arcane/img/upload.svg +5 -0
- package/runtime/arcane/modules/AI.js +2048 -0
- package/runtime/arcane/modules/AIPreferenceRuntime.js +32 -0
- package/runtime/arcane/modules/AIPreferenceTuple.js +92 -0
- package/runtime/arcane/modules/AIResponseLength.js +42 -0
- package/runtime/arcane/modules/AIResponseURLPolicy.js +626 -0
- package/runtime/arcane/modules/AnsiText.js +53 -0
- package/runtime/arcane/modules/ApiModelDatabase.js +25 -0
- package/runtime/arcane/modules/AppDataScope.js +245 -0
- package/runtime/arcane/modules/AppearancePreferences.js +28 -0
- package/runtime/arcane/modules/ArcaneCommunicationBridge.js +22 -0
- package/runtime/arcane/modules/ArcaneNavigationPolicy.js +135 -0
- package/runtime/arcane/modules/ArcaneNetworkPolicy.js +255 -0
- package/runtime/arcane/modules/AsyncBoundary.js +161 -0
- package/runtime/arcane/modules/BrowserTestSuite.js +326 -0
- package/runtime/arcane/modules/CalculatorEngine.js +20 -0
- package/runtime/arcane/modules/CaseEvidenceIndexer.js +134 -0
- package/runtime/arcane/modules/ChartLibrary.js +35 -0
- package/runtime/arcane/modules/ChatRecords.js +13 -0
- package/runtime/arcane/modules/CommunicationAppController.js +43 -0
- package/runtime/arcane/modules/CommunicationHub.js +16 -0
- package/runtime/arcane/modules/CommunicationPreferences.js +13 -0
- package/runtime/arcane/modules/CommunicationProviderRegistry.js +16 -0
- package/runtime/arcane/modules/ComponentContracts.js +586 -0
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +288 -0
- package/runtime/arcane/modules/ConversationActionItems.js +488 -0
- package/runtime/arcane/modules/ConversationClosingReport.js +274 -0
- package/runtime/arcane/modules/ConversationTimebox.js +527 -0
- package/runtime/arcane/modules/CoreLocalModelCatalog.js +255 -0
- package/runtime/arcane/modules/DBLS.js +171 -0
- package/runtime/arcane/modules/DBOPFS.js +1154 -0
- package/runtime/arcane/modules/DBOPFSWorker.js +116 -0
- package/runtime/arcane/modules/DataMaintenance.js +91 -0
- package/runtime/arcane/modules/DevelopmentWorkspace.js +74 -0
- package/runtime/arcane/modules/DirectoryPicker.js +109 -0
- package/runtime/arcane/modules/DocumentNavigation.js +223 -0
- package/runtime/arcane/modules/Errors.js +1025 -0
- package/runtime/arcane/modules/GifEncoder.js +29 -0
- package/runtime/arcane/modules/HTMLImport.js +114 -0
- package/runtime/arcane/modules/InMemoryCommunicationProvider.js +14 -0
- package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +275 -0
- package/runtime/arcane/modules/LocalAIReadiness.js +870 -0
- package/runtime/arcane/modules/LocalAIReadinessController.js +156 -0
- package/runtime/arcane/modules/MD.js +111 -0
- package/runtime/arcane/modules/Mail.js +352 -0
- package/runtime/arcane/modules/MailTransport.mjs +180 -0
- package/runtime/arcane/modules/Marked.min.js +71 -0
- package/runtime/arcane/modules/MemoryRecords.js +44 -0
- package/runtime/arcane/modules/MessageAdvisory.js +38 -0
- package/runtime/arcane/modules/ModelDefinition.js +189 -0
- package/runtime/arcane/modules/Ollama.js +74 -0
- package/runtime/arcane/modules/OllamaModelIdentifier.js +21 -0
- package/runtime/arcane/modules/OllamaSettings.js +24 -0
- package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +14 -0
- package/runtime/arcane/modules/PreferenceStore.js +109 -0
- package/runtime/arcane/modules/QRCode.min.js +1 -0
- package/runtime/arcane/modules/Questionnaire.js +61 -0
- package/runtime/arcane/modules/RecordLinkIndex.js +24 -0
- package/runtime/arcane/modules/RecordPassageIndex.js +222 -0
- package/runtime/arcane/modules/RecordReviewStore.js +94 -0
- package/runtime/arcane/modules/RevocableProjectionLedger.js +1623 -0
- package/runtime/arcane/modules/RiskSignalAnalyzer.js +37 -0
- package/runtime/arcane/modules/ScamRiskPolicy.js +62 -0
- package/runtime/arcane/modules/ScopedOPFSCache.js +183 -0
- package/runtime/arcane/modules/ScreenCapture.js +20 -0
- package/runtime/arcane/modules/SpeechPlayback.js +581 -0
- package/runtime/arcane/modules/StaticDocumentCatalog.js +1248 -0
- package/runtime/arcane/modules/SystemAppearance.js +20 -0
- package/runtime/arcane/modules/SystemPlatformPresentation.js +51 -0
- package/runtime/arcane/modules/SystemToolRegistry.js +28 -0
- package/runtime/arcane/modules/TerminalClient.js +52 -0
- package/runtime/arcane/modules/TerminalCommandRegistry.js +50 -0
- package/runtime/arcane/modules/ThemeBootstrap.js +26 -0
- package/runtime/arcane/modules/ThemeManager.js +131 -0
- package/runtime/arcane/modules/TimeGuard.js +149 -0
- package/runtime/arcane/modules/ToolCallRouter.js +83 -0
- package/runtime/arcane/modules/WaitForComponent.js +102 -0
- package/runtime/arcane/modules/YouTubeMedia.js +16 -0
- package/runtime/arcane/modules/uPlot.LICENSE.txt +21 -0
- package/runtime/arcane/modules/uPlot.iife.min.js +2 -0
- package/runtime/arcane/modules/uPlot.min.css +1 -0
- package/runtime/arcane/security/arcane-network-policy.json +6 -0
- package/runtime/strong-type/index.js +352 -0
- package/runtime/strong-type/licence +21 -0
- package/runtime/strong-type/package.json +45 -0
- package/schemas/arcane-app-bundle.schema.json +125 -0
- package/schemas/arcane-app.schema.json +329 -0
- package/schemas/arcane-lock.schema.json +86 -0
- package/schemas/arcane-package.schema.json +224 -0
- package/schemas/cli-event.schema.json +122 -0
- package/schemas/event-stack.schema.json +152 -0
- package/schemas/native-build-plan.schema.json +176 -0
- package/schemas/target-adapter.schema.json +117 -0
- package/src/app-descriptor.mjs +500 -0
- package/src/cli/main.mjs +561 -0
- package/src/constants.mjs +31 -0
- package/src/dev-server.mjs +718 -0
- package/src/doctor.mjs +315 -0
- package/src/dom-event-instrumentation.mjs +594 -0
- package/src/errors.mjs +75 -0
- package/src/event-manager.mjs +1342 -0
- package/src/event-queue.mjs +138 -0
- package/src/events.mjs +219 -0
- package/src/index.mjs +177 -0
- package/src/integrated-provider-loader.mjs +432 -0
- package/src/native-plan.mjs +698 -0
- package/src/native-provider-loader.mjs +1126 -0
- package/src/packager/core.mjs +2691 -0
- package/src/process.mjs +353 -0
- package/src/release-bundle.mjs +2523 -0
- package/src/repository.mjs +90 -0
- package/src/runtime.mjs +452 -0
- package/src/scaffold.mjs +380 -0
- package/src/targets/index.mjs +436 -0
- package/src/templates/assets/app-icon.png +0 -0
- package/src/templates/workspace-template.mjs +388 -0
- package/src/testing-loader.mjs +9 -0
- package/src/testing.mjs +427 -0
- package/src/toolchain.mjs +1335 -0
- package/src/update-check.mjs +307 -0
- package/src/workspace.mjs +449 -0
|
@@ -0,0 +1,870 @@
|
|
|
1
|
+
import {AI_PREFERENCE_SLOT_KEYS} from './AIPreferenceTuple.js';
|
|
2
|
+
import {
|
|
3
|
+
getCoreLocalModelCatalog,
|
|
4
|
+
isUserManagedLoopbackLocalAIStatus,
|
|
5
|
+
USER_MANAGED_LOOPBACK_PROVIDER_MODE
|
|
6
|
+
} from './CoreLocalModelCatalog.js';
|
|
7
|
+
import {normalizeOllamaModelIdentifier} from './OllamaModelIdentifier.js';
|
|
8
|
+
|
|
9
|
+
export const LOCAL_AI_BROWSER_ENDPOINTS=Object.freeze({
|
|
10
|
+
speech:'http://127.0.0.1:8011/health'
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const DEFAULT_TIMEOUT_MS=3000;
|
|
14
|
+
const MAX_TIMEOUT_MS=10000;
|
|
15
|
+
const MAX_HEALTH_RESPONSE_CHARACTERS=64*1024;
|
|
16
|
+
const LOCAL_SPEECH_PROVIDERS=new Set([
|
|
17
|
+
'LOCAL',
|
|
18
|
+
'LOCAL_SPEACH',
|
|
19
|
+
'LOCAL_SPEECH'
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
function token(value,{uppercase=false}={}){
|
|
23
|
+
const normalized=typeof value==='string'
|
|
24
|
+
?value.trim()
|
|
25
|
+
:'';
|
|
26
|
+
|
|
27
|
+
return uppercase
|
|
28
|
+
?normalized.toUpperCase()
|
|
29
|
+
:normalized;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function errorCode(error,fallback){
|
|
33
|
+
const code=token(error?.code,{uppercase:true});
|
|
34
|
+
return code&&code.length<=128
|
|
35
|
+
?code
|
|
36
|
+
:fallback;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function freeze(value){
|
|
40
|
+
if(!value||typeof value!=='object'||Object.isFrozen(value)){
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for(const child of Object.values(value)){
|
|
45
|
+
freeze(child);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return Object.freeze(value);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function positiveSafeInteger(value){
|
|
52
|
+
return Number.isSafeInteger(value)&&value>0
|
|
53
|
+
?value
|
|
54
|
+
:null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function preferenceSlot(preferences,index){
|
|
58
|
+
const value=preferences[index];
|
|
59
|
+
|
|
60
|
+
if(typeof value!=='string'||!value.trim()){
|
|
61
|
+
throw new TypeError(
|
|
62
|
+
'The AI preference for '
|
|
63
|
+
+AI_PREFERENCE_SLOT_KEYS[index]
|
|
64
|
+
+' must be a non-empty string.'
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return value.trim();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Maps Arcane's six-slot AI preference tuple onto the local services that the
|
|
73
|
+
* selected provider/model pairs require. This function does not alter the
|
|
74
|
+
* tuple or choose a replacement provider.
|
|
75
|
+
*/
|
|
76
|
+
export function deriveLocalAIRequirements(preferences){
|
|
77
|
+
if(!Array.isArray(preferences)||preferences.length!==AI_PREFERENCE_SLOT_KEYS.length){
|
|
78
|
+
throw new TypeError(
|
|
79
|
+
'AI preferences must contain exactly '
|
|
80
|
+
+AI_PREFERENCE_SLOT_KEYS.length
|
|
81
|
+
+' slots.'
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const llmProvider=preferenceSlot(preferences,0);
|
|
86
|
+
const sttProvider=preferenceSlot(preferences,1);
|
|
87
|
+
const ttsProvider=preferenceSlot(preferences,2);
|
|
88
|
+
const llmModel=preferenceSlot(preferences,3);
|
|
89
|
+
const ttsModel=preferenceSlot(preferences,4);
|
|
90
|
+
const sttModel=preferenceSlot(preferences,5);
|
|
91
|
+
|
|
92
|
+
return freeze({
|
|
93
|
+
llm:{
|
|
94
|
+
slot:'llm',
|
|
95
|
+
provider:llmProvider,
|
|
96
|
+
model:llmModel,
|
|
97
|
+
required:token(llmProvider,{uppercase:true})==='OLLAMA',
|
|
98
|
+
service:'ollama'
|
|
99
|
+
},
|
|
100
|
+
stt:{
|
|
101
|
+
slot:'stt',
|
|
102
|
+
provider:sttProvider,
|
|
103
|
+
model:sttModel,
|
|
104
|
+
required:LOCAL_SPEECH_PROVIDERS.has(
|
|
105
|
+
token(sttProvider,{uppercase:true})
|
|
106
|
+
),
|
|
107
|
+
service:'speech'
|
|
108
|
+
},
|
|
109
|
+
tts:{
|
|
110
|
+
slot:'tts',
|
|
111
|
+
provider:ttsProvider,
|
|
112
|
+
model:ttsModel,
|
|
113
|
+
required:LOCAL_SPEECH_PROVIDERS.has(
|
|
114
|
+
token(ttsProvider,{uppercase:true})
|
|
115
|
+
),
|
|
116
|
+
service:'speech'
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function explicitHealthBoolean(status,names){
|
|
122
|
+
for(const name of names){
|
|
123
|
+
if(Object.prototype.hasOwnProperty.call(status,name)){
|
|
124
|
+
return status[name]===true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function healthBoolean(status,names){
|
|
132
|
+
return explicitHealthBoolean(status,names)??status.ready===true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function healthText(status,names){
|
|
136
|
+
for(const name of names){
|
|
137
|
+
const value=token(status[name]);
|
|
138
|
+
|
|
139
|
+
if(value){
|
|
140
|
+
return value.slice(0,128);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Evaluates STT and TTS independently. Raw browser health responses use
|
|
149
|
+
* snake_case fields; Arcane Core responses may use camelCase or their bounded
|
|
150
|
+
* aggregate ready proof.
|
|
151
|
+
*/
|
|
152
|
+
export function evaluateLocalSpeechHealth(status){
|
|
153
|
+
if(!status||typeof status!=='object'||Array.isArray(status)){
|
|
154
|
+
return freeze({
|
|
155
|
+
reachable:false,
|
|
156
|
+
ready:false,
|
|
157
|
+
status:'unavailable',
|
|
158
|
+
ttsEngine:null,
|
|
159
|
+
sttEngine:null,
|
|
160
|
+
stt:{
|
|
161
|
+
ready:false,
|
|
162
|
+
ffmpeg:false,
|
|
163
|
+
whisperCli:false,
|
|
164
|
+
whisperServer:false,
|
|
165
|
+
whisperModel:false
|
|
166
|
+
},
|
|
167
|
+
tts:{
|
|
168
|
+
ready:false,
|
|
169
|
+
kokoro:false,
|
|
170
|
+
voices:false
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const serviceStatus=healthText(status,['status'])||'unknown';
|
|
176
|
+
const statusReady=serviceStatus.toLowerCase()==='ok'
|
|
177
|
+
||(serviceStatus==='unknown'&&status.ready===true);
|
|
178
|
+
const ttsEngine=healthText(status,['ttsEngine','tts_engine']);
|
|
179
|
+
const sttEngine=healthText(status,['sttEngine','stt_engine']);
|
|
180
|
+
const ffmpeg=healthBoolean(status,['ffmpeg','ffmpegExists','ffmpeg_exists']);
|
|
181
|
+
const whisperCli=healthBoolean(
|
|
182
|
+
status,
|
|
183
|
+
['whisperCli','whisperCliExists','whisper_cli_exists']
|
|
184
|
+
);
|
|
185
|
+
const whisperServer=healthBoolean(
|
|
186
|
+
status,
|
|
187
|
+
['whisperServer','whisperServerExists','whisper_server_exists']
|
|
188
|
+
);
|
|
189
|
+
const whisperModel=healthBoolean(
|
|
190
|
+
status,
|
|
191
|
+
['whisperModel','whisperExists','whisper_exists']
|
|
192
|
+
);
|
|
193
|
+
const kokoro=healthBoolean(
|
|
194
|
+
status,
|
|
195
|
+
['kokoro','kokoroExists','kokoro_exists']
|
|
196
|
+
);
|
|
197
|
+
const voices=healthBoolean(
|
|
198
|
+
status,
|
|
199
|
+
['voices','voicesExists','voices_exists']
|
|
200
|
+
);
|
|
201
|
+
const transcriptionAvailable=explicitHealthBoolean(
|
|
202
|
+
status,
|
|
203
|
+
['transcriptionAvailable','transcription_available']
|
|
204
|
+
);
|
|
205
|
+
const synthesisAvailable=explicitHealthBoolean(
|
|
206
|
+
status,
|
|
207
|
+
['synthesisAvailable','synthesis_available']
|
|
208
|
+
);
|
|
209
|
+
const sttReady=statusReady
|
|
210
|
+
&&sttEngine?.toLowerCase()==='whisper.cpp'
|
|
211
|
+
&&(transcriptionAvailable??(
|
|
212
|
+
ffmpeg
|
|
213
|
+
&&whisperCli
|
|
214
|
+
&&whisperServer
|
|
215
|
+
&&whisperModel
|
|
216
|
+
));
|
|
217
|
+
const ttsReady=statusReady
|
|
218
|
+
&&ttsEngine?.toLowerCase()==='kokoro-onnx'
|
|
219
|
+
&&(synthesisAvailable??(kokoro&&voices));
|
|
220
|
+
|
|
221
|
+
return freeze({
|
|
222
|
+
reachable:true,
|
|
223
|
+
ready:sttReady&&ttsReady,
|
|
224
|
+
status:serviceStatus,
|
|
225
|
+
ttsEngine,
|
|
226
|
+
sttEngine,
|
|
227
|
+
stt:{
|
|
228
|
+
ready:Boolean(sttReady),
|
|
229
|
+
ffmpeg,
|
|
230
|
+
whisperCli,
|
|
231
|
+
whisperServer,
|
|
232
|
+
whisperModel
|
|
233
|
+
},
|
|
234
|
+
tts:{
|
|
235
|
+
ready:Boolean(ttsReady),
|
|
236
|
+
kokoro,
|
|
237
|
+
voices
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function normalizedTimeout(value){
|
|
243
|
+
if(value===undefined){
|
|
244
|
+
return DEFAULT_TIMEOUT_MS;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if(!Number.isSafeInteger(value)||value<1||value>MAX_TIMEOUT_MS){
|
|
248
|
+
throw new RangeError(
|
|
249
|
+
'Local AI readiness timeout must be between 1 and '
|
|
250
|
+
+MAX_TIMEOUT_MS
|
|
251
|
+
+' milliseconds.'
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return value;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function boundedFetchJSON(fetchImpl,url,timeoutMs){
|
|
259
|
+
if(typeof fetchImpl!=='function'){
|
|
260
|
+
const error=new Error('Browser fetch is unavailable.');
|
|
261
|
+
error.code='BROWSER_FETCH_UNAVAILABLE';
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const controller=typeof AbortController==='function'
|
|
266
|
+
?new AbortController()
|
|
267
|
+
:null;
|
|
268
|
+
let timer;
|
|
269
|
+
const timeout=new Promise((_resolve,reject)=>{
|
|
270
|
+
timer=setTimeout(()=>{
|
|
271
|
+
controller?.abort();
|
|
272
|
+
const error=new Error('The local readiness request timed out.');
|
|
273
|
+
error.code='LOCAL_AI_READINESS_TIMEOUT';
|
|
274
|
+
reject(error);
|
|
275
|
+
},timeoutMs);
|
|
276
|
+
});
|
|
277
|
+
const request=(async()=>{
|
|
278
|
+
const response=await fetchImpl(
|
|
279
|
+
url,
|
|
280
|
+
{
|
|
281
|
+
method:'GET',
|
|
282
|
+
credentials:'omit',
|
|
283
|
+
cache:'no-store',
|
|
284
|
+
...(controller?{signal:controller.signal}:{})
|
|
285
|
+
}
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
if(!response||response.ok!==true){
|
|
289
|
+
const error=new Error('The local readiness endpoint was not ready.');
|
|
290
|
+
error.code='LOCAL_AI_HEALTH_HTTP_ERROR';
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if(typeof response.text!=='function'){
|
|
295
|
+
const error=new Error('The local readiness endpoint returned an invalid response.');
|
|
296
|
+
error.code='LOCAL_AI_HEALTH_INVALID_RESPONSE';
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const text=await response.text();
|
|
301
|
+
|
|
302
|
+
if(
|
|
303
|
+
typeof text!=='string'
|
|
304
|
+
||!text
|
|
305
|
+
||text.length>MAX_HEALTH_RESPONSE_CHARACTERS
|
|
306
|
+
){
|
|
307
|
+
const error=new Error('The local readiness response was outside Arcane limits.');
|
|
308
|
+
error.code='LOCAL_AI_HEALTH_INVALID_RESPONSE';
|
|
309
|
+
throw error;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
let parsed;
|
|
313
|
+
|
|
314
|
+
try{
|
|
315
|
+
parsed=JSON.parse(text);
|
|
316
|
+
}catch{
|
|
317
|
+
const error=new Error('The local readiness endpoint returned invalid JSON.');
|
|
318
|
+
error.code='LOCAL_AI_HEALTH_INVALID_RESPONSE';
|
|
319
|
+
throw error;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if(!parsed||typeof parsed!=='object'||Array.isArray(parsed)){
|
|
323
|
+
const error=new Error('The local readiness endpoint returned an invalid record.');
|
|
324
|
+
error.code='LOCAL_AI_HEALTH_INVALID_RESPONSE';
|
|
325
|
+
throw error;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return parsed;
|
|
329
|
+
})();
|
|
330
|
+
|
|
331
|
+
try{
|
|
332
|
+
return await Promise.race([request,timeout]);
|
|
333
|
+
}finally{
|
|
334
|
+
clearTimeout(timer);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function probeNativeOllama(arcane,selectedModel){
|
|
339
|
+
if(typeof arcane?.localAI?.status!=='function'){
|
|
340
|
+
return freeze({
|
|
341
|
+
ready:false,
|
|
342
|
+
model:null,
|
|
343
|
+
errorCode:'ARCANE_LOCAL_AI_STATUS_UNAVAILABLE'
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const model=normalizeOllamaModelIdentifier(selectedModel);
|
|
348
|
+
|
|
349
|
+
if(!model){
|
|
350
|
+
return freeze({
|
|
351
|
+
ready:false,
|
|
352
|
+
model:null,
|
|
353
|
+
errorCode:'OLLAMA_MODEL_INVALID'
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
try{
|
|
358
|
+
const status=await arcane.localAI.status();
|
|
359
|
+
const activeParallelRequests=positiveSafeInteger(
|
|
360
|
+
status?.ollama?.activeParallelRequests
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
if(status?.ollama?.available===false){
|
|
364
|
+
return freeze({
|
|
365
|
+
ready:false,
|
|
366
|
+
model,
|
|
367
|
+
...(activeParallelRequests===null
|
|
368
|
+
?{}
|
|
369
|
+
:{activeParallelRequests}),
|
|
370
|
+
errorCode:errorCode(
|
|
371
|
+
{code:status.ollama.errorCode},
|
|
372
|
+
'OLLAMA_UNAVAILABLE'
|
|
373
|
+
)
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const catalog=getCoreLocalModelCatalog(status);
|
|
378
|
+
const available=catalog.some(function matchesSelectedModel(entry){
|
|
379
|
+
return entry.modelId===model;
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
return freeze({
|
|
383
|
+
ready:available,
|
|
384
|
+
model,
|
|
385
|
+
...(activeParallelRequests===null
|
|
386
|
+
?{}
|
|
387
|
+
:{activeParallelRequests}),
|
|
388
|
+
errorCode:available?null:'OLLAMA_MODEL_UNAVAILABLE'
|
|
389
|
+
});
|
|
390
|
+
}catch(error){
|
|
391
|
+
return freeze({
|
|
392
|
+
ready:false,
|
|
393
|
+
model,
|
|
394
|
+
errorCode:errorCode(error,'ARCANE_LOCAL_AI_STATUS_INVALID')
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async function probeUserManagedLoopbackOllama(arcane,selectedModel){
|
|
400
|
+
if(typeof arcane?.localAI?.status!=='function'){
|
|
401
|
+
return freeze({
|
|
402
|
+
ready:false,
|
|
403
|
+
model:null,
|
|
404
|
+
providerMode:USER_MANAGED_LOOPBACK_PROVIDER_MODE,
|
|
405
|
+
managed:false,
|
|
406
|
+
errorCode:'ARCANE_LOCAL_AI_STATUS_UNAVAILABLE'
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const model=normalizeOllamaModelIdentifier(selectedModel);
|
|
411
|
+
|
|
412
|
+
if(!model){
|
|
413
|
+
return freeze({
|
|
414
|
+
ready:false,
|
|
415
|
+
model:null,
|
|
416
|
+
providerMode:USER_MANAGED_LOOPBACK_PROVIDER_MODE,
|
|
417
|
+
managed:false,
|
|
418
|
+
errorCode:'OLLAMA_MODEL_INVALID'
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
try{
|
|
423
|
+
const status=await arcane.localAI.status();
|
|
424
|
+
|
|
425
|
+
if(!isUserManagedLoopbackLocalAIStatus(status)){
|
|
426
|
+
return freeze({
|
|
427
|
+
ready:false,
|
|
428
|
+
model,
|
|
429
|
+
providerMode:USER_MANAGED_LOOPBACK_PROVIDER_MODE,
|
|
430
|
+
managed:false,
|
|
431
|
+
errorCode:'ANDROID_USER_MANAGED_OLLAMA_UNAVAILABLE'
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if(status?.ollama?.available!==true){
|
|
436
|
+
return freeze({
|
|
437
|
+
ready:false,
|
|
438
|
+
model,
|
|
439
|
+
providerMode:USER_MANAGED_LOOPBACK_PROVIDER_MODE,
|
|
440
|
+
managed:false,
|
|
441
|
+
errorCode:errorCode(
|
|
442
|
+
{code:status?.ollama?.errorCode},
|
|
443
|
+
'OLLAMA_UNAVAILABLE'
|
|
444
|
+
)
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const catalog=getCoreLocalModelCatalog(status);
|
|
449
|
+
const available=catalog.some(function matchesSelectedModel(entry){
|
|
450
|
+
return entry.modelId===model;
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
return freeze({
|
|
454
|
+
ready:available,
|
|
455
|
+
model,
|
|
456
|
+
providerMode:USER_MANAGED_LOOPBACK_PROVIDER_MODE,
|
|
457
|
+
managed:false,
|
|
458
|
+
errorCode:available?null:'OLLAMA_MODEL_UNAVAILABLE'
|
|
459
|
+
});
|
|
460
|
+
}catch(error){
|
|
461
|
+
return freeze({
|
|
462
|
+
ready:false,
|
|
463
|
+
model,
|
|
464
|
+
providerMode:USER_MANAGED_LOOPBACK_PROVIDER_MODE,
|
|
465
|
+
managed:false,
|
|
466
|
+
errorCode:errorCode(error,'ANDROID_USER_MANAGED_OLLAMA_UNAVAILABLE')
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function unavailableBrowserOllama(){
|
|
472
|
+
return freeze({
|
|
473
|
+
ready:false,
|
|
474
|
+
model:null,
|
|
475
|
+
errorCode:'ARCANE_CORE_REQUIRED'
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async function probeNativeSpeech(arcane){
|
|
480
|
+
if(typeof arcane?.speech?.status!=='function'){
|
|
481
|
+
return freeze({
|
|
482
|
+
...evaluateLocalSpeechHealth(null),
|
|
483
|
+
errorCode:'ARCANE_SPEECH_UNAVAILABLE'
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
try{
|
|
488
|
+
return freeze({
|
|
489
|
+
...evaluateLocalSpeechHealth(await arcane.speech.status()),
|
|
490
|
+
errorCode:null
|
|
491
|
+
});
|
|
492
|
+
}catch(error){
|
|
493
|
+
return freeze({
|
|
494
|
+
...evaluateLocalSpeechHealth(null),
|
|
495
|
+
errorCode:errorCode(error,'LOCAL_SPEECH_UNAVAILABLE')
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async function probeBrowserSpeech(fetchImpl,timeoutMs){
|
|
501
|
+
try{
|
|
502
|
+
return freeze({
|
|
503
|
+
...evaluateLocalSpeechHealth(
|
|
504
|
+
await boundedFetchJSON(
|
|
505
|
+
fetchImpl,
|
|
506
|
+
LOCAL_AI_BROWSER_ENDPOINTS.speech,
|
|
507
|
+
timeoutMs
|
|
508
|
+
)
|
|
509
|
+
),
|
|
510
|
+
errorCode:null
|
|
511
|
+
});
|
|
512
|
+
}catch(error){
|
|
513
|
+
return freeze({
|
|
514
|
+
...evaluateLocalSpeechHealth(null),
|
|
515
|
+
errorCode:errorCode(error,'LOCAL_SPEECH_UNAVAILABLE')
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async function resolveMode(runtime,arcane){
|
|
521
|
+
if(
|
|
522
|
+
runtime==='native'
|
|
523
|
+
||runtime==='browser'
|
|
524
|
+
||runtime===USER_MANAGED_LOOPBACK_PROVIDER_MODE
|
|
525
|
+
){
|
|
526
|
+
return runtime;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if(runtime!==undefined&&(!runtime||typeof runtime!=='object')){
|
|
530
|
+
throw new TypeError(
|
|
531
|
+
'Local AI runtime must be "native", "browser", '
|
|
532
|
+
+'"user-managed-loopback", or a runtime snapshot.'
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
let snapshot=runtime;
|
|
537
|
+
|
|
538
|
+
if(snapshot===undefined&&typeof arcane?.runtime?.current==='function'){
|
|
539
|
+
try{
|
|
540
|
+
snapshot=await arcane.runtime.current();
|
|
541
|
+
}catch{
|
|
542
|
+
snapshot=null;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if(snapshot?.managedLocalAI===true){
|
|
547
|
+
return 'native';
|
|
548
|
+
}
|
|
549
|
+
if(
|
|
550
|
+
snapshot?.native===true
|
|
551
|
+
&&snapshot?.transport==='android-webview'
|
|
552
|
+
){
|
|
553
|
+
return USER_MANAGED_LOOPBACK_PROVIDER_MODE;
|
|
554
|
+
}
|
|
555
|
+
return 'browser';
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
async function probeRequiredServices({
|
|
559
|
+
requirements,
|
|
560
|
+
mode,
|
|
561
|
+
arcane,
|
|
562
|
+
fetchImpl,
|
|
563
|
+
timeoutMs
|
|
564
|
+
}){
|
|
565
|
+
const requiresSpeech=requirements.stt.required||requirements.tts.required;
|
|
566
|
+
const [ollama,speech]=await Promise.all([
|
|
567
|
+
requirements.llm.required
|
|
568
|
+
?mode==='native'
|
|
569
|
+
?probeNativeOllama(arcane,requirements.llm.model)
|
|
570
|
+
:mode===USER_MANAGED_LOOPBACK_PROVIDER_MODE
|
|
571
|
+
?probeUserManagedLoopbackOllama(
|
|
572
|
+
arcane,
|
|
573
|
+
requirements.llm.model
|
|
574
|
+
)
|
|
575
|
+
:unavailableBrowserOllama()
|
|
576
|
+
:null,
|
|
577
|
+
requiresSpeech
|
|
578
|
+
?mode==='native'
|
|
579
|
+
?probeNativeSpeech(arcane)
|
|
580
|
+
:mode===USER_MANAGED_LOOPBACK_PROVIDER_MODE
|
|
581
|
+
?probeNativeSpeech(arcane)
|
|
582
|
+
:probeBrowserSpeech(fetchImpl,timeoutMs)
|
|
583
|
+
:null
|
|
584
|
+
]);
|
|
585
|
+
|
|
586
|
+
return freeze({ollama,speech});
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function slotResults(requirements,services){
|
|
590
|
+
const activeParallelRequests=positiveSafeInteger(
|
|
591
|
+
services.ollama?.activeParallelRequests
|
|
592
|
+
);
|
|
593
|
+
|
|
594
|
+
return freeze({
|
|
595
|
+
llm:{
|
|
596
|
+
...requirements.llm,
|
|
597
|
+
...(activeParallelRequests===null
|
|
598
|
+
?{}
|
|
599
|
+
:{activeParallelRequests}),
|
|
600
|
+
ready:requirements.llm.required
|
|
601
|
+
?services.ollama?.ready===true
|
|
602
|
+
:null,
|
|
603
|
+
status:!requirements.llm.required
|
|
604
|
+
?'not-selected'
|
|
605
|
+
:services.ollama?.ready===true?'ready':'unavailable',
|
|
606
|
+
errorCode:requirements.llm.required&&services.ollama?.ready!==true
|
|
607
|
+
?services.ollama?.errorCode||'OLLAMA_UNAVAILABLE'
|
|
608
|
+
:null
|
|
609
|
+
},
|
|
610
|
+
stt:{
|
|
611
|
+
...requirements.stt,
|
|
612
|
+
ready:requirements.stt.required
|
|
613
|
+
?services.speech?.stt?.ready===true
|
|
614
|
+
:null,
|
|
615
|
+
status:!requirements.stt.required
|
|
616
|
+
?'not-selected'
|
|
617
|
+
:services.speech?.stt?.ready===true?'ready':'unavailable',
|
|
618
|
+
errorCode:requirements.stt.required&&services.speech?.stt?.ready!==true
|
|
619
|
+
?services.speech?.errorCode||'LOCAL_STT_NOT_READY'
|
|
620
|
+
:null
|
|
621
|
+
},
|
|
622
|
+
tts:{
|
|
623
|
+
...requirements.tts,
|
|
624
|
+
ready:requirements.tts.required
|
|
625
|
+
?services.speech?.tts?.ready===true
|
|
626
|
+
:null,
|
|
627
|
+
status:!requirements.tts.required
|
|
628
|
+
?'not-selected'
|
|
629
|
+
:services.speech?.tts?.ready===true?'ready':'unavailable',
|
|
630
|
+
errorCode:requirements.tts.required&&services.speech?.tts?.ready!==true
|
|
631
|
+
?services.speech?.errorCode||'LOCAL_TTS_NOT_READY'
|
|
632
|
+
:null
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function unavailableSlots(slots){
|
|
638
|
+
return ['llm','stt','tts'].filter(
|
|
639
|
+
slot=>slots[slot].required&&slots[slot].ready!==true
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function recoveryServices(slots){
|
|
644
|
+
const services=[];
|
|
645
|
+
|
|
646
|
+
if(
|
|
647
|
+
slots.llm.required
|
|
648
|
+
&&slots.llm.ready!==true
|
|
649
|
+
&&!['OLLAMA_MODEL_INVALID','OLLAMA_MODEL_UNAVAILABLE'].includes(
|
|
650
|
+
slots.llm.errorCode
|
|
651
|
+
)
|
|
652
|
+
){
|
|
653
|
+
services.push('ollama');
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if(
|
|
657
|
+
(slots.stt.required&&slots.stt.ready!==true)
|
|
658
|
+
||(slots.tts.required&&slots.tts.ready!==true)
|
|
659
|
+
){
|
|
660
|
+
services.push('speech');
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
return services;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function recoveryFailureService(error,failedServices){
|
|
667
|
+
const service=token(
|
|
668
|
+
error?.diagnosticDetails?.service?.id,
|
|
669
|
+
{uppercase:false}
|
|
670
|
+
).toLowerCase();
|
|
671
|
+
|
|
672
|
+
return failedServices.includes(service)
|
|
673
|
+
?service
|
|
674
|
+
:null;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function preserveRecoveryFailure(
|
|
678
|
+
services,
|
|
679
|
+
failedServices,
|
|
680
|
+
recoveryErrorCode,
|
|
681
|
+
failedService
|
|
682
|
+
){
|
|
683
|
+
if(!recoveryErrorCode){
|
|
684
|
+
return services;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const targets=failedService
|
|
688
|
+
?[failedService]
|
|
689
|
+
:failedServices.length===1
|
|
690
|
+
?failedServices
|
|
691
|
+
:[];
|
|
692
|
+
|
|
693
|
+
return freeze({
|
|
694
|
+
ollama:targets.includes('ollama')
|
|
695
|
+
&&services.ollama?.ready!==true
|
|
696
|
+
?freeze({...services.ollama,errorCode:recoveryErrorCode})
|
|
697
|
+
:services.ollama,
|
|
698
|
+
speech:targets.includes('speech')
|
|
699
|
+
&&(
|
|
700
|
+
services.speech?.stt?.ready!==true
|
|
701
|
+
||services.speech?.tts?.ready!==true
|
|
702
|
+
)
|
|
703
|
+
?freeze({...services.speech,errorCode:recoveryErrorCode})
|
|
704
|
+
:services.speech
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function browserManualInstruction(services){
|
|
709
|
+
if(services.includes('ollama')&&services.includes('speech')){
|
|
710
|
+
return 'Local model access requires Arcane Core. Manually start your local speech service if you still need it.';
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if(services.includes('ollama')){
|
|
714
|
+
return 'Local model access requires Arcane Core.';
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
return 'Manually start your local speech service.';
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function userManagedLoopbackInstruction(services){
|
|
721
|
+
if(services.includes('ollama')&&services.includes('speech')){
|
|
722
|
+
return 'Start your Ollama service in Android Linux Terminal, make the selected model available, and start the Arcane speech service on its configured loopback endpoint.';
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if(services.includes('ollama')){
|
|
726
|
+
return 'Start your Ollama service in Android Linux Terminal and make the selected model available.';
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return 'Start the Arcane speech service on its configured loopback endpoint.';
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function guidance(mode,slots){
|
|
733
|
+
const affectedSlots=unavailableSlots(slots);
|
|
734
|
+
|
|
735
|
+
if(!affectedSlots.length){
|
|
736
|
+
return null;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const services=recoveryServices(slots);
|
|
740
|
+
const profileSettings=affectedSlots.map(slot=>slot+'Provider');
|
|
741
|
+
const message=mode==='browser'
|
|
742
|
+
?browserManualInstruction(services)
|
|
743
|
+
+' Alternatively, switch the affected Profile setting to OpenAI and enter an OpenAI API/license key. Arcane will not switch providers automatically.'
|
|
744
|
+
:mode===USER_MANAGED_LOOPBACK_PROVIDER_MODE
|
|
745
|
+
?userManagedLoopbackInstruction(services)
|
|
746
|
+
+' Arcane does not install, start, repair, pull, or otherwise manage this service or its models. Alternatively, switch the affected Profile setting to OpenAI. Arcane will not switch providers automatically.'
|
|
747
|
+
:'Arcane could not recover the selected local AI service. Retry or review Local AI in Arcane Settings. Arcane will not switch providers automatically.';
|
|
748
|
+
|
|
749
|
+
return freeze({
|
|
750
|
+
mode,
|
|
751
|
+
affectedSlots,
|
|
752
|
+
services,
|
|
753
|
+
profileSettings,
|
|
754
|
+
message,
|
|
755
|
+
automaticProviderSwitch:false
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function nativeRecovery(arcane,recover){
|
|
760
|
+
if(recover!==undefined){
|
|
761
|
+
if(typeof recover!=='function'){
|
|
762
|
+
throw new TypeError('Local AI recovery must be a function.');
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
return recover;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
return typeof arcane?.localAI?.recover==='function'
|
|
769
|
+
?arcane.localAI.recover.bind(arcane.localAI)
|
|
770
|
+
:null;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function report({
|
|
774
|
+
mode,
|
|
775
|
+
requirements,
|
|
776
|
+
services,
|
|
777
|
+
recovery
|
|
778
|
+
}){
|
|
779
|
+
const slots=slotResults(requirements,services);
|
|
780
|
+
const unavailable=unavailableSlots(slots);
|
|
781
|
+
|
|
782
|
+
return freeze({
|
|
783
|
+
ready:unavailable.length===0,
|
|
784
|
+
mode,
|
|
785
|
+
requirements,
|
|
786
|
+
slots,
|
|
787
|
+
services,
|
|
788
|
+
unavailableSlots:unavailable,
|
|
789
|
+
recovery,
|
|
790
|
+
guidance:guidance(mode,slots)
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Checks only the local providers selected in the supplied preference tuple.
|
|
796
|
+
*
|
|
797
|
+
* Managed native Arcane apps use capability-gated bridge methods and may
|
|
798
|
+
* request one bounded recovery attempt before one re-probe. Android uses only
|
|
799
|
+
* admitted native status/chat and speech-status methods for user-managed
|
|
800
|
+
* loopback services and never invokes lifecycle recovery. Ordinary browsers
|
|
801
|
+
* never probe Ollama; browser speech health remains read-only and never invokes
|
|
802
|
+
* recovery.
|
|
803
|
+
*/
|
|
804
|
+
export async function checkLocalAIReadiness({
|
|
805
|
+
preferences,
|
|
806
|
+
runtime,
|
|
807
|
+
arcane=globalThis.Arcane,
|
|
808
|
+
fetchImpl=globalThis.fetch,
|
|
809
|
+
recover,
|
|
810
|
+
timeoutMs
|
|
811
|
+
}={}){
|
|
812
|
+
const requirements=deriveLocalAIRequirements(preferences);
|
|
813
|
+
const mode=await resolveMode(runtime,arcane);
|
|
814
|
+
const timeout=normalizedTimeout(timeoutMs);
|
|
815
|
+
let services=await probeRequiredServices({
|
|
816
|
+
requirements,
|
|
817
|
+
mode,
|
|
818
|
+
arcane,
|
|
819
|
+
fetchImpl,
|
|
820
|
+
timeoutMs:timeout
|
|
821
|
+
});
|
|
822
|
+
let slots=slotResults(requirements,services);
|
|
823
|
+
const failedServices=recoveryServices(slots);
|
|
824
|
+
const recoverNative=mode==='native'
|
|
825
|
+
?nativeRecovery(arcane,recover)
|
|
826
|
+
:null;
|
|
827
|
+
const recovery={
|
|
828
|
+
attempted:false,
|
|
829
|
+
services:[],
|
|
830
|
+
errorCode:null
|
|
831
|
+
};
|
|
832
|
+
let failedRecoveryService=null;
|
|
833
|
+
|
|
834
|
+
if(failedServices.length&&recoverNative){
|
|
835
|
+
recovery.attempted=true;
|
|
836
|
+
recovery.services=[...failedServices];
|
|
837
|
+
|
|
838
|
+
try{
|
|
839
|
+
await recoverNative({services:[...failedServices]});
|
|
840
|
+
}catch(error){
|
|
841
|
+
recovery.errorCode=errorCode(error,'LOCAL_AI_RECOVERY_FAILED');
|
|
842
|
+
failedRecoveryService=recoveryFailureService(
|
|
843
|
+
error,
|
|
844
|
+
failedServices
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
services=await probeRequiredServices({
|
|
849
|
+
requirements,
|
|
850
|
+
mode,
|
|
851
|
+
arcane,
|
|
852
|
+
fetchImpl,
|
|
853
|
+
timeoutMs:timeout
|
|
854
|
+
});
|
|
855
|
+
services=preserveRecoveryFailure(
|
|
856
|
+
services,
|
|
857
|
+
failedServices,
|
|
858
|
+
recovery.errorCode,
|
|
859
|
+
failedRecoveryService
|
|
860
|
+
);
|
|
861
|
+
slots=slotResults(requirements,services);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
return report({
|
|
865
|
+
mode,
|
|
866
|
+
requirements,
|
|
867
|
+
services,
|
|
868
|
+
recovery:freeze(recovery)
|
|
869
|
+
});
|
|
870
|
+
}
|