@bendyline/squisq-editor-react 2.11.17 → 2.11.19

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.
Files changed (30) hide show
  1. package/dist/{chunk-US5DALTZ.js → chunk-5EB3BGPE.js} +2419 -2025
  2. package/dist/{chunk-QZWFSSDE.js → chunk-DJS5I5NO.js} +1 -1
  3. package/dist/chunk-DNEVUOBZ.js +14 -0
  4. package/dist/chunk-DNJ6HOES.js +93 -0
  5. package/dist/{chunk-DPAMJV6A.js → chunk-HSWWEWT4.js} +31 -2
  6. package/dist/{chunk-EJKGXWNK.js → chunk-LR6Q2MK4.js} +1 -1
  7. package/dist/chunk-LRZJD5YD.js +107 -0
  8. package/dist/{chunk-2XXBDECJ.js → chunk-OCQEWWHI.js} +1 -1
  9. package/dist/{chunk-HTO5MTTJ.js → chunk-TZX7RKZV.js} +1107 -130
  10. package/dist/{chunk-C6QVA6TO.js → chunk-UFK47ZAI.js} +58 -133
  11. package/dist/{chunk-UBFNLWCN.js → chunk-V2OLZOYJ.js} +2 -2
  12. package/dist/{chunk-XZVMKR5Q.js → chunk-ZS6G2P57.js} +41 -96
  13. package/dist/dictationSession-PN3OT5LJ.js +521 -0
  14. package/dist/index.d.ts +30 -11
  15. package/dist/index.js +53 -21
  16. package/dist/json-editor/index.js +3 -3
  17. package/dist/recorder/index.d.ts +2 -2
  18. package/dist/recorder/index.js +17 -13
  19. package/dist/{recorder-CRsw0eC7.d.ts → recorder-DKN3vZPi.d.ts} +1 -1
  20. package/dist/review/index.js +3 -2
  21. package/dist/shell/index.d.ts +2 -1
  22. package/dist/shell/index.js +10 -7
  23. package/dist/{shell-X0EoQbbN.d.ts → shell-Cje1A3zN.d.ts} +176 -5
  24. package/dist/speech/index.d.ts +126 -0
  25. package/dist/speech/index.js +10 -0
  26. package/dist/styles/index.css +142 -3
  27. package/dist/teleprompter/index.d.ts +13 -5
  28. package/dist/teleprompter/index.js +3 -2
  29. package/dist/{useNarrationStage-vKtk6H-V.d.ts → useNarrationStage-CB8dynUg.d.ts} +15 -1
  30. package/package.json +10 -6
@@ -0,0 +1,126 @@
1
+ /**
2
+ * The speech-input (dictation) capability contract between an editor host and
3
+ * the shell.
4
+ *
5
+ * A host that can turn speech into text passes a {@link SpeechInputProvider}
6
+ * (or a factory) to `EditorShell`'s `speechInput` prop — the same
7
+ * capability-injection semantics as `proofing`: absent means the feature is
8
+ * off, no dictation code loads, and no microphone button renders.
9
+ *
10
+ * The editor owns everything up to the recognizer: microphone capture, phrase
11
+ * segmentation, WAV encoding, the toolbar button, the caret indicator and the
12
+ * literal-text insertion. The provider owns only recognition. Audio crosses
13
+ * the boundary as one self-contained mono 16-bit PCM WAV at 16 kHz per take,
14
+ * which every whisper.cpp build accepts and which keeps IPC payloads small.
15
+ * The surface is plain data end to end, so a provider can be a local engine
16
+ * behind IPC, a call to a service, or a test fake.
17
+ */
18
+ /** Sample rate (Hz) of every WAV take handed to {@link SpeechInputProvider.transcribe}. */
19
+ declare const SPEECH_INPUT_SAMPLE_RATE = 16000;
20
+ /** Coarse readiness of a provider, as the editor's button presents it. */
21
+ type SpeechInputReadinessState =
22
+ /** Dictation can start now. */
23
+ 'ready'
24
+ /**
25
+ * The host needs a one-time setup (typically a model download) first.
26
+ * Clicking the microphone calls {@link SpeechInputProvider.requestSetup}
27
+ * instead of starting.
28
+ */
29
+ | 'download-required'
30
+ /** Dictation can never work in this host/session. The button is hidden. */
31
+ | 'unavailable'
32
+ /**
33
+ * The host still needs a permission (e.g. the OS microphone grant).
34
+ * Starting proceeds — the microphone request is what prompts for it.
35
+ */
36
+ | 'permission-required';
37
+ interface SpeechInputReadiness {
38
+ readonly state: SpeechInputReadinessState;
39
+ /** Optional human-readable detail, shown in the button's tooltip/errors. */
40
+ readonly reason?: string;
41
+ }
42
+ interface SpeechInputTranscribeOptions {
43
+ /**
44
+ * The recent dictated text of this session (a word-aligned tail of at most
45
+ * 1,000 characters), for engines that accept a priming prompt — Whisper's
46
+ * `prompt`. Absent on the first take.
47
+ */
48
+ readonly prompt?: string;
49
+ /** BCP-47-ish language hint. The editor does not currently set it. */
50
+ readonly language?: string;
51
+ /**
52
+ * Aborted when the user cancels dictation or the editor unmounts. A
53
+ * provider should stop promptly; a rejection after abort is ignored.
54
+ */
55
+ readonly signal: AbortSignal;
56
+ }
57
+ interface SpeechInputTranscript {
58
+ /** Recognized text. Blank (or Whisper's `[BLANK_AUDIO]`) means silence. */
59
+ readonly text: string;
60
+ }
61
+ interface SpeechInputProvider {
62
+ /** Stable identifier, e.g. `whisper-local`. */
63
+ readonly id: string;
64
+ /** Human-readable name, e.g. `On-device speech recognition`. */
65
+ readonly label: string;
66
+ /**
67
+ * Current readiness. Called once when the editor mounts with the
68
+ * capability (to decide whether to render the microphone at all) and again
69
+ * each time the user starts dictation. Must be cheap.
70
+ */
71
+ status(): Promise<SpeechInputReadiness>;
72
+ /** Optional readiness push (e.g. a model finished downloading). Returns unsubscribe. */
73
+ onStatus?(listener: (readiness: SpeechInputReadiness) => void): () => void;
74
+ /** Called when the user starts dictation so the host can warm its engine; may reject. */
75
+ prepare?(signal: AbortSignal): Promise<void>;
76
+ /** Called when the user clicks the mic while readiness is 'download-required'; the host shows its own setup UI. */
77
+ requestSetup?(): void;
78
+ /** Transcribe one self-contained take: mono 16-bit PCM WAV at 16 kHz. */
79
+ transcribe(wav: ArrayBuffer, options: SpeechInputTranscribeOptions): Promise<SpeechInputTranscript>;
80
+ /** Release resources. Called by the editor only for a provider it created from a factory. */
81
+ dispose?(): void;
82
+ }
83
+ /**
84
+ * Deferred construction. The shell creates the provider when it mounts with
85
+ * the capability (it needs `status()` to decide whether to render the
86
+ * microphone) and disposes it on unmount or when the capability changes. A
87
+ * host that passes an instance owns its lifetime — which is what lets one warm
88
+ * engine survive shell remounts.
89
+ */
90
+ type SpeechInputProviderFactory = () => SpeechInputProvider;
91
+ /** What the `speechInput` shell prop accepts. */
92
+ type SpeechInputCapability = SpeechInputProvider | SpeechInputProviderFactory;
93
+ /**
94
+ * Resolve a capability to a provider instance. A factory is invoked (and the
95
+ * caller then owns — must dispose — what it returns); an instance is returned
96
+ * as-is and stays host-owned.
97
+ */
98
+ declare function resolveSpeechInputProvider(capability: SpeechInputCapability): SpeechInputProvider;
99
+ /**
100
+ * Whether a capability is a factory, i.e. whether the provider resolved from
101
+ * it is owned (and must be disposed) by the resolver's caller.
102
+ */
103
+ declare function isSpeechInputProviderFactory(capability: SpeechInputCapability): capability is SpeechInputProviderFactory;
104
+ /**
105
+ * Imperative dictation control on the editor context
106
+ * (`useEditorContext().dictation`), for a host component rendered inside the
107
+ * shell's slots — e.g. to drive dictation from a native menu command. `null`
108
+ * when the host injected no `speechInput` capability.
109
+ */
110
+ interface DictationControl {
111
+ /** True from the moment dictation starts until its last take is inserted. */
112
+ readonly active: boolean;
113
+ /**
114
+ * True when `toggle()` would do something: the provider is not
115
+ * `unavailable` and a text-editing view (Write or Source) is showing.
116
+ */
117
+ readonly available: boolean;
118
+ /**
119
+ * Start dictation when idle (or ask the host to set up when readiness is
120
+ * `download-required`); finish and stop when listening; cancel while the
121
+ * microphone is still starting.
122
+ */
123
+ toggle(): void;
124
+ }
125
+
126
+ export { type DictationControl, SPEECH_INPUT_SAMPLE_RATE, type SpeechInputCapability, type SpeechInputProvider, type SpeechInputProviderFactory, type SpeechInputReadiness, type SpeechInputReadinessState, type SpeechInputTranscribeOptions, type SpeechInputTranscript, isSpeechInputProviderFactory, resolveSpeechInputProvider };
@@ -0,0 +1,10 @@
1
+ import {
2
+ SPEECH_INPUT_SAMPLE_RATE,
3
+ isSpeechInputProviderFactory,
4
+ resolveSpeechInputProvider
5
+ } from "../chunk-DNEVUOBZ.js";
6
+ export {
7
+ SPEECH_INPUT_SAMPLE_RATE,
8
+ isSpeechInputProviderFactory,
9
+ resolveSpeechInputProvider
10
+ };
@@ -7866,8 +7866,9 @@
7866
7866
  .squisq-toolbar-contextual--clipped {
7867
7867
  visibility: hidden;
7868
7868
  }
7869
- .squisq-toolbar-button--overflowed {
7870
- visibility: hidden;
7869
+ .squisq-toolbar-button.squisq-toolbar-button--overflowed,
7870
+ .squisq-toolbar-group.squisq-toolbar-group--collapsed {
7871
+ display: none;
7871
7872
  }
7872
7873
  .squisq-toolbar-view-tab {
7873
7874
  padding: 8px 14px 4px;
@@ -8145,6 +8146,131 @@
8145
8146
  text-overflow: ellipsis;
8146
8147
  white-space: nowrap;
8147
8148
  }
8149
+ .squisq-dictation {
8150
+ position: relative;
8151
+ display: inline-flex;
8152
+ align-items: center;
8153
+ flex-shrink: 0;
8154
+ }
8155
+ .squisq-dictation-button--active,
8156
+ .squisq-dictation-button--active:hover {
8157
+ background: var(--squisq-danger-soft);
8158
+ color: var(--squisq-danger-strong);
8159
+ }
8160
+ .squisq-dictation-button--listening {
8161
+ width: auto;
8162
+ gap: 6px;
8163
+ padding: 2px 8px 0;
8164
+ }
8165
+ .squisq-dictation-button[aria-busy=true] > i {
8166
+ animation: squisq-dictation-pulse 1.2s ease-in-out infinite;
8167
+ }
8168
+ .squisq-dictation-meter {
8169
+ display: inline-flex;
8170
+ align-items: center;
8171
+ gap: 1px;
8172
+ height: 16px;
8173
+ }
8174
+ .squisq-dictation-meter-bar {
8175
+ width: 2px;
8176
+ min-height: 2px;
8177
+ border-radius: 1px;
8178
+ background: currentColor;
8179
+ transition: height 0.05s linear;
8180
+ }
8181
+ .squisq-dictation-error {
8182
+ position: absolute;
8183
+ top: calc(100% + 6px);
8184
+ right: 0;
8185
+ z-index: 110;
8186
+ display: flex;
8187
+ align-items: flex-start;
8188
+ gap: 8px;
8189
+ width: max-content;
8190
+ max-width: min(320px, calc(100vw - 24px));
8191
+ padding: 8px 8px 8px 12px;
8192
+ border: 1px solid var(--squisq-danger-border);
8193
+ border-radius: 8px;
8194
+ background: var(--squisq-surface);
8195
+ box-shadow: var(--squisq-shadow-md);
8196
+ color: var(--squisq-danger-text);
8197
+ font-size: 12px;
8198
+ font-weight: 500;
8199
+ line-height: 1.4;
8200
+ white-space: normal;
8201
+ }
8202
+ .squisq-dictation-error-text {
8203
+ flex: 1;
8204
+ }
8205
+ .squisq-dictation-error-dismiss {
8206
+ flex-shrink: 0;
8207
+ padding: 0 2px;
8208
+ border: 0;
8209
+ background: transparent;
8210
+ color: var(--squisq-text-muted);
8211
+ cursor: pointer;
8212
+ font-size: 12px;
8213
+ line-height: 1.4;
8214
+ }
8215
+ .squisq-dictation-error-dismiss:hover {
8216
+ color: var(--squisq-text-strong);
8217
+ }
8218
+ .squisq-dictation-interim {
8219
+ display: inline-block;
8220
+ margin-left: 2px;
8221
+ padding: 0 6px;
8222
+ border-radius: 999px;
8223
+ background: var(--squisq-danger-soft);
8224
+ color: var(--squisq-text-muted);
8225
+ font-size: 0.78em;
8226
+ font-style: italic;
8227
+ font-weight: 400;
8228
+ line-height: 1.5;
8229
+ vertical-align: baseline;
8230
+ white-space: nowrap;
8231
+ user-select: none;
8232
+ pointer-events: none;
8233
+ }
8234
+ .squisq-dictation-interim::before {
8235
+ content: "";
8236
+ display: inline-block;
8237
+ width: 6px;
8238
+ height: 6px;
8239
+ margin-right: 5px;
8240
+ border-radius: 50%;
8241
+ background: var(--squisq-danger);
8242
+ vertical-align: 0.1em;
8243
+ animation: squisq-dictation-pulse 1.2s ease-in-out infinite;
8244
+ }
8245
+ .squisq-dictation-interim--source {
8246
+ display: inline;
8247
+ margin-left: 0.5ch;
8248
+ padding: 0;
8249
+ background: transparent;
8250
+ font-size: inherit;
8251
+ line-height: inherit;
8252
+ opacity: 0.8;
8253
+ }
8254
+ .squisq-dictation-interim--source::before {
8255
+ content: none;
8256
+ }
8257
+ @keyframes squisq-dictation-pulse {
8258
+ 0%, 100% {
8259
+ opacity: 1;
8260
+ }
8261
+ 50% {
8262
+ opacity: 0.35;
8263
+ }
8264
+ }
8265
+ @media (prefers-reduced-motion: reduce) {
8266
+ .squisq-dictation-interim::before,
8267
+ .squisq-dictation-button[aria-busy=true] > i {
8268
+ animation: none;
8269
+ }
8270
+ .squisq-dictation-meter-bar {
8271
+ transition: none;
8272
+ }
8273
+ }
8148
8274
 
8149
8275
  /* src/styles/template-picker.css */
8150
8276
  .squisq-mermaid-type-menu {
@@ -9134,6 +9260,18 @@
9134
9260
  color: var(--squisq-text-muted);
9135
9261
  flex-shrink: 0;
9136
9262
  gap: 12px;
9263
+ container: squisq-status-bar / inline-size;
9264
+ }
9265
+ @container squisq-status-bar (max-width: 520px) {
9266
+ .squisq-status-chars,
9267
+ .squisq-status-lines {
9268
+ display: none;
9269
+ }
9270
+ }
9271
+ @container squisq-status-bar (max-width: 300px) {
9272
+ .squisq-status-blocks {
9273
+ display: none;
9274
+ }
9137
9275
  }
9138
9276
  .squisq-status-item {
9139
9277
  white-space: nowrap;
@@ -11323,7 +11461,8 @@
11323
11461
  color: var(--squisq-danger-strong);
11324
11462
  background: var(--squisq-danger-soft);
11325
11463
  }
11326
- .squisq-version-history-empty {
11464
+ .squisq-version-history-empty,
11465
+ .squisq-version-history-note {
11327
11466
  padding: 16px 12px;
11328
11467
  font-size: 12px;
11329
11468
  color: var(--squisq-text-muted);
@@ -1,9 +1,9 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { Doc, Theme, MediaProvider } from '@bendyline/squisq/schemas';
3
3
  import { ContentContainer } from '@bendyline/squisq/storage';
4
- import { o as TeleprompterRecordingDeps, g as NarrationStageHandle, T as TeleprompterController, c as FloatingWindowHandle } from '../useNarrationStage-vKtk6H-V.js';
5
- export { C as CanvasSink, D as DEFAULT_TELEPROMPTER_PREFS, F as FloatOpenOptions, b as FloatTier, d as FloatingWindowManager, M as MicAnalysisHandle, e as MicAnalysisStatus, N as NarrationRecorderController, f as NarrationRecorderState, h as NarrationTake, P as PrompterTransport, J as TELEPROMPTER_PREF_LIMITS, n as TeleprompterPrefs, q as UseNarrationStageOptions, s as createFloatingWindowManager, t as detectFloatTiers, K as normalizeTeleprompterPrefs, z as useFloatingWindow, B as useMicAnalysis, E as useNarrationRecorder, G as useNarrationStage, H as useTeleprompter, I as vadConfigForSensitivity } from '../useNarrationStage-vKtk6H-V.js';
6
- import { NarrationTimingJsonV3, NarrationScript, NarrationAlignment } from '@bendyline/squisq/narration';
4
+ import { o as TeleprompterRecordingDeps, g as NarrationStageHandle, T as TeleprompterController, c as FloatingWindowHandle } from '../useNarrationStage-CB8dynUg.js';
5
+ export { C as CanvasSink, D as DEFAULT_TELEPROMPTER_PREFS, F as FloatOpenOptions, b as FloatTier, d as FloatingWindowManager, M as MicAnalysisHandle, e as MicAnalysisStatus, N as NarrationRecorderController, f as NarrationRecorderState, h as NarrationTake, P as PrompterTransport, J as TELEPROMPTER_PREF_LIMITS, n as TeleprompterPrefs, q as UseNarrationStageOptions, s as createFloatingWindowManager, t as detectFloatTiers, K as normalizeTeleprompterPrefs, z as useFloatingWindow, B as useMicAnalysis, E as useNarrationRecorder, G as useNarrationStage, H as useTeleprompter, I as vadConfigForSensitivity } from '../useNarrationStage-CB8dynUg.js';
6
+ import { NarrationTimingJsonV3, NarrationScript, NarrationAlignment, NarrationTimingGenerator } from '@bendyline/squisq/narration';
7
7
  import { ReactNode } from 'react';
8
8
 
9
9
  interface TeleprompterViewProps {
@@ -88,10 +88,18 @@ interface NarrationSavePlanArgs {
88
88
  durationSec: number;
89
89
  audioExt: string;
90
90
  cameraExt: string | null;
91
- baseWpm: number;
91
+ /** Speaking rate the take was paced at; recorded as `generator.baseWpm`. */
92
+ baseWpm?: number;
92
93
  cameraOffsetSec?: number;
93
94
  /** User-chosen filename base for the audio file; default timestamped `narration-…`. */
94
95
  audioBasename?: string;
96
+ /**
97
+ * Provenance written to the sidecar's `generator`. Defaults to the
98
+ * teleprompter aligner (`squisq-teleprompter` / `dsp-align`); a host that
99
+ * produced the timings itself names itself here — e.g.
100
+ * `{ name: 'my-tts', method: 'tts' }` for generated narration.
101
+ */
102
+ generator?: NarrationTimingGenerator;
95
103
  }
96
104
  interface NarrationSavePlan {
97
105
  audioRelativeName: string;
@@ -305,4 +313,4 @@ declare function prompterVarsFromTheme(theme: Theme): Record<string, string>;
305
313
  declare function ensureTeleprompterStyles(doc: Document): void;
306
314
  declare const TELEPROMPTER_CSS = "\n.squisq-teleprompter-root {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n min-height: 0;\n background: var(--squisq-bg, #f5f5f5);\n outline: none;\n}\n.squisq-teleprompter-stage {\n position: relative;\n flex: 1;\n min-height: 0;\n display: flex;\n}\n\n.squisq-teleprompter-surface {\n position: relative;\n flex: 1;\n min-height: 0;\n overflow: hidden;\n background: var(--squisq-prompter-bg, #101014);\n color: var(--squisq-prompter-text, #f5f5f2);\n font-family: var(--squisq-prompter-font, system-ui, sans-serif);\n user-select: none;\n}\n.squisq-teleprompter-flip {\n position: absolute;\n inset: 0;\n}\n.squisq-teleprompter-surface--mirrored .squisq-teleprompter-flip {\n transform: scaleX(-1);\n}\n.squisq-teleprompter-scroll {\n position: absolute;\n left: 0;\n right: 0;\n top: 0;\n will-change: transform;\n padding: 40vh 8% 60vh;\n box-sizing: border-box;\n}\n.squisq-teleprompter-line-guide {\n position: absolute;\n left: 0;\n right: 0;\n pointer-events: none;\n z-index: 2;\n}\n.squisq-teleprompter-line-guide::before,\n.squisq-teleprompter-line-guide::after {\n content: '';\n position: absolute;\n top: 50%;\n border: 9px solid transparent;\n transform: translateY(-50%);\n}\n.squisq-teleprompter-line-guide::before {\n left: 6px;\n border-left-color: var(--squisq-prompter-accent, #e8b64c);\n}\n.squisq-teleprompter-line-guide::after {\n right: 6px;\n border-right-color: var(--squisq-prompter-accent, #e8b64c);\n}\n.squisq-teleprompter-guide-band {\n position: absolute;\n left: 0;\n right: 0;\n background: color-mix(in srgb, var(--squisq-prompter-accent, #e8b64c) 9%, transparent);\n pointer-events: none;\n z-index: 1;\n}\n\n.squisq-teleprompter-block-marker {\n display: block;\n margin: 1.1em 0 0.35em;\n font-size: 0.38em;\n font-weight: 600;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--squisq-prompter-muted, #9a9aa0);\n border-top: 1px solid color-mix(in srgb, var(--squisq-prompter-muted, #9a9aa0) 35%, transparent);\n padding-top: 0.6em;\n}\n.squisq-teleprompter-para {\n margin: 0 0 0.55em;\n line-height: 1.4;\n font-weight: 500;\n}\n.squisq-teleprompter-word {\n opacity: 0.45;\n transition: color 0.1s linear, opacity 0.1s linear;\n}\n.squisq-teleprompter-word--read {\n opacity: 0.9;\n}\n.squisq-teleprompter-word--active {\n opacity: 1;\n font-weight: 800;\n color: var(--squisq-prompter-accent, #e8b64c);\n}\n\n.squisq-teleprompter-countdown {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 3;\n background: color-mix(in srgb, var(--squisq-prompter-bg, #101014) 72%, transparent);\n}\n.squisq-teleprompter-countdown-digit {\n font-size: 18vmin;\n font-weight: 800;\n color: var(--squisq-prompter-accent, #e8b64c);\n animation: squisq-prompter-pulse 1s ease-in-out infinite;\n}\n.squisq-teleprompter-recdot {\n position: absolute;\n top: 14px;\n right: 16px;\n z-index: 4;\n width: 14px;\n height: 14px;\n border-radius: 50%;\n background: #e5484d;\n box-shadow: 0 0 0 3px color-mix(in srgb, #e5484d 30%, transparent);\n animation: squisq-prompter-pulse 1.4s ease-in-out infinite;\n}\n/* Counter-flip overlays so they stay readable in mirror mode. */\n.squisq-teleprompter-surface--mirrored .squisq-teleprompter-countdown,\n.squisq-teleprompter-surface--mirrored .squisq-teleprompter-recdot {\n transform: scaleX(-1);\n}\n\n.squisq-teleprompter-selfview {\n position: absolute;\n right: 14px;\n bottom: 14px;\n width: 160px;\n border-radius: 8px;\n border: 2px solid color-mix(in srgb, #e5484d 60%, transparent);\n z-index: 5;\n background: #000;\n}\n\n.squisq-teleprompter-review {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 10px;\n padding: 8px 12px;\n border-top: 1px solid var(--squisq-border, #e5e7eb);\n background: var(--squisq-surface, var(--squisq-bg, #fff));\n color: var(--squisq-text, #111827);\n font-size: 12.5px;\n}\n.squisq-teleprompter-review audio {\n height: 28px;\n max-width: 260px;\n}\n.squisq-teleprompter-review button {\n font: inherit;\n color: inherit;\n background: var(--squisq-input-bg, #fff);\n border: 1px solid var(--squisq-border, #d1d5db);\n border-radius: 6px;\n padding: 4px 10px;\n cursor: pointer;\n}\n\n.squisq-teleprompter-float-note {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 12px;\n color: var(--squisq-text-muted, #4b5563);\n font-size: 14px;\n}\n\n.squisq-teleprompter-controls {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 10px;\n padding: 8px 12px;\n border-top: 1px solid var(--squisq-border, #e5e7eb);\n background: var(--squisq-surface, var(--squisq-bg, #fff));\n color: var(--squisq-text, #111827);\n font-size: 12.5px;\n}\n.squisq-teleprompter-controls .squisq-teleprompter-group {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n white-space: nowrap;\n}\n.squisq-teleprompter-controls button {\n font: inherit;\n color: inherit;\n background: var(--squisq-input-bg, #fff);\n border: 1px solid var(--squisq-border, #d1d5db);\n border-radius: 6px;\n padding: 4px 10px;\n cursor: pointer;\n}\n.squisq-teleprompter-controls button:hover {\n background: var(--squisq-surface-hover, #f3f4f6);\n}\n.squisq-teleprompter-controls button[aria-pressed='true'] {\n background: var(--squisq-text, #111827);\n color: var(--squisq-bg, #fff);\n border-color: var(--squisq-text, #111827);\n}\n.squisq-teleprompter-controls select,\n.squisq-teleprompter-controls input[type='range'] {\n font: inherit;\n color: inherit;\n background: var(--squisq-input-bg, #fff);\n border: 1px solid var(--squisq-border, #d1d5db);\n border-radius: 6px;\n max-width: 170px;\n}\n.squisq-teleprompter-controls input[type='range'] {\n border: none;\n background: transparent;\n width: 110px;\n}\n.squisq-teleprompter-meter {\n position: relative;\n width: 64px;\n height: 8px;\n border-radius: 4px;\n overflow: hidden;\n background: var(--squisq-border, #e5e7eb);\n}\n.squisq-teleprompter-meter-fill {\n position: absolute;\n inset: 0 auto 0 0;\n background: #9ca3af;\n transition: width 0.08s linear;\n}\n.squisq-teleprompter-meter--voice .squisq-teleprompter-meter-fill {\n background: #30a46c;\n}\n\n@keyframes squisq-prompter-pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.45; }\n}\n@media (prefers-reduced-motion: reduce) {\n .squisq-teleprompter-countdown-digit,\n .squisq-teleprompter-recdot {\n animation: none;\n }\n .squisq-teleprompter-word {\n transition: none;\n }\n}\n";
307
315
 
308
- export { EYE_LINE_FRACTION, FloatingWindowHandle, type NarrationSavePlan, type NarrationSaveResult, NarrationStage, NarrationStageHandle, type NarrationStageProps, PCM_WORKLET_NAME, PCM_WORKLET_SOURCE, TELEPROMPTER_CSS, TeleprompterController, TeleprompterControls, type TeleprompterControlsProps, TeleprompterRecordingDeps, TeleprompterSurface, type TeleprompterSurfaceProps, TeleprompterView, type TeleprompterViewProps, type TokenLineMap, buildNarrationSavePlan, cameraVideoLine, ensureTeleprompterStyles, executeNarrationSave, insertNarrationPreamble, measureTokenLines, narrationAnnotationLine, prompterVarsFromTheme, registerPcmWorklet, stepScroll, targetOffsetFor };
316
+ export { EYE_LINE_FRACTION, type ExecuteNarrationSaveDeps, FloatingWindowHandle, type NarrationSavePlan, type NarrationSavePlanArgs, type NarrationSaveProgress, type NarrationSaveResult, NarrationStage, NarrationStageHandle, type NarrationStageProps, PCM_WORKLET_NAME, PCM_WORKLET_SOURCE, TELEPROMPTER_CSS, TeleprompterController, TeleprompterControls, type TeleprompterControlsProps, TeleprompterRecordingDeps, TeleprompterSurface, type TeleprompterSurfaceProps, TeleprompterView, type TeleprompterViewProps, type TokenLineMap, buildNarrationSavePlan, cameraVideoLine, ensureTeleprompterStyles, executeNarrationSave, insertNarrationPreamble, measureTokenLines, narrationAnnotationLine, prompterVarsFromTheme, registerPcmWorklet, stepScroll, targetOffsetFor };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  TeleprompterView
3
- } from "../chunk-QZWFSSDE.js";
3
+ } from "../chunk-DJS5I5NO.js";
4
4
  import {
5
5
  DEFAULT_TELEPROMPTER_PREFS,
6
6
  EYE_LINE_FRACTION,
@@ -31,7 +31,8 @@ import {
31
31
  useNarrationStage,
32
32
  useTeleprompter,
33
33
  vadConfigForSensitivity
34
- } from "../chunk-XZVMKR5Q.js";
34
+ } from "../chunk-ZS6G2P57.js";
35
+ import "../chunk-DNJ6HOES.js";
35
36
  export {
36
37
  DEFAULT_TELEPROMPTER_PREFS,
37
38
  EYE_LINE_FRACTION,
@@ -380,6 +380,13 @@ interface TeleprompterController {
380
380
  prefs: TeleprompterPrefs;
381
381
  setPrefs: (patch: Partial<TeleprompterPrefs>) => void;
382
382
  play: () => void;
383
+ /**
384
+ * `play()`, resolving when the prompter actually rolls: true once the
385
+ * countdown finishes (at once if there is none or it is already rolling),
386
+ * false if pause/restart/unmount cancels the countdown first. Capture
387
+ * awaits this so a take opens at "go", not on "3, 2, 1".
388
+ */
389
+ countIn: () => Promise<boolean>;
383
390
  pause: () => void;
384
391
  restart: () => void;
385
392
  /** Move by spoken words and re-anchor voice tracking. */
@@ -519,8 +526,15 @@ interface UseNarrationRecorderOptions {
519
526
  cameraRecorderOptions?: NarrationMediaRecorderOptions;
520
527
  /** Combined audio/camera soft stop threshold; defaults to 90 MiB. Final chunks are retained. */
521
528
  maxRecordingBytes?: number;
522
- /** Fired when capture actually starts (View starts the prompter). */
529
+ /**
530
+ * Awaited once every device is live, immediately before the first byte is
531
+ * captured — the prompter's countdown. Resolving false cancels the start
532
+ * with nothing recorded.
533
+ */
534
+ countIn?: () => Promise<boolean>;
535
+ /** Fired once capture is running. */
523
536
  onRecordingStart?: () => void;
537
+ /** Fired on stop, including a stop while the start (or its count-in) is pending. */
524
538
  onRecordingStop?: () => void;
525
539
  }
526
540
  interface NarrationRecorderController {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bendyline/squisq-editor-react",
3
- "version": "2.11.17",
3
+ "version": "2.11.19",
4
4
  "description": "React editor shell with raw/WYSIWYG/preview modes for Squisq documents",
5
5
  "license": "MIT",
6
6
  "author": "Bendyline",
@@ -76,6 +76,10 @@
76
76
  "types": "./dist/review/index.d.ts",
77
77
  "import": "./dist/review/index.js"
78
78
  },
79
+ "./speech": {
80
+ "types": "./dist/speech/index.d.ts",
81
+ "import": "./dist/speech/index.js"
82
+ },
79
83
  "./styles": {
80
84
  "types": "./dist/styles/index.d.ts",
81
85
  "default": "./dist/styles/index.css"
@@ -98,12 +102,12 @@
98
102
  }
99
103
  },
100
104
  "dependencies": {
101
- "@bendyline/squisq": "2.11.14",
105
+ "@bendyline/squisq": "2.11.16",
102
106
  "@bendyline/squisq-calc": "2.11.1",
103
- "@bendyline/squisq-formats": "2.6.14",
104
- "@bendyline/squisq-grid-react": "2.11.15",
105
- "@bendyline/squisq-react": "2.11.16",
106
- "@bendyline/squisq-video-react": "2.4.20",
107
+ "@bendyline/squisq-formats": "2.6.16",
108
+ "@bendyline/squisq-grid-react": "2.11.17",
109
+ "@bendyline/squisq-react": "2.11.18",
110
+ "@bendyline/squisq-video-react": "2.4.22",
107
111
  "@fortawesome/fontawesome-free": "7.2.0",
108
112
  "@tiptap/core": "2.27.3",
109
113
  "@tiptap/extension-heading": "2.27.3",