@solidrt/core 0.0.10 → 0.0.13
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 +112 -0
- package/README.md +2 -0
- package/jsx-runtime.d.ts +3 -0
- package/package.json +8 -5
- package/src/camera.ts +83 -40
- package/src/color.ts +58 -0
- package/src/core.ts +24 -18
- package/src/gpu.ts +51 -14
- package/src/image.ts +19 -0
- package/src/index.ts +7 -3
- package/src/microphone.ts +43 -25
- package/src/renderer.ts +25 -12
- package/src/speech-recognition.ts +149 -0
- package/src/text-input.ts +207 -0
- package/src/types.d.ts +27 -49
- package/src/window.ts +88 -8
- package/src/camera-view.tsx +0 -55
- package/src/speech.ts +0 -67
package/src/window.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { onCleanup, onSettled, flush } from "@solidjs/signals"
|
|
1
|
+
import { createSignal, onCleanup, onSettled, flush } from "@solidjs/signals"
|
|
2
|
+
import { requestFrame } from "flux:rendertree"
|
|
3
|
+
import { renderFrame } from "srt:render"
|
|
2
4
|
import { on, once } from "srt:events"
|
|
3
5
|
import { getEventHandler, getFocusedNodeId, setFocus } from "./core"
|
|
4
6
|
|
|
@@ -28,12 +30,12 @@ export function onFrame(fn: (tick: number, frame: number, rate: number) => void)
|
|
|
28
30
|
frameId = nextFrameId++
|
|
29
31
|
animationFrames.set(frameId, extendedFn)
|
|
30
32
|
// A pending onFrame callback is a standing request for the next frame.
|
|
31
|
-
|
|
33
|
+
requestFrame()
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
frameId = nextFrameId++
|
|
35
37
|
animationFrames.set(frameId, extendedFn)
|
|
36
|
-
|
|
38
|
+
requestFrame()
|
|
37
39
|
|
|
38
40
|
let cleanup = () => animationFrames.delete(frameId)
|
|
39
41
|
onCleanup(cleanup)
|
|
@@ -42,6 +44,8 @@ export function onFrame(fn: (tick: number, frame: number, rate: number) => void)
|
|
|
42
44
|
|
|
43
45
|
// ------ Resize ----------------
|
|
44
46
|
|
|
47
|
+
// Insets: each value is the distance from the corresponding window edge, like
|
|
48
|
+
// CSS env(safe-area-inset-*).
|
|
45
49
|
interface SafeArea {
|
|
46
50
|
top: number
|
|
47
51
|
left: number
|
|
@@ -62,10 +66,86 @@ export function onResize(fn: (data: ResizeEvent) => void) {
|
|
|
62
66
|
return unsubscribe
|
|
63
67
|
}
|
|
64
68
|
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
+
// ------ Reactive window state ----------------
|
|
70
|
+
|
|
71
|
+
// Singleton accessors over the same events as onResize / onWindowFocus. There
|
|
72
|
+
// is one window, so these are bare accessors rather than a createX instance.
|
|
73
|
+
// Lazily subscribed on first read (resize is sticky, so the first read sees the
|
|
74
|
+
// current value); app-lifetime, so no onCleanup.
|
|
75
|
+
|
|
76
|
+
let sizeAccessor: (() => { width: number; height: number }) | undefined
|
|
77
|
+
let safeAreaAccessor: (() => SafeArea) | undefined
|
|
78
|
+
let displayScaleAccessor: (() => number) | undefined
|
|
79
|
+
|
|
80
|
+
function ensureResizeState() {
|
|
81
|
+
if (sizeAccessor) return
|
|
82
|
+
let [size, setSize] = createSignal({ width: 0, height: 0 })
|
|
83
|
+
let [safe, setSafe] = createSignal<SafeArea>({ top: 0, left: 0, right: 0, bottom: 0 })
|
|
84
|
+
let [scale, setScale] = createSignal(1)
|
|
85
|
+
on("resize", (e: ResizeEvent) => {
|
|
86
|
+
setSize({ width: e.width, height: e.height })
|
|
87
|
+
setSafe(e.safeArea)
|
|
88
|
+
setScale(e.displayScale)
|
|
89
|
+
})
|
|
90
|
+
sizeAccessor = size
|
|
91
|
+
safeAreaAccessor = safe
|
|
92
|
+
displayScaleAccessor = scale
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Current window size, as a reactive accessor. */
|
|
96
|
+
export function windowSize(): { width: number; height: number } {
|
|
97
|
+
ensureResizeState()
|
|
98
|
+
return sizeAccessor!()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Current safe-area insets, as a reactive accessor. */
|
|
102
|
+
export function safeArea(): SafeArea {
|
|
103
|
+
ensureResizeState()
|
|
104
|
+
return safeAreaAccessor!()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Current display scale (device pixel ratio), as a reactive accessor. */
|
|
108
|
+
export function displayScale(): number {
|
|
109
|
+
ensureResizeState()
|
|
110
|
+
return displayScaleAccessor!()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let focusedAccessor: (() => boolean) | undefined
|
|
114
|
+
|
|
115
|
+
/** Whether the window currently has focus, as a reactive accessor. */
|
|
116
|
+
export function windowFocused(): boolean {
|
|
117
|
+
if (!focusedAccessor) {
|
|
118
|
+
let [focused, setFocused] = createSignal(true)
|
|
119
|
+
on("windowFocus", () => setFocused(true))
|
|
120
|
+
on("windowBlur", () => setFocused(false))
|
|
121
|
+
focusedAccessor = focused
|
|
122
|
+
}
|
|
123
|
+
return focusedAccessor()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let keyboardHeightAccessor: (() => number) | undefined
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Height in logical pixels that the on-screen keyboard overlaps the window
|
|
130
|
+
* (0 when hidden or on platforms without a soft keyboard), as a reactive
|
|
131
|
+
* accessor. The window is not resized for the keyboard, so pad or lift content
|
|
132
|
+
* by this much to keep it above the keyboard.
|
|
133
|
+
*/
|
|
134
|
+
export function keyboardHeight(): number {
|
|
135
|
+
if (!keyboardHeightAccessor) {
|
|
136
|
+
let [height, setHeight] = createSignal(0)
|
|
137
|
+
on("keyboardVisibility", ({ height: h }: { height: number }) => setHeight(h ?? 0))
|
|
138
|
+
keyboardHeightAccessor = height
|
|
139
|
+
}
|
|
140
|
+
return keyboardHeightAccessor()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Fires after layout has been computed for the current frame but before paint.
|
|
145
|
+
* Setting properties that affect layout from this callback will be picked up
|
|
146
|
+
* by a re-layout pass before painting (one extra pass; cascades beyond that
|
|
147
|
+
* paint stale).
|
|
148
|
+
*/
|
|
69
149
|
export function onLayout(fn: () => void) {
|
|
70
150
|
let unsubscribe = on("postLayout", fn)
|
|
71
151
|
onCleanup(unsubscribe)
|
|
@@ -108,7 +188,7 @@ export function attachWindow(_nodeId: number) {
|
|
|
108
188
|
for (let fn of frames.values()) fn(t, frame, refreshRate)
|
|
109
189
|
}
|
|
110
190
|
flush()
|
|
111
|
-
|
|
191
|
+
renderFrame()
|
|
112
192
|
}
|
|
113
193
|
|
|
114
194
|
onSettled(() => {
|
package/src/camera-view.tsx
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { createSignal, onCleanup } from "@solidjs/signals"
|
|
2
|
-
import { openCamera, type BarcodeResult, type Camera } from "./camera"
|
|
3
|
-
|
|
4
|
-
// Convenience viewfinder over openCamera: opens on mount, renders the stream
|
|
5
|
-
// texture, closes on cleanup. Use openCamera directly for anything it does not
|
|
6
|
-
// cover; this is just composition, no extra capability.
|
|
7
|
-
|
|
8
|
-
export interface CameraViewProps {
|
|
9
|
-
/** Explicit device id from listCameras(); takes precedence over facing. */
|
|
10
|
-
camera?: number
|
|
11
|
-
facing?: "front" | "back"
|
|
12
|
-
/**
|
|
13
|
-
* Size hint for the stream and explicit size of the view. Omit height to
|
|
14
|
-
* follow the stream's aspect ratio, which can flip when a phone rotates.
|
|
15
|
-
*/
|
|
16
|
-
width?: number
|
|
17
|
-
height?: number
|
|
18
|
-
scan?: "qr"[]
|
|
19
|
-
onReady?: (cam: Camera) => void
|
|
20
|
-
onError?: (error: Error) => void
|
|
21
|
-
onBarcode?: (result: BarcodeResult) => void
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function CameraView(props: CameraViewProps) {
|
|
25
|
-
let [texture, setTexture] = createSignal<number | undefined>(undefined)
|
|
26
|
-
let cam: Camera | undefined
|
|
27
|
-
let disposed = false
|
|
28
|
-
|
|
29
|
-
openCamera({
|
|
30
|
-
camera: props.camera,
|
|
31
|
-
facing: props.facing,
|
|
32
|
-
width: props.width,
|
|
33
|
-
height: props.height,
|
|
34
|
-
scan: props.scan,
|
|
35
|
-
})
|
|
36
|
-
.then((opened) => {
|
|
37
|
-
if (disposed) {
|
|
38
|
-
opened.close()
|
|
39
|
-
return
|
|
40
|
-
}
|
|
41
|
-
cam = opened
|
|
42
|
-
if (props.onBarcode) opened.onBarcode(props.onBarcode)
|
|
43
|
-
setTexture(opened.texture)
|
|
44
|
-
props.onReady?.(opened)
|
|
45
|
-
})
|
|
46
|
-
.catch((e) => props.onError?.(e instanceof Error ? e : new Error(String(e))))
|
|
47
|
-
|
|
48
|
-
onCleanup(() => {
|
|
49
|
-
disposed = true
|
|
50
|
-
cam?.close()
|
|
51
|
-
cam = undefined
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
return <texture src={texture()} width={props.width} height={props.height} />
|
|
55
|
-
}
|
package/src/speech.ts
DELETED
|
@@ -1,67 +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 loading fails.
|
|
7
|
-
// Models are passed as bytes so any source composes: flux:fs file(), fetch
|
|
8
|
-
// (incl. the dev-server file proxy), or a download cache layered on top.
|
|
9
|
-
// Requires a runtime built with speech support.
|
|
10
|
-
|
|
11
|
-
export type SpeechOptions = {
|
|
12
|
-
/** A ggml Whisper model (file contents, e.g. ggml-tiny.en.bin). */
|
|
13
|
-
model: Uint8Array
|
|
14
|
-
/** A ggml Silero VAD model (file contents). */
|
|
15
|
-
vadModel: Uint8Array
|
|
16
|
-
/** Whisper language code; "auto" detects (multilingual models only). Default "en". */
|
|
17
|
-
language?: string
|
|
18
|
-
/** Explicit microphone device id from listMicrophones(). */
|
|
19
|
-
microphone?: number
|
|
20
|
-
/** Stop automatically after the first final result (with wakeWord: re-arm instead, one result per wake). */
|
|
21
|
-
singleUtterance?: boolean
|
|
22
|
-
/** Also deliver snapshot transcripts (final: false) while an utterance is still being spoken. */
|
|
23
|
-
interimResults?: boolean
|
|
24
|
-
/**
|
|
25
|
-
* Wake word: start asleep, fire onWake when it is heard, then transcribe
|
|
26
|
-
* the speech that follows. How the wake word is specified depends on the
|
|
27
|
-
* engine. The current engine detects with a trained classifier and takes
|
|
28
|
-
* the model's bytes (livekit-wakeword ONNX, e.g. the pretrained "hey
|
|
29
|
-
* livekit"; custom phrases are trained offline with its toolkit). Phrase
|
|
30
|
-
* strings are reserved for engines that match text; passing them to this
|
|
31
|
-
* engine rejects with an error.
|
|
32
|
-
*/
|
|
33
|
-
wakeWord?: Uint8Array | string | string[]
|
|
34
|
-
/** Detector confidence (0..1) that counts as a wake. Default 0.5. */
|
|
35
|
-
wakeThreshold?: number
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export type SpeechResult = {
|
|
39
|
-
/** Transcript of the utterance (a snapshot of it when final is false). */
|
|
40
|
-
text: string
|
|
41
|
-
/** True for the completed utterance, false for interim snapshots. */
|
|
42
|
-
final: boolean
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export type SpeechSession = {
|
|
46
|
-
/** Receive transcripts (replaces any previous callback). */
|
|
47
|
-
onResult(callback: (result: SpeechResult) => void): void
|
|
48
|
-
/** The user started speaking (replaces any previous callback). */
|
|
49
|
-
onSpeechStart(callback: () => void): void
|
|
50
|
-
/** The utterance ended; its final result follows once transcribed. */
|
|
51
|
-
onSpeechEnd(callback: () => void): void
|
|
52
|
-
/** The wake word was heard (wakeWordModel sessions only). */
|
|
53
|
-
onWake(callback: () => void): void
|
|
54
|
-
/** Release the microphone and discard any utterance in progress. */
|
|
55
|
-
stop(): void
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export async function startRecognition(options: SpeechOptions): Promise<SpeechSession> {
|
|
59
|
-
let started = await speech.start(options)
|
|
60
|
-
return {
|
|
61
|
-
onResult: (callback: (result: SpeechResult) => void) => speech.setResultCallback(started.handle, callback),
|
|
62
|
-
onSpeechStart: (callback: () => void) => speech.setSpeechStartCallback(started.handle, callback),
|
|
63
|
-
onSpeechEnd: (callback: () => void) => speech.setSpeechEndCallback(started.handle, callback),
|
|
64
|
-
onWake: (callback: () => void) => speech.setWakeCallback(started.handle, callback),
|
|
65
|
-
stop: () => speech.stop(started.handle),
|
|
66
|
-
}
|
|
67
|
-
}
|