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,581 @@
|
|
|
1
|
+
const MAX_SPEECH_INPUT=3900;
|
|
2
|
+
const MAX_SPEECH_CHUNKS=32;
|
|
3
|
+
const MAX_SPEECH_CHARACTERS=MAX_SPEECH_INPUT*MAX_SPEECH_CHUNKS;
|
|
4
|
+
const PREFERRED_STREAM_SEGMENT=700;
|
|
5
|
+
const SPEECH_VOICE_OPTIONS=Object.freeze([
|
|
6
|
+
Object.freeze({value:'alloy',label:'Alloy'}),
|
|
7
|
+
Object.freeze({value:'ash',label:'Ash'}),
|
|
8
|
+
Object.freeze({value:'ballad',label:'Ballad'}),
|
|
9
|
+
Object.freeze({value:'coral',label:'Coral'}),
|
|
10
|
+
Object.freeze({value:'echo',label:'Echo'}),
|
|
11
|
+
Object.freeze({value:'fable',label:'Fable'}),
|
|
12
|
+
Object.freeze({value:'nova',label:'Nova'}),
|
|
13
|
+
Object.freeze({value:'onyx',label:'Onyx'}),
|
|
14
|
+
Object.freeze({value:'sage',label:'Sage'}),
|
|
15
|
+
Object.freeze({value:'shimmer',label:'Shimmer'})
|
|
16
|
+
]);
|
|
17
|
+
const SUPERSEDED=Object.freeze({superseded:true});
|
|
18
|
+
const queuesBySpeechClient=new WeakMap();
|
|
19
|
+
|
|
20
|
+
class ReadonlyAliasSet{
|
|
21
|
+
#values;
|
|
22
|
+
|
|
23
|
+
constructor(values){
|
|
24
|
+
this.#values=new Set(values);
|
|
25
|
+
Object.freeze(this);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get size(){return this.#values.size;}
|
|
29
|
+
get [Symbol.toStringTag](){return 'Set';}
|
|
30
|
+
has(value){return this.#values.has(value);}
|
|
31
|
+
entries(){return this.#values.entries();}
|
|
32
|
+
keys(){return this.#values.keys();}
|
|
33
|
+
values(){return this.#values.values();}
|
|
34
|
+
[Symbol.iterator](){return this.values();}
|
|
35
|
+
|
|
36
|
+
forEach(callback,thisArgument){
|
|
37
|
+
for(const value of this.#values)callback.call(thisArgument,value,value,this);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const SPEECH_VOICE_ALIASES=new ReadonlyAliasSet(
|
|
42
|
+
SPEECH_VOICE_OPTIONS.map(function voiceAlias(option){return option.value;})
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
function speechLimitError(message='Speech text is too long for bounded playback. The full visual content remains available.'){
|
|
46
|
+
return new RangeError(message);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function speechBoundary(value,limit,minimum){
|
|
50
|
+
const candidates=[
|
|
51
|
+
value.lastIndexOf('\n\n',limit),
|
|
52
|
+
value.lastIndexOf('. ',limit),
|
|
53
|
+
value.lastIndexOf('? ',limit),
|
|
54
|
+
value.lastIndexOf('! ',limit),
|
|
55
|
+
value.lastIndexOf('; ',limit),
|
|
56
|
+
value.lastIndexOf(', ',limit),
|
|
57
|
+
value.lastIndexOf(' ',limit)
|
|
58
|
+
];
|
|
59
|
+
const boundary=Math.max(...candidates);
|
|
60
|
+
return boundary>=minimum
|
|
61
|
+
?Math.min(limit,boundary+(/^[.!?;,]$/.test(value[boundary]||'')?1:0))
|
|
62
|
+
:limit;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function splitSpeechText(
|
|
66
|
+
value='',
|
|
67
|
+
maximum=MAX_SPEECH_INPUT,
|
|
68
|
+
maximumChunks=MAX_SPEECH_CHUNKS,
|
|
69
|
+
{
|
|
70
|
+
tooLongMessage='Speech text is too long for bounded playback. The full visual content remains available.',
|
|
71
|
+
queueLimitMessage='Speech text exceeds the bounded playback queue. The full visual content remains available.'
|
|
72
|
+
}={}
|
|
73
|
+
){
|
|
74
|
+
const text=String(value??'').trim();
|
|
75
|
+
if(!text)return [];
|
|
76
|
+
if(!Number.isInteger(maximum)||maximum<100||maximum>4000){
|
|
77
|
+
throw new RangeError('Speech chunk size must be between 100 and 4,000 characters.');
|
|
78
|
+
}
|
|
79
|
+
if(!Number.isInteger(maximumChunks)||maximumChunks<1||maximumChunks>MAX_SPEECH_CHUNKS){
|
|
80
|
+
throw new RangeError(`Speech chunk count must be between 1 and ${MAX_SPEECH_CHUNKS}.`);
|
|
81
|
+
}
|
|
82
|
+
if(text.length>maximum*maximumChunks)throw speechLimitError(tooLongMessage);
|
|
83
|
+
const chunks=[];
|
|
84
|
+
let remaining=text;
|
|
85
|
+
while(remaining.length){
|
|
86
|
+
const slots=maximumChunks-chunks.length;
|
|
87
|
+
if(slots<1)throw speechLimitError(queueLimitMessage);
|
|
88
|
+
if(remaining.length<=maximum&&remaining.length<=PREFERRED_STREAM_SEGMENT){
|
|
89
|
+
chunks.push(remaining);
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
const minimum=Math.ceil(remaining.length/slots);
|
|
93
|
+
const preferred=chunks.length===0?PREFERRED_STREAM_SEGMENT:maximum;
|
|
94
|
+
const target=Math.min(maximum,Math.max(preferred,minimum));
|
|
95
|
+
if(remaining.length<=target){
|
|
96
|
+
chunks.push(remaining);
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
const boundary=speechBoundary(remaining,target,Math.max(minimum,Math.floor(target*0.55)));
|
|
100
|
+
chunks.push(remaining.slice(0,boundary).trim());
|
|
101
|
+
remaining=remaining.slice(boundary).trimStart();
|
|
102
|
+
}
|
|
103
|
+
return chunks;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function normalizeParts(parts,defaultVoice='alloy',defaultSpeed=1){
|
|
107
|
+
if(!Array.isArray(parts)||!parts.length){
|
|
108
|
+
throw new TypeError('Narration text cannot be blank. The full visual content remains available.');
|
|
109
|
+
}
|
|
110
|
+
if(parts.length>MAX_SPEECH_CHUNKS){
|
|
111
|
+
throw speechLimitError('Speech text exceeds the bounded playback queue. The full visual content remains available.');
|
|
112
|
+
}
|
|
113
|
+
return parts.map(function normalizePart(part){
|
|
114
|
+
const candidate=typeof part==='string'?{input:part}:part;
|
|
115
|
+
const input=String(candidate?.input??'').trim();
|
|
116
|
+
if(!input)throw new TypeError('Speech segments cannot be blank. The full visual content remains available.');
|
|
117
|
+
if(input.length>MAX_SPEECH_INPUT){
|
|
118
|
+
throw speechLimitError('A speech segment exceeds the local service limit. The full visual content remains available.');
|
|
119
|
+
}
|
|
120
|
+
const pauseAfterMs=Number(candidate?.pauseAfterMs??0);
|
|
121
|
+
if(!Number.isFinite(pauseAfterMs)||pauseAfterMs<0||pauseAfterMs>60_000){
|
|
122
|
+
throw new RangeError('Speech segment pauses must be between 0 and 60,000 milliseconds.');
|
|
123
|
+
}
|
|
124
|
+
const voice=String(candidate?.voice??defaultVoice).trim();
|
|
125
|
+
const speed=Number(candidate?.speed??defaultSpeed);
|
|
126
|
+
if(!voice)throw new TypeError('Every speech segment requires a voice.');
|
|
127
|
+
if(!Number.isFinite(speed)||speed<=0)throw new RangeError('Every speech segment requires a positive speech speed.');
|
|
128
|
+
return Object.freeze({
|
|
129
|
+
input,
|
|
130
|
+
pauseAfterMs,
|
|
131
|
+
role:String(candidate?.role||'narration'),
|
|
132
|
+
speed,
|
|
133
|
+
voice
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function base64Bytes(value=''){
|
|
139
|
+
const binary=globalThis.atob(String(value));
|
|
140
|
+
const bytes=new Uint8Array(binary.length);
|
|
141
|
+
for(let index=0;index<binary.length;index+=1)bytes[index]=binary.charCodeAt(index);
|
|
142
|
+
return bytes;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
class LatestSpeechQueue{
|
|
146
|
+
constructor(){
|
|
147
|
+
this.active=null;
|
|
148
|
+
this.pending=null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
get busy(){return Boolean(this.active);}
|
|
152
|
+
|
|
153
|
+
enqueue(execute,{speculative=false}={}){
|
|
154
|
+
let resolveJob;
|
|
155
|
+
let rejectJob;
|
|
156
|
+
const promise=new Promise(function captureJob(resolve,reject){
|
|
157
|
+
resolveJob=resolve;
|
|
158
|
+
rejectJob=reject;
|
|
159
|
+
});
|
|
160
|
+
const job={execute,resolve:resolveJob,reject:rejectJob,speculative:Boolean(speculative)};
|
|
161
|
+
if(this.active){
|
|
162
|
+
if(job.speculative&&this.pending&&!this.pending.speculative){
|
|
163
|
+
job.resolve(SUPERSEDED);
|
|
164
|
+
}else{
|
|
165
|
+
if(this.pending)this.pending.resolve(SUPERSEDED);
|
|
166
|
+
this.pending=job;
|
|
167
|
+
}
|
|
168
|
+
}else{
|
|
169
|
+
this.start(job);
|
|
170
|
+
}
|
|
171
|
+
return promise;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
start(job){
|
|
175
|
+
this.active=job;
|
|
176
|
+
Promise.resolve()
|
|
177
|
+
.then(job.execute)
|
|
178
|
+
.then(job.resolve,job.reject)
|
|
179
|
+
.finally(this.finish.bind(this,job));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
finish(job){
|
|
183
|
+
if(this.active!==job)return;
|
|
184
|
+
this.active=null;
|
|
185
|
+
const next=this.pending;
|
|
186
|
+
this.pending=null;
|
|
187
|
+
if(next)this.start(next);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function queueFor(speech){
|
|
192
|
+
if((typeof speech!=='object'||speech===null)&&typeof speech!=='function'){
|
|
193
|
+
throw new TypeError('A local speech client is required.');
|
|
194
|
+
}
|
|
195
|
+
let queue=queuesBySpeechClient.get(speech);
|
|
196
|
+
if(!queue){
|
|
197
|
+
queue=new LatestSpeechQueue();
|
|
198
|
+
queuesBySpeechClient.set(speech,queue);
|
|
199
|
+
}
|
|
200
|
+
return queue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const DEFAULT_MESSAGES=Object.freeze({
|
|
204
|
+
idle:'Speech is ready when visual content is available.',
|
|
205
|
+
queued:'Waiting for the current local speech request. The latest narration will begin next.',
|
|
206
|
+
preparing:function preparing({count}){return `Preparing ${count===1?'the narration':`${count} speech segments`}.`;},
|
|
207
|
+
ready:'Speech is ready.',
|
|
208
|
+
autoplayBlocked:'Speech is ready. Browser autoplay was blocked; press Play.',
|
|
209
|
+
playing:'Reading the visible content aloud.',
|
|
210
|
+
buffering:'Preparing the next speech segment.',
|
|
211
|
+
paused:'Narration paused.',
|
|
212
|
+
pausing:'Pausing between narration sections.',
|
|
213
|
+
stopped:'Speech stopped. Start narration again to replay.',
|
|
214
|
+
preparationStopped:'Speech preparation stopped.',
|
|
215
|
+
ended:'Narration complete.',
|
|
216
|
+
unavailable:'Arcane speech is unavailable. The full visual content remains available.',
|
|
217
|
+
blank:'Narration text cannot be blank. The full visual content remains available.',
|
|
218
|
+
invalidAudio:'Arcane returned invalid synthesized audio.',
|
|
219
|
+
playbackError:'The synthesized narration could not be played. The visual content remains available.',
|
|
220
|
+
fallbackError:'Speech is unavailable. The visual content remains available.'
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
class SpeechPlayback{
|
|
224
|
+
constructor({
|
|
225
|
+
audio,
|
|
226
|
+
speech=globalThis.Arcane?.speech,
|
|
227
|
+
onState=function noop(){},
|
|
228
|
+
createObjectURL,
|
|
229
|
+
revokeObjectURL,
|
|
230
|
+
delay,
|
|
231
|
+
messages={}
|
|
232
|
+
}={}){
|
|
233
|
+
if(!audio||typeof audio.addEventListener!=='function'){
|
|
234
|
+
throw new TypeError('SpeechPlayback requires an audio element.');
|
|
235
|
+
}
|
|
236
|
+
this.audio=audio;
|
|
237
|
+
this.speech=speech;
|
|
238
|
+
this.onState=onState;
|
|
239
|
+
this.createObjectURL=createObjectURL||function createAudioURL(blob){return URL.createObjectURL(blob);};
|
|
240
|
+
this.revokeObjectURL=revokeObjectURL||function revokeAudioURL(url){URL.revokeObjectURL(url);};
|
|
241
|
+
this.delay=delay||function playbackDelay(duration){
|
|
242
|
+
return new Promise(function wait(resolve){globalThis.setTimeout(resolve,duration);});
|
|
243
|
+
};
|
|
244
|
+
this.messages=Object.freeze({...DEFAULT_MESSAGES,...messages});
|
|
245
|
+
this.urls=[];
|
|
246
|
+
this.parts=[];
|
|
247
|
+
this.index=0;
|
|
248
|
+
this.generation=0;
|
|
249
|
+
this.pendingGenerations=new Set();
|
|
250
|
+
this.lookahead=null;
|
|
251
|
+
this.lookaheadError=null;
|
|
252
|
+
this.voice='alloy';
|
|
253
|
+
this.speed=1;
|
|
254
|
+
this.key='';
|
|
255
|
+
this.state='idle';
|
|
256
|
+
this.boundEnded=this.handleEnded.bind(this);
|
|
257
|
+
this.boundPlay=this.handlePlay.bind(this);
|
|
258
|
+
this.boundPause=this.handlePause.bind(this);
|
|
259
|
+
this.boundError=this.handleError.bind(this);
|
|
260
|
+
this.audio.addEventListener('ended',this.boundEnded);
|
|
261
|
+
this.audio.addEventListener('play',this.boundPlay);
|
|
262
|
+
this.audio.addEventListener('pause',this.boundPause);
|
|
263
|
+
this.audio.addEventListener('error',this.boundError);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
get synthesisInFlight(){return this.pendingGenerations.size>0;}
|
|
267
|
+
|
|
268
|
+
message(name,details={}){
|
|
269
|
+
const value=this.messages[name];
|
|
270
|
+
return String(typeof value==='function'?value(details):value||'');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
emit(state,message,key=this.key){
|
|
274
|
+
this.state=state;
|
|
275
|
+
this.onState({
|
|
276
|
+
state,
|
|
277
|
+
message,
|
|
278
|
+
key,
|
|
279
|
+
index:this.index,
|
|
280
|
+
total:this.parts.length||this.urls.filter(Boolean).length,
|
|
281
|
+
producing:this.synthesisInFlight,
|
|
282
|
+
buffered:this.urls.filter(Boolean).length,
|
|
283
|
+
hasAudio:this.hasAudio()
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
available(){return Boolean(this.speech&&typeof this.speech.synthesize==='function');}
|
|
288
|
+
|
|
289
|
+
hasAudio(key=this.key){return Boolean(this.urls.some(Boolean)&&(!key||key===this.key));}
|
|
290
|
+
|
|
291
|
+
releaseURLs(){
|
|
292
|
+
for(const url of this.urls){
|
|
293
|
+
if(url)this.revokeObjectURL(url);
|
|
294
|
+
}
|
|
295
|
+
this.urls=[];
|
|
296
|
+
this.parts=[];
|
|
297
|
+
this.lookahead=null;
|
|
298
|
+
this.lookaheadError=null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
releaseUrls(){this.releaseURLs();}
|
|
302
|
+
|
|
303
|
+
cancel(message=this.message('idle')){
|
|
304
|
+
const cancelledKey=this.key;
|
|
305
|
+
this.generation+=1;
|
|
306
|
+
this.audio.pause();
|
|
307
|
+
this.audio.removeAttribute?.('src');
|
|
308
|
+
this.audio.load?.();
|
|
309
|
+
this.audio.hidden=true;
|
|
310
|
+
this.releaseURLs();
|
|
311
|
+
this.index=0;
|
|
312
|
+
this.key='';
|
|
313
|
+
this.emit('idle',message,cancelledKey);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async synthesizeSegment(index,generation,announce=false){
|
|
317
|
+
const playback=this;
|
|
318
|
+
const token=Object.freeze({generation,index});
|
|
319
|
+
const queue=queueFor(this.speech);
|
|
320
|
+
this.pendingGenerations.add(token);
|
|
321
|
+
try{
|
|
322
|
+
return await queue.enqueue(async function synthesizeQueuedSegment(){
|
|
323
|
+
const part=playback.parts[index];
|
|
324
|
+
if(generation!==playback.generation||!part)return SUPERSEDED;
|
|
325
|
+
if(announce){
|
|
326
|
+
playback.emit('synthesizing',playback.message('preparing',{count:playback.parts.length}));
|
|
327
|
+
}
|
|
328
|
+
const response=await playback.speech.synthesize({
|
|
329
|
+
model:'kokoro',
|
|
330
|
+
input:part.input,
|
|
331
|
+
voice:part.voice,
|
|
332
|
+
responseFormat:'opus',
|
|
333
|
+
speed:part.speed
|
|
334
|
+
});
|
|
335
|
+
if(generation!==playback.generation)return SUPERSEDED;
|
|
336
|
+
if(!response||typeof response.audioBase64!=='string'||!response.audioBase64){
|
|
337
|
+
throw new TypeError(playback.message('invalidAudio'));
|
|
338
|
+
}
|
|
339
|
+
const contentType=typeof response.contentType==='string'&&response.contentType
|
|
340
|
+
?response.contentType
|
|
341
|
+
:'audio/ogg';
|
|
342
|
+
const url=playback.createObjectURL(new Blob([base64Bytes(response.audioBase64)],{type:contentType}));
|
|
343
|
+
if(generation!==playback.generation){
|
|
344
|
+
playback.revokeObjectURL(url);
|
|
345
|
+
return SUPERSEDED;
|
|
346
|
+
}
|
|
347
|
+
return {url};
|
|
348
|
+
},{speculative:!announce});
|
|
349
|
+
}finally{
|
|
350
|
+
this.pendingGenerations.delete(token);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
startLookahead(index,generation=this.generation){
|
|
355
|
+
if(
|
|
356
|
+
generation!==this.generation
|
|
357
|
+
||index<0
|
|
358
|
+
||index>=this.parts.length
|
|
359
|
+
||this.urls[index]
|
|
360
|
+
)return null;
|
|
361
|
+
if(
|
|
362
|
+
this.lookahead
|
|
363
|
+
&&this.lookahead.generation===generation
|
|
364
|
+
&&this.lookahead.index===index
|
|
365
|
+
)return this.lookahead;
|
|
366
|
+
|
|
367
|
+
const playback=this;
|
|
368
|
+
const record={generation,index,promise:null};
|
|
369
|
+
record.promise=this.synthesizeSegment(index,generation).then(
|
|
370
|
+
function storeSynthesizedSegment(outcome){
|
|
371
|
+
if(outcome===SUPERSEDED)return {ready:false,superseded:true};
|
|
372
|
+
if(generation!==playback.generation){
|
|
373
|
+
playback.revokeObjectURL(outcome.url);
|
|
374
|
+
return {ready:false,superseded:true};
|
|
375
|
+
}
|
|
376
|
+
playback.urls[index]=outcome.url;
|
|
377
|
+
return {ready:true};
|
|
378
|
+
},
|
|
379
|
+
function handleLookaheadFailure(error){
|
|
380
|
+
if(generation===playback.generation){
|
|
381
|
+
playback.lookaheadError={generation,index,error};
|
|
382
|
+
}
|
|
383
|
+
return {ready:false,error};
|
|
384
|
+
}
|
|
385
|
+
).finally(function clearSettledLookahead(){
|
|
386
|
+
if(playback.lookahead===record)playback.lookahead=null;
|
|
387
|
+
});
|
|
388
|
+
this.lookahead=record;
|
|
389
|
+
return record;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async waitForSegment(index,generation){
|
|
393
|
+
if(generation!==this.generation)return false;
|
|
394
|
+
if(this.urls[index])return true;
|
|
395
|
+
if(
|
|
396
|
+
this.lookaheadError
|
|
397
|
+
&&this.lookaheadError.generation===generation
|
|
398
|
+
&&this.lookaheadError.index===index
|
|
399
|
+
)return false;
|
|
400
|
+
const record=(
|
|
401
|
+
this.lookahead
|
|
402
|
+
&&this.lookahead.generation===generation
|
|
403
|
+
&&this.lookahead.index===index
|
|
404
|
+
)?this.lookahead:this.startLookahead(index,generation);
|
|
405
|
+
if(!record)return Boolean(this.urls[index]);
|
|
406
|
+
const result=await record.promise;
|
|
407
|
+
return generation===this.generation&&result.ready===true&&Boolean(this.urls[index]);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async prepare({key,parts,voice='alloy',speed=1,autoplay=true}={}){
|
|
411
|
+
this.cancel();
|
|
412
|
+
const generation=this.generation;
|
|
413
|
+
this.key=String(key||'narration');
|
|
414
|
+
const requestKey=this.key;
|
|
415
|
+
let normalized;
|
|
416
|
+
try{
|
|
417
|
+
if(!this.available())throw new Error(this.message('unavailable'));
|
|
418
|
+
const numericSpeed=Number(speed);
|
|
419
|
+
if(!Number.isFinite(numericSpeed)||numericSpeed<=0)throw new RangeError('Speech speed must be a positive number.');
|
|
420
|
+
const selectedVoice=String(voice).trim();
|
|
421
|
+
if(!selectedVoice)throw new TypeError('Speech voice cannot be blank.');
|
|
422
|
+
this.speed=numericSpeed;
|
|
423
|
+
this.voice=selectedVoice;
|
|
424
|
+
normalized=normalizeParts(
|
|
425
|
+
typeof parts==='function'?parts():parts,
|
|
426
|
+
selectedVoice,
|
|
427
|
+
numericSpeed
|
|
428
|
+
);
|
|
429
|
+
this.parts=normalized;
|
|
430
|
+
}catch(error){
|
|
431
|
+
this.fail(error);
|
|
432
|
+
throw error;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const queue=queueFor(this.speech);
|
|
436
|
+
if(queue.busy)this.emit('synthesizing',this.message('queued',{count:normalized.length}));
|
|
437
|
+
let outcome;
|
|
438
|
+
try{
|
|
439
|
+
outcome=await this.synthesizeSegment(0,generation,true);
|
|
440
|
+
}catch(error){
|
|
441
|
+
if(generation!==this.generation)return {ready:false,played:false};
|
|
442
|
+
this.releaseURLs();
|
|
443
|
+
this.fail(error);
|
|
444
|
+
throw error;
|
|
445
|
+
}
|
|
446
|
+
if(outcome===SUPERSEDED||generation!==this.generation){
|
|
447
|
+
if(outcome?.url)this.revokeObjectURL(outcome.url);
|
|
448
|
+
if(generation===this.generation){
|
|
449
|
+
this.releaseURLs();
|
|
450
|
+
this.key='';
|
|
451
|
+
this.emit('idle',this.message('idle'),requestKey);
|
|
452
|
+
}
|
|
453
|
+
return {ready:false,played:false};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
this.urls[0]=outcome.url;
|
|
457
|
+
this.index=0;
|
|
458
|
+
this.audio.hidden=false;
|
|
459
|
+
this.loadCurrent();
|
|
460
|
+
this.emit('ready',this.message('ready'));
|
|
461
|
+
const played=autoplay?await this.play():false;
|
|
462
|
+
if(generation===this.generation)this.startLookahead(1,generation);
|
|
463
|
+
return {ready:true,played};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
loadCurrent(){
|
|
467
|
+
if(!this.urls[this.index])return;
|
|
468
|
+
this.audio.src=this.urls[this.index];
|
|
469
|
+
this.audio.load?.();
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async play(){
|
|
473
|
+
if(!this.hasAudio())return false;
|
|
474
|
+
try{
|
|
475
|
+
await this.audio.play();
|
|
476
|
+
return true;
|
|
477
|
+
}catch{
|
|
478
|
+
this.emit('ready',this.message('autoplayBlocked'));
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async restart(){
|
|
484
|
+
if(!this.hasAudio())return false;
|
|
485
|
+
this.generation+=1;
|
|
486
|
+
const generation=this.generation;
|
|
487
|
+
this.lookahead=null;
|
|
488
|
+
this.lookaheadError=null;
|
|
489
|
+
this.audio.pause();
|
|
490
|
+
this.index=0;
|
|
491
|
+
this.loadCurrent();
|
|
492
|
+
this.audio.currentTime=0;
|
|
493
|
+
const played=await this.play();
|
|
494
|
+
if(generation===this.generation)this.startLookahead(1,generation);
|
|
495
|
+
return played;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
replay(){return this.restart();}
|
|
499
|
+
|
|
500
|
+
async togglePause(){
|
|
501
|
+
if(!this.hasAudio())return false;
|
|
502
|
+
if(this.audio.paused)return this.play();
|
|
503
|
+
this.audio.pause();
|
|
504
|
+
return true;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
stop(){
|
|
508
|
+
const hasAudio=this.hasAudio();
|
|
509
|
+
if(!this.synthesisInFlight&&!hasAudio)return;
|
|
510
|
+
this.cancel(this.message(hasAudio?'stopped':'preparationStopped'));
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async advance(){
|
|
514
|
+
if(!this.hasAudio())return false;
|
|
515
|
+
if(this.index+1>=this.parts.length){
|
|
516
|
+
this.emit('ended',this.message('ended'));
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
const generation=this.generation;
|
|
520
|
+
const pauseAfterMs=this.parts[this.index]?.pauseAfterMs||0;
|
|
521
|
+
if(pauseAfterMs>0){
|
|
522
|
+
this.emit('pausing',this.message('pausing',{duration:pauseAfterMs}));
|
|
523
|
+
await this.delay(pauseAfterMs);
|
|
524
|
+
if(generation!==this.generation||!this.hasAudio())return false;
|
|
525
|
+
}
|
|
526
|
+
const nextIndex=this.index+1;
|
|
527
|
+
if(!this.urls[nextIndex]){
|
|
528
|
+
this.emit('buffering',this.message('buffering'));
|
|
529
|
+
const ready=await this.waitForSegment(nextIndex,generation);
|
|
530
|
+
if(!ready||generation!==this.generation){
|
|
531
|
+
if(generation===this.generation&&this.state==='buffering'){
|
|
532
|
+
const bufferedError=(
|
|
533
|
+
this.lookaheadError
|
|
534
|
+
&&this.lookaheadError.generation===generation
|
|
535
|
+
&&this.lookaheadError.index===nextIndex
|
|
536
|
+
)?this.lookaheadError.error:null;
|
|
537
|
+
if(bufferedError)this.fail(bufferedError);
|
|
538
|
+
else this.emit('ready',this.message('ready'));
|
|
539
|
+
}
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
this.index=nextIndex;
|
|
544
|
+
this.loadCurrent();
|
|
545
|
+
const played=await this.play();
|
|
546
|
+
if(generation===this.generation)this.startLookahead(nextIndex+1,generation);
|
|
547
|
+
return played;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async handleEnded(){
|
|
551
|
+
try{
|
|
552
|
+
await this.advance();
|
|
553
|
+
}catch(error){
|
|
554
|
+
this.fail(error);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
handlePlay(){this.emit('playing',this.message('playing'));}
|
|
559
|
+
|
|
560
|
+
handlePause(){
|
|
561
|
+
if(this.hasAudio()&&this.audio.currentTime>0&&!this.audio.ended){
|
|
562
|
+
this.emit('paused',this.message('paused'));
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
handleError(){this.fail(new Error(this.message('playbackError')));}
|
|
567
|
+
|
|
568
|
+
fail(error){this.emit('error',String(error?.message||this.message('fallbackError')));}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export {
|
|
572
|
+
MAX_SPEECH_CHARACTERS,
|
|
573
|
+
MAX_SPEECH_CHUNKS,
|
|
574
|
+
MAX_SPEECH_INPUT,
|
|
575
|
+
PREFERRED_STREAM_SEGMENT,
|
|
576
|
+
SPEECH_VOICE_ALIASES,
|
|
577
|
+
SPEECH_VOICE_OPTIONS,
|
|
578
|
+
SpeechPlayback,
|
|
579
|
+
splitSpeechText
|
|
580
|
+
};
|
|
581
|
+
export default SpeechPlayback;
|