@solidrt/core 0.0.73 → 0.0.75

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/AGENTS.md CHANGED
@@ -118,16 +118,21 @@ tsconfig.json - the two load-bearing lines are jsx + jsxImportSource:
118
118
  Peer deps @solidjs/signals and @solidjs/universal must match (currently
119
119
  2.0.0-rc.14); bun resolves them from peerDependencies. On a version bump,
120
120
  `bun install` only warns ("incorrect peer dependency") and keeps the old
121
- ones, and `bun update solid-js @solidjs/signals @solidjs/universal` does
122
- not read the pin either: it adds all three to package.json at registry
123
- latest (solid-js 1.x, off Solid 2.0 entirely). The recipe that works
124
- deletes the installed copies and leaves the lockfile alone:
125
- `rm -rf node_modules/solid-js node_modules/@solidjs && bun install`
126
- (delete any nested `node_modules/@solidjs` too, or the old copy keeps
127
- resolving). Removing the directories is what forces re-resolution. Do not
128
- delete `bun.lock` to force it: that re-resolves every unrelated dependency
129
- as well, and a package that floats elsewhere fails the typecheck in a way
130
- that reads exactly like Solid fallout.
121
+ ones, and so does deleting their node_modules copies first (bun 1.3.14
122
+ reinstalls them from bun.lock). `bun update solid-js @solidjs/signals
123
+ @solidjs/universal` does not read the pin either: it adds all three to
124
+ package.json at registry latest (solid-js 1.x, off Solid 2.0 entirely).
125
+ The recipe that works pins all three explicitly at the new version:
126
+ `bun add solid-js@2.0.0-rc.14 @solidjs/signals@2.0.0-rc.14 @solidjs/universal@2.0.0-rc.14`.
127
+ It moves exactly those three lock entries and nothing else. An app `sol
128
+ init` wrote has the three pinned in package.json already (the scaffold
129
+ carries them, and release.yml takes the pins from core's peerDependencies
130
+ at publish), so there the upgrade is the pins edited and `bun install`.
131
+ Do not delete `bun.lock` or its entries to force it: bun then re-resolves
132
+ every dependency, and a package that floats elsewhere fails the typecheck
133
+ in a way that reads exactly like Solid fallout. A Solid bump in this repo
134
+ moves core's peerDependencies and the scaffold's pins
135
+ (packages/cli/src/init/scaffold/package.json) together.
131
136
 
132
137
  ## Element model (the parts that are easy to get wrong)
133
138
 
package/docs/index.md CHANGED
@@ -157,7 +157,7 @@ let camera = createCamera()
157
157
  ```
158
158
 
159
159
  The same pattern covers `@solidrt/core/microphone`, `/sound`,
160
- `/speech-recognition`, `/text-input`, `/image`, `/color`, and `/gpu`.
160
+ `/text-input`, `/image`, `/color`, and `/gpu`.
161
161
 
162
162
  ## Lifecycle
163
163
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/core",
3
- "version": "0.0.73",
3
+ "version": "0.0.75",
4
4
  "license": "MIT",
5
5
  "funding": "https://github.com/sponsors/wellawaretech",
6
6
  "author": "Antoine van Wel",
@@ -17,7 +17,6 @@
17
17
  "./input": "./src/input.ts",
18
18
  "./audio": "./src/audio.ts",
19
19
  "./microphone": "./src/microphone.ts",
20
- "./speech-recognition": "./src/speech-recognition.ts",
21
20
  "./text-input": "./src/text-input.ts",
22
21
  "./textures": "./src/textures.ts",
23
22
  "./video": "./src/video.ts",
@@ -33,8 +32,8 @@
33
32
  "AGENTS.md"
34
33
  ],
35
34
  "devDependencies": {
36
- "@solidrt/flux-types": "0.0.73",
37
- "@solidrt/test": "0.0.73"
35
+ "@solidrt/flux-types": "0.0.75",
36
+ "@solidrt/test": "0.0.75"
38
37
  },
39
38
  "peerDependencies": {
40
39
  "@solidjs/signals": "2.0.0-rc.14",
package/src/microphone.ts CHANGED
@@ -1,8 +1,11 @@
1
1
  // Microphone capture, reactive (SolidJS) layer. A session delivers raw mono
2
2
  // float32 samples at the requested sample rate (the device format is converted
3
- // by SDL); read() drains whatever was captured since the last call. Captured
4
- // audio buffers until read, so poll read() regularly (e.g. once per frame) while
5
- // open.
3
+ // by SDL); read() drains whatever was captured since the last call, out of a
4
+ // 10 s window the session keeps, so nothing has to poll: read when the app
5
+ // wants samples, and a runtime module that works on the audio reads the same
6
+ // session on its own thread, given id(). Opening the microphone IS the
7
+ // permission request (SDL semantics): it resolves once capture runs and
8
+ // rejects if the user denies access.
6
9
  //
7
10
  // The imperative primitive lives in the `flux:microphone` module; import
8
11
  // { open, listMicrophones } from "flux:microphone" for non-reactive use.
@@ -17,33 +20,48 @@ export type MicrophoneOptions = {
17
20
  sampleRate?: number
18
21
  }
19
22
 
20
- /** A live microphone with reactive lifecycle. */
23
+ /** A live microphone as reactive accessors. */
21
24
  export type MicrophoneStream = {
22
- /** Sample rate of read() samples (0 if opening failed). */
25
+ /** The session id to hand a runtime module that reads this microphone, undefined while opening or if opening failed. */
26
+ id(): number | undefined
27
+ /** Sample rate of read() samples, 0 while opening or if opening failed. */
23
28
  sampleRate(): number
24
29
  /** Drain the mono float32 samples captured since the last read (empty if not open). */
25
30
  read(): Float32Array
26
- /** Set if opening failed. */
31
+ /** Set if opening failed (e.g. permission denied). */
27
32
  error(): Error | undefined
28
33
  }
29
34
 
30
35
  /**
31
- * Opens a microphone and owns its lifecycle: closes when the reactive owner is
32
- * disposed. Capture stays pull-based, so read() drains samples on demand (e.g.
33
- * once per frame). For imperative use, call open() from "flux:microphone".
36
+ * Opens a microphone and exposes it as reactive signals: id() and
37
+ * sampleRate() are set once capture runs, error() if the open failed or the
38
+ * user denied the permission. Closes automatically when the reactive owner
39
+ * is disposed (e.g. the component unmounts). Capture stays pull-based:
40
+ * read() drains the samples captured since the last read, out of the
41
+ * session's 10 s window. For imperative use, call open() from
42
+ * "flux:microphone".
34
43
  */
35
44
  export function createMicrophone(options: MicrophoneOptions = {}): MicrophoneStream {
45
+ let [id, setId] = createSignal<number | undefined>(undefined)
46
+ let [sampleRate, setSampleRate] = createSignal(0)
36
47
  let [error, setError] = createSignal<Error | undefined>(undefined)
37
- let session: ReturnType<typeof open> | undefined
38
- let rate = 0
39
- try {
40
- session = open(options)
41
- rate = session.sampleRate
42
- } catch (e) {
43
- setError(e instanceof Error ? e : new Error(String(e)))
44
- }
48
+ let session: Awaited<ReturnType<typeof open>> | undefined
49
+ let disposed = false
50
+
51
+ open(options)
52
+ .then((mic) => {
53
+ if (disposed) {
54
+ mic.close()
55
+ return
56
+ }
57
+ session = mic
58
+ setId(mic.id)
59
+ setSampleRate(mic.sampleRate)
60
+ })
61
+ .catch((e) => setError(e instanceof Error ? e : new Error(String(e))))
45
62
 
46
63
  onCleanup(() => {
64
+ disposed = true
47
65
  if (session) {
48
66
  session.close()
49
67
  session = undefined
@@ -51,7 +69,8 @@ export function createMicrophone(options: MicrophoneOptions = {}): MicrophoneStr
51
69
  })
52
70
 
53
71
  return {
54
- sampleRate: () => rate,
72
+ id,
73
+ sampleRate,
55
74
  read: () => (session ? session.read() : new Float32Array(0)),
56
75
  error,
57
76
  }
@@ -258,6 +258,13 @@ declare module "sol:test" {
258
258
  * Throws on a size that does not match the pixels.
259
259
  */
260
260
  export function camera(width: number, height: number, pixels: Uint8Array): void
261
+ /**
262
+ * Installs a mono float clip at `sampleRate` Hz as the microphone: every
263
+ * microphone the app opens at that rate from now on holds it from the
264
+ * open, with no device, and an open one hears it now. Throws on a rate of
265
+ * 0 or one that differs from an open test microphone's.
266
+ */
267
+ export function microphone(sampleRate: number, samples: Float32Array): void
261
268
  /**
262
269
  * Runs one frame: timers due by its time fire, then frame callbacks and
263
270
  * the flush, then the draw if anything demanded one. Fulfills once the
package/src/types.d.ts CHANGED
@@ -23,18 +23,6 @@ declare global {
23
23
  */
24
24
  readonly env: { readonly DEV: boolean }
25
25
  }
26
-
27
- let speech: {
28
- start(options: {
29
- model: Uint8Array, vadModel: Uint8Array, lang?: string, microphone?: number,
30
- continuous?: boolean, interimResults?: boolean, wakeWord?: Uint8Array | string | string[], wakeThreshold?: number,
31
- }): Promise<{ handle: number }>
32
- setResultCallback(handle: number, callback: (result: { transcript: string, isFinal: boolean }) => void): void
33
- setSpeechStartCallback(handle: number, callback: () => void): void
34
- setSpeechEndCallback(handle: number, callback: () => void): void
35
- setWakeCallback(handle: number, callback: () => void): void
36
- stop(handle: number): void
37
- }
38
26
  }
39
27
 
40
28
  // JSX value model. The element type is solid-js's (its control-flow components
@@ -1,150 +0,0 @@
1
- // Speech recognition. A session captures the microphone, segments utterances
2
- // by silence (Silero VAD) and transcribes each one with Whisper, delivering
3
- // final transcripts through onResult. With wakeWord the session starts
4
- // asleep behind an efficient wake word detector (livekit-wakeword) and only
5
- // transcribes after the wake word. startRecognition resolves once the models
6
- // are loaded and listening has begun; it rejects when the microphone cannot be
7
- // opened or the models fail to load.
8
- // Models are passed as bytes so any source composes: flux:fs file(), fetch
9
- // (incl. the dev-server file proxy), or a download cache layered on top.
10
- // Requires a runtime built with speech support.
11
-
12
- import { createSignal, onCleanup } from "@solidjs/signals"
13
-
14
- export type SpeechOptions = {
15
- /** A ggml Whisper model (file contents, e.g. ggml-tiny.en.bin). */
16
- model: Uint8Array
17
- /** A ggml Silero VAD model (file contents). */
18
- vadModel: Uint8Array
19
- /** Whisper language code; "auto" detects (multilingual models only). Default "en". */
20
- lang?: string
21
- /** Explicit microphone device id from listMicrophones(). */
22
- microphone?: number
23
- /**
24
- * Keep transcribing utterance after utterance. Default true. Set false to
25
- * stop after the first final result (with wakeWord: re-arm instead, one
26
- * result per wake). Inverse of the Web Speech API default (false there).
27
- */
28
- continuous?: boolean
29
- /** Also deliver snapshot transcripts (final: false) while an utterance is still being spoken. */
30
- interimResults?: boolean
31
- /**
32
- * Wake word: start asleep, fire onWake when it is heard, then transcribe
33
- * the speech that follows. How the wake word is specified depends on the
34
- * engine. The current engine detects with a trained classifier and takes
35
- * the model's bytes (livekit-wakeword ONNX, e.g. the pretrained "hey
36
- * livekit"; custom phrases are trained offline with its toolkit). Phrase
37
- * strings are reserved for engines that match text; passing them to this
38
- * engine rejects with an error.
39
- */
40
- wakeWord?: Uint8Array | string | string[]
41
- /** Detector confidence (0..1) that counts as a wake. Default 0.5. */
42
- wakeThreshold?: number
43
- }
44
-
45
- export type SpeechResult = {
46
- /** Transcript of the utterance (a snapshot of it when isFinal is false). */
47
- transcript: string
48
- /** True for the completed utterance, false for interim snapshots. */
49
- isFinal: boolean
50
- }
51
-
52
- export type SpeechSession = {
53
- /** Receive transcripts (replaces any previous callback). */
54
- onResult(callback: (result: SpeechResult) => void): void
55
- /** The user started speaking (replaces any previous callback). */
56
- onSpeechStart(callback: () => void): void
57
- /** The utterance ended; its final result follows once transcribed. */
58
- onSpeechEnd(callback: () => void): void
59
- /** The wake word was heard (wakeWordModel sessions only). */
60
- onWake(callback: () => void): void
61
- /** Release the microphone and discard any utterance in progress. */
62
- stop(): void
63
- }
64
-
65
- export async function startRecognition(options: SpeechOptions): Promise<SpeechSession> {
66
- let started = await speech.start(options)
67
- return {
68
- onResult: (callback: (result: SpeechResult) => void) => speech.setResultCallback(started.handle, callback),
69
- onSpeechStart: (callback: () => void) => speech.setSpeechStartCallback(started.handle, callback),
70
- onSpeechEnd: (callback: () => void) => speech.setSpeechEndCallback(started.handle, callback),
71
- onWake: (callback: () => void) => speech.setWakeCallback(started.handle, callback),
72
- stop: () => speech.stop(started.handle),
73
- }
74
- }
75
-
76
- /** A live recognition session as reactive accessors. */
77
- export type SpeechStream = {
78
- /** True once the models are loaded and listening has begun. */
79
- ready(): boolean
80
- /** Latest transcript, "" until the first result (a snapshot while isFinal is false). */
81
- transcript(): string
82
- /** Whether transcript() is the completed utterance rather than an interim snapshot. */
83
- isFinal(): boolean
84
- /** True while the user is mid-utterance (between speech start and end). */
85
- speaking(): boolean
86
- /** True from when an utterance ends until its transcript arrives. */
87
- transcribing(): boolean
88
- /**
89
- * wakeWord sessions only: true from when the wake word is heard until the
90
- * following command is transcribed (the next final result), then false.
91
- * Always false without a wakeWord.
92
- */
93
- awake(): boolean
94
- /** Set if loading or starting failed. */
95
- error(): Error | undefined
96
- }
97
-
98
- /**
99
- * Starts speech recognition and exposes it as reactive signals: read
100
- * transcript()/isFinal() for results, speaking() and awake() for session
101
- * state. Stops when the reactive owner is disposed. The lower-level
102
- * startRecognition() is the imperative alternative.
103
- */
104
- export function createSpeechRecognition(options: SpeechOptions): SpeechStream {
105
- let [ready, setReady] = createSignal(false)
106
- let [transcript, setTranscript] = createSignal("")
107
- let [isFinal, setIsFinal] = createSignal(false)
108
- let [speaking, setSpeaking] = createSignal(false)
109
- let [transcribing, setTranscribing] = createSignal(false)
110
- let [awake, setAwake] = createSignal(false)
111
- let [error, setError] = createSignal<Error | undefined>(undefined)
112
- let handle: number | undefined
113
- let disposed = false
114
-
115
- speech
116
- .start(options)
117
- .then((started) => {
118
- if (disposed) {
119
- speech.stop(started.handle)
120
- return
121
- }
122
- handle = started.handle
123
- speech.setResultCallback(started.handle, (result) => {
124
- setTranscript(result.transcript)
125
- setIsFinal(result.isFinal)
126
- if (result.isFinal) {
127
- setTranscribing(false)
128
- setAwake(false)
129
- }
130
- })
131
- speech.setSpeechStartCallback(started.handle, () => setSpeaking(true))
132
- speech.setSpeechEndCallback(started.handle, () => {
133
- setSpeaking(false)
134
- setTranscribing(true)
135
- })
136
- speech.setWakeCallback(started.handle, () => setAwake(true))
137
- setReady(true)
138
- })
139
- .catch((e) => setError(e instanceof Error ? e : new Error(String(e))))
140
-
141
- onCleanup(() => {
142
- disposed = true
143
- if (handle !== undefined) {
144
- speech.stop(handle)
145
- handle = undefined
146
- }
147
- })
148
-
149
- return { ready, transcript, isFinal, speaking, transcribing, awake, error }
150
- }