arcane-os 0.5.4 → 0.5.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 CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.5
4
+
5
+ - Added configurable complete text-to-speech stream segmentation through
6
+ `AI.configureTTSSegmentation()`. Applications can speak on Unicode punctuation
7
+ or a whole-word cadence without discarding any text, while apostrophes,
8
+ commas, and hyphens that join Unicode letters or numbers remain intact.
9
+ Synthesis and playback stay sequential, and mute or cancellation still stops
10
+ the complete queue.
11
+
3
12
  ## 0.5.4
4
13
 
5
14
  - Reduced Browser-WASM HTTP Range parts from roughly 128 MB to roughly 4 MB
package/README.md CHANGED
@@ -19,7 +19,7 @@ version-locked SDK runtime, while an integrated Arcane checkout uses its live
19
19
  `arcane/` runtime. Both profiles preserve the same app URLs, theme, packaging,
20
20
  event, cancellation, and browser run contracts.
21
21
 
22
- This checkout defines the `0.5.4` SDK contract. Applications pin one exact npm
22
+ This checkout defines the `0.5.5` SDK contract. Applications pin one exact npm
23
23
  version and lockfile; registry state is deliberately not baked into application
24
24
  artifacts.
25
25
 
@@ -153,6 +153,24 @@ must supply each runtime/model selection explicitly. Speech roles
153
153
  load, cancel, unload, fail, and recover independently, so speech failure never
154
154
  silently falls back or prevents text chat.
155
155
 
156
+ Applications that need faster spoken-response onset can configure the shared
157
+ TTS stream without taking over synthesis or playback:
158
+
159
+ ```js
160
+ ai.configureTTSSegmentation({
161
+ punctuation:'any',
162
+ wordCadence:4
163
+ });
164
+ ```
165
+
166
+ The compatibility default remains sentence punctuation with no word cadence.
167
+ The configured stream preserves every character and punctuation mark, chooses
168
+ the earliest complete boundary, and keeps synthesis and playback sequential.
169
+ Any-punctuation mode recognizes boundaries without requiring whitespace while
170
+ keeping apostrophes, commas, and hyphens that join Unicode letters or numbers
171
+ inside the same segment.
172
+ Mute, stop, provider transition, and cancellation still govern the whole queue.
173
+
156
174
  The SDK runtime also owns `DBOPFSDocumentLibrary`,
157
175
  `DocumentLexicalSearch`, and `PersistentAIChatSession`. Document bootstrap is
158
176
  schema-driven and explicit; chat never searches a corpus unless the app wires
@@ -168,7 +186,7 @@ uses the same controller for automatic memory extraction.
168
186
  Create a new repository-shaped Arcane application with the exact stable SDK:
169
187
 
170
188
  ```bash
171
- npx arcane-os@0.5.4 new my-app --path ./my-app --target portable --git
189
+ npx arcane-os@0.5.5 new my-app --path ./my-app --target portable --git
172
190
  cd my-app
173
191
  npm install
174
192
  npm run check
@@ -179,7 +197,7 @@ To enroll an existing repository, install the exact SDK and initialize only
179
197
  missing Arcane files:
180
198
 
181
199
  ```bash
182
- npm install --save-dev --save-exact arcane-os@0.5.4
200
+ npm install --save-dev --save-exact arcane-os@0.5.5
183
201
  npm exec -- arcane init my-app --target portable
184
202
  ```
185
203
 
@@ -195,7 +213,7 @@ npm exec -- arcane-os targets
195
213
  No global SDK install or standalone Arcane CLI is required. The application
196
214
  repository's exact npm dependency and lockfile own the CLI and toolchain version.
197
215
 
198
- Use `npx arcane-os@0.5.4` for the initial bootstrap because it names this npm
216
+ Use `npx arcane-os@0.5.5` for the initial bootstrap because it names this npm
199
217
  package explicitly; bare `npx arcane` outside an installed project could resolve
200
218
  a different package. Both installed commands invoke the same headless toolchain.
201
219
  Project-local npm scripts use the SDK pinned by that app's `package-lock.json`,
@@ -216,7 +234,7 @@ node ./bin/arcane.mjs new local-app --path ../local-app --target portable --git
216
234
 
217
235
  # From the generated app repository
218
236
  cd ../local-app
219
- npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.5.4.tgz
237
+ npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.5.5.tgz
220
238
  npm run check
221
239
  npm ci
222
240
  ```
@@ -226,7 +244,7 @@ same location. The lockfile retains the selected package dependency while
226
244
  Arcane uses the installed package name and version. Local directory `file:` dependencies are not
227
245
  accepted because npm may install them as links; use a packed `.tgz`. A GitHub
228
246
  runner also needs that tarball at the locked path. After publication, replace
229
- the local declaration with the exact `arcane-os@0.5.4` registry package and
247
+ the local declaration with the exact `arcane-os@0.5.5` registry package and
230
248
  commit the regenerated lock.
231
249
 
232
250
  Generated repositories use `npm ci --ignore-scripts` in CI. Run dependency
@@ -364,7 +382,7 @@ package installation, or assertions.
364
382
 
365
383
  ## Current target support
366
384
 
367
- Version `0.5.4` exposes one browser target and five explicitly paired
385
+ Version `0.5.5` exposes one browser target and five explicitly paired
368
386
  native development targets: a non-runnable portable directory, a
369
387
  Windows x64 unsigned-local-test EXE bundle, Linux x64 and Linux ARM64
370
388
  unsigned-local-test DEBs, and an Android development-signed APK. The
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -17,6 +17,16 @@ const completeValue=(value)=>value;
17
17
 
18
18
  let credentials='include';
19
19
  const LEGACY_TTS_RESPONSE_FORMAT='opus';
20
+ const DEFAULT_TTS_SEGMENTATION={
21
+ punctuation:'sentence',
22
+ wordCadence:null
23
+ };
24
+ const TTS_PUNCTUATION_MODES=new Set(['sentence','any','none']);
25
+ // A complete punctuation run is a boundary unless the whole run consists of
26
+ // apostrophe, comma, or dash punctuation joining Unicode letters or numbers.
27
+ // The streaming expression leaves a trailing joining run for the next chunk.
28
+ const TTS_ANY_PUNCTUATION=/(?<!\p{P})(?:(?<![\p{L}\p{N}])\p{P}+(?!\p{P})(?=[\s\S])|\p{P}+(?!\p{P})(?=[^\p{L}\p{N}])|\p{P}*[^\P{P}'\u2019\uFF07,\u060C\u3001\uFF0C\p{Pd}]\p{P}*)(?!\p{P})/gu;
29
+ const TTS_ANY_PUNCTUATION_AT_END=/(?<!\p{P})(?:(?<![\p{L}\p{N}])\p{P}+|\p{P}+(?![\p{L}\p{N}])|\p{P}*[^\P{P}'\u2019\uFF07,\u060C\u3001\uFF0C\p{Pd}]\p{P}*)(?!\p{P})/gu;
20
30
  credentials='omit';
21
31
 
22
32
  const LEGACY_AI_SERVICES=new Set(['OPENAI','OLLAMA','LOCAL_SPEACH']);
@@ -142,6 +152,38 @@ function isPlainAIRecord(value){
142
152
  return prototype===Object.prototype||prototype===null;
143
153
  }
144
154
 
155
+ function normalizeTTSSegmentation(value,current=DEFAULT_TTS_SEGMENTATION){
156
+ if(value===undefined||value===null){
157
+ return {...current};
158
+ }
159
+ if(!isPlainAIRecord(value)){
160
+ throw new TypeError('TTS segmentation must be a plain object.');
161
+ }
162
+
163
+ const punctuation=value.punctuation===undefined
164
+ ?current.punctuation
165
+ :value.punctuation;
166
+ if(!TTS_PUNCTUATION_MODES.has(punctuation)){
167
+ throw new TypeError(
168
+ 'TTS segmentation punctuation must be sentence, any, or none.'
169
+ );
170
+ }
171
+
172
+ const wordCadence=value.wordCadence===undefined
173
+ ?current.wordCadence
174
+ :value.wordCadence;
175
+ if(
176
+ wordCadence!==null
177
+ &&(!Number.isSafeInteger(wordCadence)||wordCadence<1)
178
+ ){
179
+ throw new RangeError(
180
+ 'TTS segmentation wordCadence must be null or a positive integer.'
181
+ );
182
+ }
183
+
184
+ return {punctuation,wordCadence};
185
+ }
186
+
145
187
  function requireAIToolMessageSchemas(value,label='AI tools'){
146
188
  if(!Array.isArray(value)){
147
189
  throw aiStructuralError(
@@ -1069,6 +1111,7 @@ class AI {
1069
1111
  #speechControlGeneration=0;
1070
1112
  #speechFailureSequence=0;
1071
1113
  #stopOllamaReady=null;
1114
+ #ttsSegmentation={...DEFAULT_TTS_SEGMENTATION};
1072
1115
  #preferenceTuple=completeValue([
1073
1116
  'OPENAI',
1074
1117
  'LOCAL_SPEACH',
@@ -5512,6 +5555,18 @@ class AI {
5512
5555
  return responseJSON;
5513
5556
  }
5514
5557
 
5558
+ get ttsSegmentation(){
5559
+ return {...this.#ttsSegmentation};
5560
+ }
5561
+
5562
+ configureTTSSegmentation(options={}){
5563
+ this.#ttsSegmentation=normalizeTTSSegmentation(
5564
+ options,
5565
+ this.#ttsSegmentation
5566
+ );
5567
+ return this.ttsSegmentation;
5568
+ }
5569
+
5515
5570
  streamTTS(
5516
5571
  text='',
5517
5572
  end=false
@@ -5563,10 +5618,16 @@ class AI {
5563
5618
  let remainder=this.audioMessageChunks;
5564
5619
 
5565
5620
  while(remainder.length>0){
5566
- const terminator=this.#findSpeechTerminator(remainder,end);
5567
- let boundary=terminator
5568
- ?terminator.index+terminator[0].length
5569
- :-1;
5621
+ const punctuationBoundary=this.#findSpeechPunctuationBoundary(
5622
+ remainder,
5623
+ end
5624
+ );
5625
+ const cadenceBoundary=this.#findSpeechCadenceBoundary(remainder,end);
5626
+ let boundary=punctuationBoundary;
5627
+
5628
+ if(boundary<0||(cadenceBoundary>0&&cadenceBoundary<boundary)){
5629
+ boundary=cadenceBoundary;
5630
+ }
5570
5631
 
5571
5632
  if(boundary<0&&end){
5572
5633
  boundary=remainder.length;
@@ -5588,7 +5649,14 @@ class AI {
5588
5649
  return segments;
5589
5650
  }
5590
5651
 
5591
- #findSpeechTerminator(text,end=false){
5652
+ #findSpeechPunctuationBoundary(text,end=false){
5653
+ if(this.#ttsSegmentation.punctuation==='none'){
5654
+ return -1;
5655
+ }
5656
+ if(this.#ttsSegmentation.punctuation==='any'){
5657
+ return this.#findAnySpeechPunctuationBoundary(text,end);
5658
+ }
5659
+
5592
5660
  const pattern=end
5593
5661
  ?/(?:[\u3002\uFF01\uFF1F]|[.!?](?=\s|$))/g
5594
5662
  :/(?:[\u3002\uFF01\uFF1F]|[.!?](?=\s+\S))/g;
@@ -5602,10 +5670,71 @@ class AI {
5602
5670
  continue;
5603
5671
  }
5604
5672
 
5605
- return terminator;
5673
+ return this.#includeSpeechBoundaryWhitespace(
5674
+ text,
5675
+ terminator.index+terminator[0].length
5676
+ );
5606
5677
  }
5607
5678
 
5608
- return null;
5679
+ return -1;
5680
+ }
5681
+
5682
+ #findAnySpeechPunctuationBoundary(text,end=false){
5683
+ const pattern=end
5684
+ ?TTS_ANY_PUNCTUATION_AT_END
5685
+ :TTS_ANY_PUNCTUATION;
5686
+ pattern.lastIndex=0;
5687
+ const firstContent=text.search(/\S/u);
5688
+ let punctuation;
5689
+
5690
+ while((punctuation=pattern.exec(text))){
5691
+ if(firstContent<0||punctuation.index<=firstContent){
5692
+ continue;
5693
+ }
5694
+ const boundary=punctuation.index+punctuation[0].length;
5695
+ return this.#includeSpeechBoundaryWhitespace(text,boundary);
5696
+ }
5697
+
5698
+ return -1;
5699
+ }
5700
+
5701
+ #findSpeechCadenceBoundary(text,end=false){
5702
+ const wordCadence=this.#ttsSegmentation.wordCadence;
5703
+ if(wordCadence===null){
5704
+ return -1;
5705
+ }
5706
+
5707
+ const words=/\S+/gu;
5708
+ let word;
5709
+ let wordCount=0;
5710
+
5711
+ while((word=words.exec(text))){
5712
+ wordCount+=1;
5713
+ if(wordCount!==wordCadence){
5714
+ continue;
5715
+ }
5716
+ const boundary=word.index+word[0].length;
5717
+ if(boundary===text.length&&!end){
5718
+ return -1;
5719
+ }
5720
+ if(boundary<text.length&&!/\s/u.test(text[boundary])){
5721
+ return -1;
5722
+ }
5723
+ return this.#includeSpeechBoundaryWhitespace(text,boundary);
5724
+ }
5725
+
5726
+ return -1;
5727
+ }
5728
+
5729
+ #includeSpeechBoundaryWhitespace(text,boundary){
5730
+ let completeBoundary=boundary;
5731
+ while(
5732
+ completeBoundary<text.length
5733
+ &&/\s/u.test(text[completeBoundary])
5734
+ ){
5735
+ completeBoundary+=1;
5736
+ }
5737
+ return completeBoundary;
5609
5738
  }
5610
5739
 
5611
5740
  #isSpeechAbbreviation(text,periodIndex){