@gjsify/speech-synthesis 0.0.0-stage → 0.64.11
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/README.md +48 -2
- package/globals.mjs +28 -0
- package/lib/esm/_virtual/_rolldown/runtime.js +1 -0
- package/lib/esm/backend.js +1 -0
- package/lib/esm/events.js +1 -0
- package/lib/esm/fake-spiel.js +1 -0
- package/lib/esm/index.js +1 -0
- package/lib/esm/register.js +1 -0
- package/lib/esm/speech-synthesis.js +1 -0
- package/lib/esm/spiel-types.js +1 -0
- package/lib/esm/utterance.js +1 -0
- package/lib/esm/voice.js +1 -0
- package/lib/types/backend.d.ts +54 -0
- package/lib/types/events.d.ts +36 -0
- package/lib/types/fake-spiel.d.ts +35 -0
- package/lib/types/index.d.ts +11 -0
- package/lib/types/real-spiel.spec.d.ts +2 -0
- package/lib/types/register.d.ts +1 -0
- package/lib/types/speech-synthesis.d.ts +23 -0
- package/lib/types/speech-synthesis.spec.d.ts +2 -0
- package/lib/types/spiel-types.d.ts +56 -0
- package/lib/types/utterance.d.ts +22 -0
- package/lib/types/voice.d.ts +21 -0
- package/package.json +86 -5
package/README.md
CHANGED
|
@@ -1,3 +1,49 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @gjsify/speech-synthesis
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The W3C Web Speech synthesis API for GJS: `speechSynthesis`, `SpeechSynthesis`, `SpeechSynthesisUtterance`, `SpeechSynthesisVoice`, `SpeechSynthesisEvent`, `SpeechSynthesisErrorEvent`. The backend is [libspiel](https://project-spiel.org) (`gi://Spiel?version=1.0`) and its speech providers. Linux only. Tier 3 (experimental, [ADR 0003](../../../docs/adr/0003-package-tiering.md)) until promoted. See [ADR 0121](../../../docs/adr/0121-speech-synthesis-on-gjs-via-libspiel.md).
|
|
4
|
+
|
|
5
|
+
Part of the [gjsify](https://github.com/gjsify/gjsify) project — Node.js and Web APIs for GJS (GNOME JavaScript).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
gjsify install @gjsify/speech-synthesis
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import '@gjsify/speech-synthesis/register';
|
|
17
|
+
|
|
18
|
+
speechSynthesis.addEventListener('voiceschanged', () => {
|
|
19
|
+
const voice = speechSynthesis.getVoices().find((v) => v.lang.startsWith('de'));
|
|
20
|
+
const utterance = new SpeechSynthesisUtterance('Hallo Welt');
|
|
21
|
+
if (voice) utterance.voice = voice;
|
|
22
|
+
speechSynthesis.speak(utterance);
|
|
23
|
+
});
|
|
24
|
+
speechSynthesis.getVoices(); // starts the probe; [] until `voiceschanged`
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Without a speech stack
|
|
28
|
+
|
|
29
|
+
The W3C API cannot tell "no voices" from "no libspiel". Without the typelib or a provider, `getVoices()` returns `[]` and `speak()` fires an `error` event with `error: 'synthesis-unavailable'`. Ask why with the non-standard pair:
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import { hasSpeechSynthesisBackend, describeSpeechSynthesisBackend } from '@gjsify/speech-synthesis';
|
|
33
|
+
|
|
34
|
+
if (!hasSpeechSynthesisBackend()) console.log(describeSpeechSynthesisBackend());
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Install voices with the Spiel Installer flatpak `org.project_spiel.SpielInstaller`.
|
|
38
|
+
|
|
39
|
+
## Notes
|
|
40
|
+
|
|
41
|
+
- `voiceURI` is `<provider>#<voice>`; the first voice is `default`.
|
|
42
|
+
- `rate` 0.1–10, `pitch` 0–2, `volume` 0–1 are clamped by this package.
|
|
43
|
+
- `cancel()` fires `error` with `canceled`; Spiel `word-started` becomes a `boundary` event.
|
|
44
|
+
- No cloud voices; apps bring their own.
|
|
45
|
+
- `src/backend.ts` asks `probeSpiel()` from `@gjsify/spiel-native`: the host's libspiel wins, the bundled prebuild is the fallback, and `describeSpeechSynthesisBackend()` says which one answered.
|
|
46
|
+
|
|
47
|
+
## License
|
|
48
|
+
|
|
49
|
+
MIT
|
package/globals.mjs
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-exports the native Web Speech synthesis globals for browser builds; the libspiel GJS
|
|
3
|
+
* backend stays GJS-only.
|
|
4
|
+
*
|
|
5
|
+
* `@gjsify/resolve-npm/runtime-aliases.mjs` routes `@gjsify/speech-synthesis` here whenever
|
|
6
|
+
* `gjsify.runtimes.<target> === "native"`, so every root export a consumer may import must
|
|
7
|
+
* be answerable from here or the routed bundle dies with `MISSING_EXPORT`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const speechSynthesis = globalThis.speechSynthesis;
|
|
11
|
+
export const SpeechSynthesis = globalThis.SpeechSynthesis;
|
|
12
|
+
export const SpeechSynthesisUtterance = globalThis.SpeechSynthesisUtterance;
|
|
13
|
+
export const SpeechSynthesisVoice = globalThis.SpeechSynthesisVoice;
|
|
14
|
+
export const SpeechSynthesisEvent = globalThis.SpeechSynthesisEvent;
|
|
15
|
+
export const SpeechSynthesisErrorEvent = globalThis.SpeechSynthesisErrorEvent;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Whether this host can speak at all. REAL rather than omitted (the README's recommended
|
|
19
|
+
* usage must build on every target): a browser ships its own voices.
|
|
20
|
+
*/
|
|
21
|
+
export async function hasSpeechSynthesisBackend() {
|
|
22
|
+
return typeof globalThis.speechSynthesis?.speak === 'function';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Which backend speaks, in words. In a browser: the browser's own. */
|
|
26
|
+
export async function describeSpeechSynthesisBackend() {
|
|
27
|
+
return (await hasSpeechSynthesisBackend()) ? 'browser (native speechSynthesis)' : 'none';
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var e=Object.defineProperty,__name=(t,n)=>e(t,`name`,{value:n,configurable:!0});export{__name};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import{SPIEL_NAMESPACE as e,probeSpiel as t}from"@gjsify/spiel-native";import{hostOs as n}from"@gjsify/utils/core";const r=`@gjsify/spiel-native`,i=`[@gjsify/speech-synthesis] No speech backend on this host — the Spiel-1.0 typelib (libspiel) is absent, so getVoices() is empty and speak() fires an error event (synthesis-unavailable). Install libspiel from your distribution, or the ${r}-<os>-<arch> package for this machine (linux-x64 and linux-arm64 only). Install a speech provider with the Spiel Installer (flatpak install flathub org.project_spiel.SpielInstaller) and add a voice there. Gate on hasSpeechSynthesisBackend().`,a=`[@gjsify/speech-synthesis] libspiel is available but no speech provider is installed, so getVoices() is empty and speak() fires an error event (synthesis-unavailable). Install a speech provider with the Spiel Installer (flatpak install flathub org.project_spiel.SpielInstaller) and add a voice there. Gate on hasSpeechSynthesisBackend().`;let o=null,s=!1;function isNoGiError(e){return e instanceof Error&&e.message.includes(`GObject-Introspection`)}function _diagnoseSpielLoadError(e,t=null){return isNoGiError(e)?{status:`absent`,diagnostic:null}:t!==null&&!(e instanceof Error&&e.name===`NativeLibraryLoadError`)?{status:`absent`,diagnostic:`${i} (${t})`}:{status:`failed`,diagnostic:`[@gjsify/speech-synthesis] The Spiel speech backend failed to load — a fault on this host, NOT a missing libspiel. getVoices() is empty until it is fixed.`}}async function probe(r){let i=(r.hostOs??n)();if(i===`darwin`||i===`win32`)return{status:`absent`,source:`absent`,speaker:null,namespace:null,error:null,diagnostic:`[@gjsify/speech-synthesis] No speech backend on this OS — libspiel exists for Linux only, so getVoices() is empty and speak() fires an error event (synthesis-unavailable).`};let o=(r.probe??t)();if(o.module===null){let{status:e,diagnostic:t}=_diagnoseSpielLoadError(o.error,o.reason??`not available`);return{status:e,source:`absent`,speaker:null,namespace:null,error:o.error,diagnostic:t}}let s,c=o.module;try{if(typeof c.Speaker?.new_sync!=`function`)throw Error(`${e} resolved without a Speaker class — unexpected Spiel ABI`);s=c.Speaker.new_sync(null),r.configureSpeaker?.(s)}catch(e){let{status:t,diagnostic:n}=_diagnoseSpielLoadError(e);return{status:t,source:o.source,speaker:null,namespace:null,error:e,diagnostic:n}}let l=s.providers.get_n_items()===0;return{status:`spiel`,source:o.source,speaker:s,namespace:c,error:null,diagnostic:l?a:null}}function loadSpeechSynthesisBackend(e={}){if(o!==null){if(e.probe||e.hostOs||e.configureSpeaker)throw Error(`loadSpeechSynthesisBackend(options) called after the probe already ran — the probe is cached per process, so the override would be ignored. Call _resetSpeechBackendCache() first (tests only).`);return o}return o=probe(e),o}function reportSpeechBackendOnce(e){s||e.diagnostic===null||(s=!0,e.status===`failed`?console.error(e.diagnostic,e.error):console.warn(e.diagnostic))}function backendCanSpeak(e){return e.status===`spiel`&&(e.speaker?.providers.get_n_items()??0)>0}async function hasSpeechSynthesisBackend(){return backendCanSpeak(await loadSpeechSynthesisBackend())}async function describeSpeechSynthesisBackend(){let e=await loadSpeechSynthesisBackend();if(e.status!==`spiel`)return`none (${e.status}): ${e.diagnostic??`not available on this target`}`;let t=e.speaker.providers.get_n_items();return t===0?`none (no-providers): ${a}`:`libspiel (${e.source===`prebuild`?`gi://Spiel from ${r}`:`gi://Spiel, system`}), ${t} provider(s), ${e.speaker.voices.get_n_items()} voice(s)`}function _resetSpeechBackendCache(){o=null,s=!1}export{_diagnoseSpielLoadError,_resetSpeechBackendCache,backendCanSpeak,describeSpeechSynthesisBackend,hasSpeechSynthesisBackend,loadSpeechSynthesisBackend,reportSpeechBackendOnce};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import{Event as e}from"@gjsify/dom-events";var SpeechSynthesisEvent=class extends e{utterance;charIndex;charLength;elapsedTime;name;constructor(e,t){super(e,t),this.utterance=t.utterance,this.charIndex=t.charIndex??0,this.charLength=t.charLength??0,this.elapsedTime=t.elapsedTime??0,this.name=t.name??``}get[Symbol.toStringTag](){return`SpeechSynthesisEvent`}},SpeechSynthesisErrorEvent=class extends SpeechSynthesisEvent{error;constructor(e,t){super(e,t),this.error=t.error}get[Symbol.toStringTag](){return`SpeechSynthesisErrorEvent`}};function defineEventHandler(e,t){let n=Symbol(`on${t}`);Object.defineProperty(e,`on${t}`,{configurable:!0,enumerable:!0,get(){return this[n]??null},set(e){this[n]&&this.removeEventListener(t,this[n]),this[n]=typeof e==`function`?e:null,this[n]&&this.addEventListener(t,this[n])}})}export{SpeechSynthesisErrorEvent,SpeechSynthesisEvent,defineEventHandler};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";var Emitter=class{#e=1;#t=new Map;connect(e,t){let n=this.#e++;return this.#t.set(n,{signal:e,cb:t}),n}disconnect(e){this.#t.delete(e)}emit(e,...t){for(let n of this.#t.values())n.signal===e&&n.cb(this,...t)}},FakeVoices=class extends Emitter{items;constructor(e){super(),this.items=e}get_n_items(){return this.items.length}get_item(e){return this.items[e]??null}set(e){let t=this.items.length;this.items=e,this.emit(`items-changed`,0,t,e.length)}},FakeSpeaker=class extends Emitter{voices;providers;speaking=!1;paused=!1;spoken=[];calls=[];props={};constructor(e,t){super(),this.voices=new FakeVoices(e),this.providers=new FakeVoices(t)}speak(e){this.spoken.push(e),this.calls.push(`speak`)}cancel(){this.calls.push(`cancel`),this.emit(`utterance-canceled`,this.spoken[this.spoken.length-1])}pause(){this.calls.push(`pause`)}resume(){this.calls.push(`resume`)}set_property(e,t){this.props[e]=t}get last(){return this.spoken[this.spoken.length-1]}};const e={identifier:`ai.piper.Speech.Provider`,name:`Piper`},voice=(t,n,r,i=e)=>({identifier:t,name:n,languages:r,provider:i});function fakeNamespace(e){return{Speaker:{new_sync:()=>e},Utterance:class{text=``;language=``;voice=null;pitch=1;rate=1;volume=1;constructor(e){Object.assign(this,e)}}}}export{FakeSpeaker,FakeVoices,e as PROVIDER,fakeNamespace,voice};
|
package/lib/esm/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{describeSpeechSynthesisBackend as e,hasSpeechSynthesisBackend as t}from"./backend.js";import{SpeechSynthesisErrorEvent as n,SpeechSynthesisEvent as r}from"./events.js";import{SpeechSynthesisVoice as i}from"./voice.js";import{SpeechSynthesis as a}from"./speech-synthesis.js";import{SpeechSynthesisUtterance as o}from"./utterance.js";const s=new a;export{a as SpeechSynthesis,n as SpeechSynthesisErrorEvent,r as SpeechSynthesisEvent,o as SpeechSynthesisUtterance,i as SpeechSynthesisVoice,e as describeSpeechSynthesisBackend,t as hasSpeechSynthesisBackend,s as speechSynthesis};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{SpeechSynthesisErrorEvent as e,SpeechSynthesisEvent as t}from"./events.js";import{SpeechSynthesisVoice as n}from"./voice.js";import{SpeechSynthesis as r}from"./speech-synthesis.js";import{SpeechSynthesisUtterance as i}from"./utterance.js";const a=globalThis;a.speechSynthesis===void 0&&(a.speechSynthesis=new r,a.SpeechSynthesis=r,a.SpeechSynthesisUtterance=i,a.SpeechSynthesisVoice=n,a.SpeechSynthesisEvent=t,a.SpeechSynthesisErrorEvent=e);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import{backendCanSpeak as e,loadSpeechSynthesisBackend as t,reportSpeechBackendOnce as n}from"./backend.js";import{SpeechSynthesisErrorEvent as r,SpeechSynthesisEvent as i,defineEventHandler as a}from"./events.js";import{SPIEL_ERROR as o}from"./spiel-types.js";import{createVoice as s,spielVoiceOf as c}from"./voice.js";import{Event as l,EventTarget as u}from"@gjsify/dom-events";const clamp=(e,t,n)=>Math.min(n,Math.max(t,e));var SpeechSynthesis=class extends u{#e;#t=[];#n=null;#r=!1;#i=[];#a=null;#o=null;#s=[];#c=0;constructor(e={}){super(),this.#e=e}get speaking(){return this.#n!==null}get pending(){return this.#t.length>0}get paused(){return this.#r}getVoices(){return this.#l(),this.#i.slice()}speak(t){this.#t.push(t),this.#l().then(t=>{if(!e(t)){n(t),this.#b();return}this.#f()})}cancel(){this.#t.length=0;let e=this.#n;e!==null&&(this.#n=null,this.#r=!1,this.#a?.speaker?.cancel(),this.#S(e,`canceled`))}pause(){this.#r||this.#n===null||(this.#r=!0,this.#a?.speaker?.pause(),this.#x(this.#n,`pause`))}resume(){this.#r&&(this.#r=!1,this.#a?.speaker?.resume(),this.#n&&this.#x(this.#n,`resume`))}get[Symbol.toStringTag](){return`SpeechSynthesis`}#l(){return this.#o??=t(this.#e).then(e=>(this.#a=e,e.status===`spiel`&&e.speaker&&this.#u(e.speaker),e)),this.#o}#u(e){this.#d(e),this.#s.push(e.voices.connect(`items-changed`,()=>this.#d(e)),e.connect(`utterance-started`,()=>this.#h()),e.connect(`utterance-finished`,()=>this.#g()),e.connect(`utterance-canceled`,()=>this.#_()),e.connect(`utterance-error`,(e,t,n)=>this.#v(n)),e.connect(`word-started`,(e,t,n,r)=>this.#y(n,r)))}#d(e){let t=[],n=e.voices;for(let e=0;e<n.get_n_items();e++){let r=n.get_item(e);r&&t.push(s(r,t.length===0))}let r=t.length===this.#i.length&&t.every((e,t)=>e.voiceURI===this.#i[t].voiceURI);this.#i=t,r||this.dispatchEvent(new l(`voiceschanged`))}#f(){if(this.#n!==null||this.#t.length===0)return;let e=this.#a?.speaker;if(!e)return;let t=this.#t.shift(),n=this.#p(t);this.#n=t,this.#r=!1;try{e.speak(n)}catch{this.#n=null,this.#S(t,`synthesis-failed`),this.#f()}}#p(e){let t=c(e.voice),n={text:e.text,language:t?.languages?.[0]??e.lang??``,rate:clamp(Number(e.rate)||1,.1,10),pitch:clamp(Number.isFinite(Number(e.pitch))?Number(e.pitch):1,0,2),volume:clamp(Number.isFinite(Number(e.volume))?Number(e.volume):1,0,1)};return e.lang&&(n.language=e.lang),t&&(n.voice=t),(this.#e.createUtterance??this.#m())(n)}#m(){let e=this.#a?.namespace;if(!e)throw Error(`Spiel namespace unavailable`);return t=>new e.Utterance(t)}#h(){this.#n&&(this.#c=Date.now(),this.#x(this.#n,`start`))}#g(){let e=this.#n;e&&(this.#n=null,this.#x(e,`end`),this.#f())}#_(){let e=this.#n;e&&(this.#n=null,this.#S(e,`canceled`),this.#f())}#v(e){let t=this.#n;if(!t)return;this.#n=null;let n=e?.code===o.NO_PROVIDERS?`synthesis-unavailable`:e?.code===o.MISCONFIGURED_VOICE?`voice-unavailable`:`synthesis-failed`;this.#S(t,n),this.#f()}#y(e,t){this.#n&&this.#x(this.#n,`boundary`,{charIndex:e,charLength:Math.max(0,t-e),name:`word`})}#b(){let e=this.#t.splice(0);for(let t of e)this.#S(t,`synthesis-unavailable`)}#x(e,t,n={}){e.dispatchEvent(new i(t,{utterance:e,elapsedTime:this.#c?(Date.now()-this.#c)/1e3:0,...n}))}#S(e,t){e.dispatchEvent(new r(`error`,{utterance:e,error:t}))}};a(SpeechSynthesis.prototype,`voiceschanged`);export{SpeechSynthesis};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const e={NO_PROVIDERS:0,MISCONFIGURED_VOICE:1,PROVIDER_UNEXPECTEDLY_DIED:2,INTERNAL_PROVIDER_FAILURE:3};export{e as SPIEL_ERROR};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import{defineEventHandler as e}from"./events.js";import{EventTarget as t}from"@gjsify/dom-events";var SpeechSynthesisUtterance=class extends t{text;lang=``;voice=null;volume=1;rate=1;pitch=1;constructor(e=``){super(),this.text=String(e)}get[Symbol.toStringTag](){return`SpeechSynthesisUtterance`}};for(let t of[`start`,`end`,`error`,`pause`,`resume`,`mark`,`boundary`])e(SpeechSynthesisUtterance.prototype,t);export{SpeechSynthesisUtterance};
|
package/lib/esm/voice.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";const e=new WeakMap;var SpeechSynthesisVoice=class{voiceURI;name;lang;localService;default;constructor(e){this.voiceURI=e.voiceURI,this.name=e.name,this.lang=e.lang,this.localService=e.localService,this.default=e.default}get[Symbol.toStringTag](){return`SpeechSynthesisVoice`}};function voiceUriOf(e){return`${e.provider.identifier}#${e.identifier}`}function createVoice(t,n){let r=new SpeechSynthesisVoice({voiceURI:voiceUriOf(t),name:t.name,lang:t.languages?.[0]??``,localService:!0,default:n});return e.set(r,t),r}function spielVoiceOf(t){return t?e.get(t):void 0}export{SpeechSynthesisVoice,createVoice,spielVoiceOf,voiceUriOf};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type SpielProbe, type SpielSource } from '@gjsify/spiel-native';
|
|
2
|
+
import { type TargetOs } from '@gjsify/utils/core';
|
|
3
|
+
import type { SpielNamespace, SpielSpeaker } from './spiel-types.js';
|
|
4
|
+
export type SpeechBackendStatus = 'spiel' | 'absent' | 'failed';
|
|
5
|
+
export interface SpeechBackend {
|
|
6
|
+
status: SpeechBackendStatus;
|
|
7
|
+
/** Where libspiel came from: the host (`system`), `@gjsify/spiel-native` (`prebuild`), or `absent`. */
|
|
8
|
+
source: SpielSource;
|
|
9
|
+
speaker: SpielSpeaker | null;
|
|
10
|
+
namespace: SpielNamespace | null;
|
|
11
|
+
/** The original load error for `absent`/`failed`; `null` otherwise. */
|
|
12
|
+
error: unknown;
|
|
13
|
+
/** The one-time line the USE site prints; `null` = nothing to say. */
|
|
14
|
+
diagnostic: string | null;
|
|
15
|
+
}
|
|
16
|
+
export interface LoadSpeechBackendOptions {
|
|
17
|
+
/** Override `probeSpiel()` from `@gjsify/spiel-native`. Tests only. */
|
|
18
|
+
probe?: () => Pick<SpielProbe, 'source' | 'module' | 'reason' | 'error'>;
|
|
19
|
+
/** Override the host OS the probe branches on. Tests only. */
|
|
20
|
+
hostOs?: () => TargetOs | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Runs on the fresh `Spiel.Speaker` before the first utterance — the place to route
|
|
23
|
+
* audio elsewhere (`speaker.set_property('sink', element)`); the specs use it to keep
|
|
24
|
+
* the real-Spiel test silent.
|
|
25
|
+
*/
|
|
26
|
+
configureSpeaker?: (speaker: SpielSpeaker) => void;
|
|
27
|
+
}
|
|
28
|
+
/** Classify a failed Spiel probe or `Speaker` creation. Exported (underscored) for the specs. */
|
|
29
|
+
export declare function _diagnoseSpielLoadError(error: unknown, reason?: string | null): {
|
|
30
|
+
status: 'absent' | 'failed';
|
|
31
|
+
diagnostic: string | null;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Resolve (once) what speech stack this host has. Emits NOTHING. Passing options after the
|
|
35
|
+
* probe ran THROWS instead of being ignored — call {@link _resetSpeechBackendCache} first.
|
|
36
|
+
*/
|
|
37
|
+
export declare function loadSpeechSynthesisBackend(options?: LoadSpeechBackendOptions): Promise<SpeechBackend>;
|
|
38
|
+
/** Emit the backend's one-time diagnostic — called by `speak()`, never by the queries. */
|
|
39
|
+
export declare function reportSpeechBackendOnce(backend: SpeechBackend): void;
|
|
40
|
+
/** Whether `backend` can speak right now: libspiel loaded and at least one provider is there. */
|
|
41
|
+
export declare function backendCanSpeak(backend: SpeechBackend): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Whether this host can speak at all. `false` means `getVoices()` stays empty and every
|
|
44
|
+
* `speak()` ends in an `error` event, no matter what — which the W3C surface cannot express,
|
|
45
|
+
* since an empty voice list also means "still loading". Quiet; QUERY, not a guarantee.
|
|
46
|
+
*/
|
|
47
|
+
export declare function hasSpeechSynthesisBackend(): Promise<boolean>;
|
|
48
|
+
/**
|
|
49
|
+
* The speech stack in words, and — when there is none — why and what to install. For a
|
|
50
|
+
* settings panel or a bug report; branch on {@link hasSpeechSynthesisBackend} instead.
|
|
51
|
+
*/
|
|
52
|
+
export declare function describeSpeechSynthesisBackend(): Promise<string>;
|
|
53
|
+
/** Reset the cached probe and its one-time diagnostic — tests only. */
|
|
54
|
+
export declare function _resetSpeechBackendCache(): void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Event } from '@gjsify/dom-events';
|
|
2
|
+
import type { SpeechSynthesisUtterance } from './utterance.js';
|
|
3
|
+
export interface SpeechSynthesisEventInit {
|
|
4
|
+
utterance: SpeechSynthesisUtterance;
|
|
5
|
+
charIndex?: number;
|
|
6
|
+
charLength?: number;
|
|
7
|
+
elapsedTime?: number;
|
|
8
|
+
name?: string;
|
|
9
|
+
bubbles?: boolean;
|
|
10
|
+
cancelable?: boolean;
|
|
11
|
+
composed?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare class SpeechSynthesisEvent extends Event {
|
|
14
|
+
readonly utterance: SpeechSynthesisUtterance;
|
|
15
|
+
readonly charIndex: number;
|
|
16
|
+
readonly charLength: number;
|
|
17
|
+
readonly elapsedTime: number;
|
|
18
|
+
readonly name: string;
|
|
19
|
+
constructor(type: string, init: SpeechSynthesisEventInit);
|
|
20
|
+
get [Symbol.toStringTag](): string;
|
|
21
|
+
}
|
|
22
|
+
/** https://webaudio.github.io/web-speech-api/#enumdef-speechsynthesiserrorcode */
|
|
23
|
+
export type SpeechSynthesisErrorCode = 'canceled' | 'interrupted' | 'audio-busy' | 'audio-hardware' | 'network' | 'synthesis-unavailable' | 'synthesis-failed' | 'language-unavailable' | 'voice-unavailable' | 'text-too-long' | 'invalid-argument' | 'not-allowed';
|
|
24
|
+
export interface SpeechSynthesisErrorEventInit extends SpeechSynthesisEventInit {
|
|
25
|
+
error: SpeechSynthesisErrorCode;
|
|
26
|
+
}
|
|
27
|
+
export declare class SpeechSynthesisErrorEvent extends SpeechSynthesisEvent {
|
|
28
|
+
readonly error: SpeechSynthesisErrorCode;
|
|
29
|
+
constructor(type: string, init: SpeechSynthesisErrorEventInit);
|
|
30
|
+
get [Symbol.toStringTag](): string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Install an `on<type>` IDL attribute on a prototype: assigning a function adds it as the
|
|
34
|
+
* listener, reassigning replaces it, `null` removes it.
|
|
35
|
+
*/
|
|
36
|
+
export declare function defineEventHandler(proto: object, type: string): void;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { SpielNamespace, SpielProvider, SpielUtterance, SpielVoice } from './spiel-types.js';
|
|
2
|
+
type Cb = (...args: any[]) => void;
|
|
3
|
+
declare class Emitter {
|
|
4
|
+
#private;
|
|
5
|
+
connect(signal: string, cb: Cb): number;
|
|
6
|
+
disconnect(id: number): void;
|
|
7
|
+
emit(signal: string, ...args: unknown[]): void;
|
|
8
|
+
}
|
|
9
|
+
export declare class FakeVoices<T> extends Emitter {
|
|
10
|
+
items: T[];
|
|
11
|
+
constructor(items: T[]);
|
|
12
|
+
get_n_items(): number;
|
|
13
|
+
get_item(i: number): T;
|
|
14
|
+
set(items: T[]): void;
|
|
15
|
+
}
|
|
16
|
+
export declare class FakeSpeaker extends Emitter {
|
|
17
|
+
voices: FakeVoices<SpielVoice>;
|
|
18
|
+
providers: FakeVoices<SpielProvider>;
|
|
19
|
+
speaking: boolean;
|
|
20
|
+
paused: boolean;
|
|
21
|
+
spoken: SpielUtterance[];
|
|
22
|
+
calls: string[];
|
|
23
|
+
props: Record<string, unknown>;
|
|
24
|
+
constructor(voices: SpielVoice[], providers: SpielProvider[]);
|
|
25
|
+
speak(u: SpielUtterance): void;
|
|
26
|
+
cancel(): void;
|
|
27
|
+
pause(): void;
|
|
28
|
+
resume(): void;
|
|
29
|
+
set_property(name: string, value: unknown): void;
|
|
30
|
+
get last(): SpielUtterance;
|
|
31
|
+
}
|
|
32
|
+
export declare const PROVIDER: SpielProvider;
|
|
33
|
+
export declare const voice: (identifier: string, name: string, languages: string[], provider?: SpielProvider) => SpielVoice;
|
|
34
|
+
export declare function fakeNamespace(speaker: FakeSpeaker): SpielNamespace;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { SpeechSynthesis } from './speech-synthesis.js';
|
|
2
|
+
export type { SpeechSynthesisOptions } from './speech-synthesis.js';
|
|
3
|
+
export { SpeechSynthesisUtterance } from './utterance.js';
|
|
4
|
+
export { SpeechSynthesisVoice } from './voice.js';
|
|
5
|
+
export { SpeechSynthesisEvent, SpeechSynthesisErrorEvent } from './events.js';
|
|
6
|
+
export type { SpeechSynthesisErrorCode, SpeechSynthesisEventInit, SpeechSynthesisErrorEventInit } from './events.js';
|
|
7
|
+
export { describeSpeechSynthesisBackend, hasSpeechSynthesisBackend } from './backend.js';
|
|
8
|
+
export type { SpeechBackendStatus } from './backend.js';
|
|
9
|
+
import { SpeechSynthesis } from './speech-synthesis.js';
|
|
10
|
+
/** The shared instance, as `window.speechSynthesis`. */
|
|
11
|
+
export declare const speechSynthesis: SpeechSynthesis;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Event, EventTarget } from '@gjsify/dom-events';
|
|
2
|
+
import { type LoadSpeechBackendOptions } from './backend.js';
|
|
3
|
+
import { type SpielUtterance } from './spiel-types.js';
|
|
4
|
+
import type { SpeechSynthesisUtterance } from './utterance.js';
|
|
5
|
+
import { type SpeechSynthesisVoice } from './voice.js';
|
|
6
|
+
export interface SpeechSynthesisOptions extends LoadSpeechBackendOptions {
|
|
7
|
+
/** Constructor of `Spiel.Utterance`; taken from the loaded namespace when omitted. Tests only. */
|
|
8
|
+
createUtterance?: (props: Partial<SpielUtterance>) => SpielUtterance;
|
|
9
|
+
}
|
|
10
|
+
export declare class SpeechSynthesis extends EventTarget {
|
|
11
|
+
#private;
|
|
12
|
+
onvoiceschanged: ((ev: Event) => any) | null;
|
|
13
|
+
constructor(options?: SpeechSynthesisOptions);
|
|
14
|
+
get speaking(): boolean;
|
|
15
|
+
get pending(): boolean;
|
|
16
|
+
get paused(): boolean;
|
|
17
|
+
getVoices(): SpeechSynthesisVoice[];
|
|
18
|
+
speak(utterance: SpeechSynthesisUtterance): void;
|
|
19
|
+
cancel(): void;
|
|
20
|
+
pause(): void;
|
|
21
|
+
resume(): void;
|
|
22
|
+
get [Symbol.toStringTag](): string;
|
|
23
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export interface SpielProvider {
|
|
2
|
+
identifier: string;
|
|
3
|
+
name: string;
|
|
4
|
+
}
|
|
5
|
+
export interface SpielVoice {
|
|
6
|
+
identifier: string;
|
|
7
|
+
name: string;
|
|
8
|
+
/** BCP 47 tags, best match first. */
|
|
9
|
+
languages: string[];
|
|
10
|
+
provider: SpielProvider;
|
|
11
|
+
}
|
|
12
|
+
export interface SpielListModel<T> {
|
|
13
|
+
get_n_items(): number;
|
|
14
|
+
get_item(position: number): T | null;
|
|
15
|
+
connect(signal: 'items-changed', cb: (model: unknown, position: number, removed: number, added: number) => void): number;
|
|
16
|
+
disconnect(id: number): void;
|
|
17
|
+
}
|
|
18
|
+
export interface SpielUtterance {
|
|
19
|
+
text: string;
|
|
20
|
+
language: string;
|
|
21
|
+
voice: SpielVoice | null;
|
|
22
|
+
pitch: number;
|
|
23
|
+
rate: number;
|
|
24
|
+
volume: number;
|
|
25
|
+
}
|
|
26
|
+
export interface SpielError {
|
|
27
|
+
code: number;
|
|
28
|
+
message: string;
|
|
29
|
+
}
|
|
30
|
+
export interface SpielSpeaker {
|
|
31
|
+
voices: SpielListModel<SpielVoice>;
|
|
32
|
+
providers: SpielListModel<SpielProvider>;
|
|
33
|
+
speaking: boolean;
|
|
34
|
+
paused: boolean;
|
|
35
|
+
speak(utterance: SpielUtterance): void;
|
|
36
|
+
cancel(): void;
|
|
37
|
+
pause(): void;
|
|
38
|
+
resume(): void;
|
|
39
|
+
/** `sink` is not introspectable: only `set_property('sink', element)` reaches it. */
|
|
40
|
+
set_property(name: string, value: unknown): void;
|
|
41
|
+
connect(signal: string, cb: (...args: any[]) => void): number;
|
|
42
|
+
disconnect(id: number): void;
|
|
43
|
+
}
|
|
44
|
+
export interface SpielNamespace {
|
|
45
|
+
Speaker: {
|
|
46
|
+
new_sync(cancellable: null): SpielSpeaker;
|
|
47
|
+
};
|
|
48
|
+
Utterance: new (props: Partial<SpielUtterance>) => SpielUtterance;
|
|
49
|
+
}
|
|
50
|
+
/** `Spiel.Error` codes (the GIR enum), by number — the GIR carries no names at runtime we can rely on. */
|
|
51
|
+
export declare const SPIEL_ERROR: {
|
|
52
|
+
readonly NO_PROVIDERS: 0;
|
|
53
|
+
readonly MISCONFIGURED_VOICE: 1;
|
|
54
|
+
readonly PROVIDER_UNEXPECTEDLY_DIED: 2;
|
|
55
|
+
readonly INTERNAL_PROVIDER_FAILURE: 3;
|
|
56
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { EventTarget } from '@gjsify/dom-events';
|
|
2
|
+
import { type SpeechSynthesisErrorEvent, type SpeechSynthesisEvent } from './events.js';
|
|
3
|
+
import type { SpeechSynthesisVoice } from './voice.js';
|
|
4
|
+
type Handler<E> = ((this: SpeechSynthesisUtterance, ev: E) => any) | null;
|
|
5
|
+
export declare class SpeechSynthesisUtterance extends EventTarget {
|
|
6
|
+
text: string;
|
|
7
|
+
lang: string;
|
|
8
|
+
voice: SpeechSynthesisVoice | null;
|
|
9
|
+
volume: number;
|
|
10
|
+
rate: number;
|
|
11
|
+
pitch: number;
|
|
12
|
+
onstart: Handler<SpeechSynthesisEvent>;
|
|
13
|
+
onend: Handler<SpeechSynthesisEvent>;
|
|
14
|
+
onerror: Handler<SpeechSynthesisErrorEvent>;
|
|
15
|
+
onpause: Handler<SpeechSynthesisEvent>;
|
|
16
|
+
onresume: Handler<SpeechSynthesisEvent>;
|
|
17
|
+
onmark: Handler<SpeechSynthesisEvent>;
|
|
18
|
+
onboundary: Handler<SpeechSynthesisEvent>;
|
|
19
|
+
constructor(text?: string);
|
|
20
|
+
get [Symbol.toStringTag](): string;
|
|
21
|
+
}
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { SpielVoice } from './spiel-types.js';
|
|
2
|
+
export declare class SpeechSynthesisVoice {
|
|
3
|
+
readonly voiceURI: string;
|
|
4
|
+
readonly name: string;
|
|
5
|
+
readonly lang: string;
|
|
6
|
+
readonly localService: boolean;
|
|
7
|
+
readonly default: boolean;
|
|
8
|
+
/** Not constructible from user code in browsers; here only `getVoices()` builds them. */
|
|
9
|
+
constructor(init: {
|
|
10
|
+
voiceURI: string;
|
|
11
|
+
name: string;
|
|
12
|
+
lang: string;
|
|
13
|
+
localService: boolean;
|
|
14
|
+
default: boolean;
|
|
15
|
+
});
|
|
16
|
+
get [Symbol.toStringTag](): string;
|
|
17
|
+
}
|
|
18
|
+
/** voiceURI = provider identifier + voice identifier, so two providers can offer the same voice. */
|
|
19
|
+
export declare function voiceUriOf(voice: SpielVoice): string;
|
|
20
|
+
export declare function createVoice(voice: SpielVoice, isDefault: boolean): SpeechSynthesisVoice;
|
|
21
|
+
export declare function spielVoiceOf(voice: SpeechSynthesisVoice | null | undefined): SpielVoice | undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,87 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@gjsify/speech-synthesis",
|
|
3
|
+
"version": "0.64.11",
|
|
4
|
+
"description": "Web Speech synthesis API (speechSynthesis, SpeechSynthesisUtterance) for GJS, backed by libspiel",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"module": "lib/esm/index.js",
|
|
7
|
+
"types": "lib/types/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/types/index.d.ts",
|
|
11
|
+
"default": "./lib/esm/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./register": {
|
|
14
|
+
"types": "./lib/types/register.d.ts",
|
|
15
|
+
"default": "./lib/esm/register.js"
|
|
16
|
+
},
|
|
17
|
+
"./globals": "./globals.mjs"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"lib",
|
|
21
|
+
"globals.mjs"
|
|
22
|
+
],
|
|
23
|
+
"sideEffects": [
|
|
24
|
+
"./lib/esm/register.js",
|
|
25
|
+
"./globals.mjs"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"clear": "gjsify clear lib tsconfig.tsbuildinfo tsconfig.types.tsbuildinfo test.gjs.mjs",
|
|
29
|
+
"check": "gjsify tsc --noEmit",
|
|
30
|
+
"build": "gjsify run build:gjsify && gjsify run build:types",
|
|
31
|
+
"build:gjsify": "gjsify build --library 'src/**/*.{ts,js}' --exclude 'src/**/*.spec.{mts,ts}' 'src/test*.{mts,ts}'",
|
|
32
|
+
"build:types": "gjsify tsc",
|
|
33
|
+
"build:test": "gjsify run build:test:gjs",
|
|
34
|
+
"build:test:gjs": "gjsify build src/test.mts --app gjs --outfile test.gjs.mjs",
|
|
35
|
+
"test": "gjsify run build:gjsify && gjsify run build:test && gjsify run test:gjs",
|
|
36
|
+
"test:gjs": "gjsify run test.gjs.mjs"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"gjs",
|
|
40
|
+
"speech",
|
|
41
|
+
"speechsynthesis",
|
|
42
|
+
"tts",
|
|
43
|
+
"libspiel"
|
|
44
|
+
],
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@gjsify/dom-events": "^0.64.11",
|
|
47
|
+
"@gjsify/node-globals": "^0.64.11",
|
|
48
|
+
"@gjsify/spiel-native": "^0.64.11",
|
|
49
|
+
"@gjsify/utils": "^0.64.11"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@girs/gjs": "5.4.0",
|
|
53
|
+
"@girs/glib-2.0": "5.4.0",
|
|
54
|
+
"@gjsify/cli": "^0.64.11",
|
|
55
|
+
"@gjsify/unit": "^0.64.11",
|
|
56
|
+
"@types/node": "^25.9.2",
|
|
57
|
+
"typescript": "^6.0.3"
|
|
58
|
+
},
|
|
59
|
+
"gjsify": {
|
|
60
|
+
"runtimes": {
|
|
61
|
+
"gjs": "polyfill",
|
|
62
|
+
"node": "partial",
|
|
63
|
+
"browser": "native",
|
|
64
|
+
"nativescript": "native"
|
|
65
|
+
},
|
|
66
|
+
"os": {
|
|
67
|
+
"linux": "supported",
|
|
68
|
+
"darwin": "none",
|
|
69
|
+
"win32": "none"
|
|
70
|
+
},
|
|
71
|
+
"osNotes": {
|
|
72
|
+
"darwin": "libspiel is a Linux/GNOME speech stack; getVoices() answers [] and speak() fires an error event with code synthesis-unavailable. describeSpeechSynthesisBackend() says why.",
|
|
73
|
+
"win32": "libspiel is a Linux/GNOME speech stack; getVoices() answers [] and speak() fires an error event with code synthesis-unavailable. describeSpeechSynthesisBackend() says why."
|
|
74
|
+
},
|
|
75
|
+
"tier": 3
|
|
76
|
+
},
|
|
77
|
+
"license": "MIT",
|
|
78
|
+
"repository": {
|
|
79
|
+
"type": "git",
|
|
80
|
+
"url": "git+https://github.com/gjsify/gjsify.git",
|
|
81
|
+
"directory": "packages/web/speech-synthesis"
|
|
82
|
+
},
|
|
83
|
+
"bugs": {
|
|
84
|
+
"url": "https://github.com/gjsify/gjsify/issues"
|
|
85
|
+
},
|
|
86
|
+
"homepage": "https://github.com/gjsify/gjsify/tree/main/packages/web/speech-synthesis#readme"
|
|
87
|
+
}
|