@bendyline/docblocks-react 2.6.1 → 2.7.0
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 +3 -10
- package/THIRD_PARTY_NOTICES.txt +4284 -3651
- package/dist/AiDiagrams-XWOM3YZX.js +13 -0
- package/dist/AiEditorAssistant-M4LI2CRY.js +771 -0
- package/dist/AudioExportDialog-F23QXNPB.js +253 -0
- package/dist/DocumentConflictDialog-5LRVIGXS.js +111 -0
- package/dist/ExportDialog-L6FG6W3H.js +9 -0
- package/dist/{ExportToolbarControls-QYLBRWM7.js → ExportToolbarControls-BRCPZ7H3.js} +108 -42
- package/dist/{GitUI-L5T3XHZG.js → GitUI-LZW3IUXO.js} +5 -5
- package/dist/PresentationMaker-622NHMIN.js +9 -0
- package/dist/QuickOpenDialog-2KK4SEBU.js +356 -0
- package/dist/Settings-DuXk6baH.d.ts +144 -0
- package/dist/{ShareDialog-4OX3UPJ4.js → ShareDialog-N7GPM4SB.js} +10 -59
- package/dist/SpeechToolbarControl-UZQKGQWD.js +434 -0
- package/dist/TextFromNarration-XYRAB6O2.js +598 -0
- package/dist/WorkspaceSearchPanel-OUQQ5DOM.js +328 -0
- package/dist/chunk-2IYBBFQX.js +201 -0
- package/dist/{chunk-LG6HAWCK.js → chunk-56KO2LYN.js} +35 -4
- package/dist/chunk-5OZBGIIT.js +1826 -0
- package/dist/chunk-6B7G33D5.js +88 -0
- package/dist/chunk-ARATARVJ.js +40 -0
- package/dist/{chunk-ODYNOPC3.js → chunk-BCQZTWE5.js} +57 -17
- package/dist/chunk-EA4BO6E4.js +73 -0
- package/dist/chunk-G4MFB3IO.js +208 -0
- package/dist/chunk-I44HKDQX.js +314 -0
- package/dist/chunk-K4I3NJFL.js +49 -0
- package/dist/chunk-KBDZDFPU.js +61 -0
- package/dist/{chunk-FNSRCCBC.js → chunk-KWG6UP5O.js} +0 -10
- package/dist/chunk-MMOTEQK6.js +518 -0
- package/dist/chunk-OINKNX26.js +574 -0
- package/dist/chunk-Q5ZVP5LV.js +1711 -0
- package/dist/chunk-RP3DVNTI.js +62 -0
- package/dist/{chunk-FMDLZ4ZG.js → chunk-SGQKTKDX.js} +2 -2
- package/dist/chunk-TBLZBNQF.js +25 -0
- package/dist/{chunk-M5Y5WO7Z.js → chunk-UG4G7PJ4.js} +1 -1
- package/dist/chunk-UONFWKJ7.js +22 -0
- package/dist/chunk-VO2JYYQZ.js +40 -0
- package/dist/{chunk-EBWYGTN7.js → chunk-X437UAF7.js} +6 -0
- package/dist/chunk-YS4HVWIO.js +63 -0
- package/dist/editor.d.ts +36 -1
- package/dist/editor.js +16 -1
- package/dist/export/index.d.ts +27 -11
- package/dist/export/index.js +11 -7
- package/dist/index.d.ts +100 -13
- package/dist/index.js +3916 -1633
- package/dist/{run-export-I2XOVGG4.js → run-export-47X5FTYZ.js} +3 -3
- package/dist/settings/index.d.ts +32 -101
- package/dist/settings/index.js +14 -2
- package/package.json +9 -9
- package/src/styles/docblocks.css +2337 -650
- package/dist/ExportDialog-Q4VWI43H.js +0 -9
- package/dist/chunk-JDJVRDOP.js +0 -92
- package/dist/chunk-Q6XNIKDG.js +0 -669
- package/dist/{GitToolbarControl-O5AXBC6T.js → GitToolbarControl-J4UYZDV6.js} +3 -3
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
// src/Speech/useGenerateNarration.ts
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { useEditorContext } from "@bendyline/squisq-editor-react";
|
|
4
|
+
import {
|
|
5
|
+
buildNarrationSavePlan,
|
|
6
|
+
executeNarrationSave
|
|
7
|
+
} from "@bendyline/squisq-editor-react/teleprompter";
|
|
8
|
+
|
|
9
|
+
// src/Speech/speakable-text.ts
|
|
10
|
+
var MAX_SEGMENT_CHARACTERS = 4e3;
|
|
11
|
+
var FENCE = /^(?<indent> {0,3})(?<marker>`{3,}|~{3,})[^\n]*\n[\s\S]*?^\k<indent>\k<marker>[ \t]*$/gmu;
|
|
12
|
+
function stripFencedCode(markdown) {
|
|
13
|
+
return markdown.replace(FENCE, "");
|
|
14
|
+
}
|
|
15
|
+
function ensureSentenceEnd(text) {
|
|
16
|
+
const trimmed = text.trimEnd();
|
|
17
|
+
if (!trimmed) return trimmed;
|
|
18
|
+
return /[.!?…:;]["'”’)\]]*$/u.test(trimmed) ? trimmed : `${trimmed}.`;
|
|
19
|
+
}
|
|
20
|
+
function splitLongText(text, start, max = MAX_SEGMENT_CHARACTERS) {
|
|
21
|
+
const pieces = [];
|
|
22
|
+
let offset = 0;
|
|
23
|
+
while (offset < text.length) {
|
|
24
|
+
let end = Math.min(text.length, offset + max);
|
|
25
|
+
if (end < text.length) {
|
|
26
|
+
const window = text.slice(offset, end);
|
|
27
|
+
const sentence = Math.max(
|
|
28
|
+
window.lastIndexOf(". "),
|
|
29
|
+
window.lastIndexOf("! "),
|
|
30
|
+
window.lastIndexOf("? "),
|
|
31
|
+
window.lastIndexOf("\n")
|
|
32
|
+
);
|
|
33
|
+
const space = window.lastIndexOf(" ");
|
|
34
|
+
const cut = sentence > max / 4 ? sentence + 1 : space > max / 4 ? space : window.length;
|
|
35
|
+
end = offset + cut;
|
|
36
|
+
}
|
|
37
|
+
const piece = text.slice(offset, end);
|
|
38
|
+
const leading = piece.length - piece.trimStart().length;
|
|
39
|
+
const trimmed = piece.trim();
|
|
40
|
+
if (trimmed) {
|
|
41
|
+
pieces.push({
|
|
42
|
+
text: trimmed,
|
|
43
|
+
start: start + offset + leading,
|
|
44
|
+
end: start + offset + leading + trimmed.length
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
offset = end;
|
|
48
|
+
}
|
|
49
|
+
return pieces;
|
|
50
|
+
}
|
|
51
|
+
async function planSpeech(markdown) {
|
|
52
|
+
const [{ parseMarkdown }, { markdownToDoc }, { buildNarrationScript }] = await Promise.all([
|
|
53
|
+
import("@bendyline/squisq/markdown"),
|
|
54
|
+
import("@bendyline/squisq/doc"),
|
|
55
|
+
import("@bendyline/squisq/narration")
|
|
56
|
+
]);
|
|
57
|
+
const doc = markdownToDoc(parseMarkdown(stripFencedCode(markdown)));
|
|
58
|
+
const script = buildNarrationScript(doc);
|
|
59
|
+
const segments = [];
|
|
60
|
+
for (const block of script.blocks) {
|
|
61
|
+
const text = script.sourceText.slice(block.charStart, block.charEnd);
|
|
62
|
+
let bodyStart = block.charStart;
|
|
63
|
+
const heading = block.heading?.trim();
|
|
64
|
+
if (heading && text.startsWith(heading)) {
|
|
65
|
+
segments.push({
|
|
66
|
+
text: ensureSentenceEnd(heading),
|
|
67
|
+
blockId: block.blockId,
|
|
68
|
+
kind: "heading",
|
|
69
|
+
sourceStart: block.charStart,
|
|
70
|
+
sourceEnd: block.charStart + heading.length
|
|
71
|
+
});
|
|
72
|
+
bodyStart = block.charStart + heading.length;
|
|
73
|
+
}
|
|
74
|
+
const lines = /[^\n]+/gu;
|
|
75
|
+
const body = script.sourceText.slice(bodyStart, block.charEnd);
|
|
76
|
+
for (const line of body.matchAll(lines)) {
|
|
77
|
+
for (const piece of splitLongText(line[0], bodyStart + line.index)) {
|
|
78
|
+
segments.push({
|
|
79
|
+
text: ensureSentenceEnd(piece.text),
|
|
80
|
+
blockId: block.blockId,
|
|
81
|
+
kind: "body",
|
|
82
|
+
sourceStart: piece.start,
|
|
83
|
+
sourceEnd: piece.end
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return { sourceText: script.sourceText, segments, doc, script };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/Speech/tts-narration.ts
|
|
92
|
+
function spokenSpanForChunk(chunk, segment, elapsed) {
|
|
93
|
+
const length = segment.sourceEnd - segment.sourceStart;
|
|
94
|
+
const sourceOffset = (offset) => segment.sourceStart + Math.min(offset, length);
|
|
95
|
+
return {
|
|
96
|
+
sourceStart: sourceOffset(chunk.textStart),
|
|
97
|
+
sourceEnd: sourceOffset(chunk.textEnd),
|
|
98
|
+
startSec: elapsed,
|
|
99
|
+
endSec: elapsed + chunk.durationSec,
|
|
100
|
+
...chunk.wordTimings !== void 0 ? {
|
|
101
|
+
wordTimings: chunk.wordTimings.map((word) => ({
|
|
102
|
+
textStart: sourceOffset(word.textStart),
|
|
103
|
+
textEnd: sourceOffset(word.textEnd),
|
|
104
|
+
startSec: elapsed + word.startSec,
|
|
105
|
+
endSec: elapsed + word.endSec
|
|
106
|
+
})).filter((word) => word.textEnd > word.textStart)
|
|
107
|
+
} : {}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function buildTtsAlignment(script, spans, durationSec) {
|
|
111
|
+
const ordered = [...spans].sort((a, b) => a.sourceStart - b.sourceStart);
|
|
112
|
+
const spanOf = new Array(script.tokens.length).fill(-1);
|
|
113
|
+
let cursor = 0;
|
|
114
|
+
for (const [tokenIndex, token] of script.tokens.entries()) {
|
|
115
|
+
while (cursor < ordered.length && ordered[cursor].sourceEnd <= token.charOffset) cursor += 1;
|
|
116
|
+
const span = ordered[cursor];
|
|
117
|
+
if (span && token.charOffset >= span.sourceStart) spanOf[tokenIndex] = cursor;
|
|
118
|
+
}
|
|
119
|
+
const totals = new Array(ordered.length).fill(0);
|
|
120
|
+
for (const [tokenIndex, token] of script.tokens.entries()) {
|
|
121
|
+
const span = spanOf[tokenIndex];
|
|
122
|
+
if (span >= 0) totals[span] += Math.max(1, token.syllables);
|
|
123
|
+
}
|
|
124
|
+
const consumed = new Array(ordered.length).fill(0);
|
|
125
|
+
const modelWords = ordered.flatMap((span) => span.wordTimings ?? []);
|
|
126
|
+
const modelTimes = [];
|
|
127
|
+
let modelCursor = 0;
|
|
128
|
+
for (const token of script.tokens) {
|
|
129
|
+
while (modelCursor < modelWords.length && modelWords[modelCursor].textEnd <= token.charOffset)
|
|
130
|
+
modelCursor++;
|
|
131
|
+
const word = modelWords[modelCursor];
|
|
132
|
+
modelTimes.push(word && word.textStart < token.charEnd ? word.startSec : void 0);
|
|
133
|
+
}
|
|
134
|
+
const nextModelTimes = [];
|
|
135
|
+
let nextTime = durationSec;
|
|
136
|
+
for (let i = modelTimes.length - 1; i >= 0; i--) {
|
|
137
|
+
nextTime = modelTimes[i] ?? nextTime;
|
|
138
|
+
nextModelTimes[i] = nextTime;
|
|
139
|
+
}
|
|
140
|
+
const words = [];
|
|
141
|
+
let lastTime = 0;
|
|
142
|
+
for (const [tokenIndex, token] of script.tokens.entries()) {
|
|
143
|
+
const index = spanOf[tokenIndex];
|
|
144
|
+
const modelTime = modelTimes[tokenIndex];
|
|
145
|
+
const before = index >= 0 ? consumed[index] : 0;
|
|
146
|
+
if (index >= 0) consumed[index] = before + Math.max(1, token.syllables);
|
|
147
|
+
if (modelTime !== void 0) {
|
|
148
|
+
lastTime = Math.max(lastTime, modelTime);
|
|
149
|
+
words.push({ tokenIndex, tSec: lastTime, interpolated: false });
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (index < 0) {
|
|
153
|
+
const next = ordered.find((candidate) => candidate.sourceStart > token.charOffset);
|
|
154
|
+
lastTime = Math.max(
|
|
155
|
+
lastTime,
|
|
156
|
+
Math.min(next?.startSec ?? durationSec, nextModelTimes[tokenIndex])
|
|
157
|
+
);
|
|
158
|
+
words.push({ tokenIndex, tSec: lastTime, interpolated: true });
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const span = ordered[index];
|
|
162
|
+
const t = span.startSec + (span.endSec - span.startSec) * (before / Math.max(1, totals[index]));
|
|
163
|
+
lastTime = Math.max(lastTime, Math.min(t, nextModelTimes[tokenIndex]));
|
|
164
|
+
words.push({ tokenIndex, tSec: lastTime, interpolated: true });
|
|
165
|
+
}
|
|
166
|
+
const blocks = [];
|
|
167
|
+
for (const [blockIndex, block] of script.blocks.entries()) {
|
|
168
|
+
const first = words[block.tokenStart];
|
|
169
|
+
blocks.push({
|
|
170
|
+
blockId: block.blockId,
|
|
171
|
+
...block.heading !== void 0 ? { heading: block.heading } : {},
|
|
172
|
+
blockIndex,
|
|
173
|
+
charStart: block.charStart,
|
|
174
|
+
charEnd: block.charEnd,
|
|
175
|
+
startSec: blockIndex === 0 ? 0 : first?.tSec ?? 0,
|
|
176
|
+
endSec: durationSec
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
for (let i = 0; i < blocks.length - 1; i += 1) {
|
|
180
|
+
const current = blocks[i];
|
|
181
|
+
const next = blocks[i + 1];
|
|
182
|
+
blocks[i] = { ...current, endSec: Math.max(current.startSec, next.startSec) };
|
|
183
|
+
}
|
|
184
|
+
return { words, blocks, detectedSyllables: 0, cost: 0 };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/Speech/useGenerateNarration.ts
|
|
188
|
+
var TTS_NARRATION_GENERATOR = { name: "docblocks-kokoro", method: "tts" };
|
|
189
|
+
function hasDocumentNarration(markdown) {
|
|
190
|
+
return /\{\[audio\b[^\]]*\banchor=document\b[^\]]*\]\}/u.test(markdown);
|
|
191
|
+
}
|
|
192
|
+
function useGenerateNarration(speech) {
|
|
193
|
+
const {
|
|
194
|
+
markdownSource,
|
|
195
|
+
setMarkdownSource,
|
|
196
|
+
mediaProvider,
|
|
197
|
+
workspaceContainer,
|
|
198
|
+
bumpMediaRevision
|
|
199
|
+
} = useEditorContext();
|
|
200
|
+
const [state, setState] = useState({ phase: "idle" });
|
|
201
|
+
const sourceRef = useRef(markdownSource);
|
|
202
|
+
sourceRef.current = markdownSource;
|
|
203
|
+
const run = useRef(null);
|
|
204
|
+
const cancel = useCallback(() => {
|
|
205
|
+
if (run.current) {
|
|
206
|
+
run.current.cancelled = true;
|
|
207
|
+
run.current.handle?.cancel();
|
|
208
|
+
run.current = null;
|
|
209
|
+
}
|
|
210
|
+
setState({ phase: "idle" });
|
|
211
|
+
}, []);
|
|
212
|
+
useEffect(
|
|
213
|
+
() => () => {
|
|
214
|
+
if (run.current) {
|
|
215
|
+
run.current.cancelled = true;
|
|
216
|
+
run.current.handle?.cancel();
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
[]
|
|
220
|
+
);
|
|
221
|
+
const generate = useCallback(() => {
|
|
222
|
+
const synthesize = speech.synthesize;
|
|
223
|
+
if (!synthesize || !mediaProvider || run.current) return;
|
|
224
|
+
const current = { cancelled: false, handle: null };
|
|
225
|
+
run.current = current;
|
|
226
|
+
setState({ phase: "generating", done: 0, total: 1 });
|
|
227
|
+
void (async () => {
|
|
228
|
+
const [plan, { createAudioFileEncoder }] = await Promise.all([
|
|
229
|
+
planSpeech(sourceRef.current),
|
|
230
|
+
import("@bendyline/squisq-video-react/encoder")
|
|
231
|
+
]);
|
|
232
|
+
if (current.cancelled) return;
|
|
233
|
+
if (plan.segments.length === 0) throw new Error("There is nothing to narrate yet.");
|
|
234
|
+
const encoder = await createAudioFileEncoder({
|
|
235
|
+
format: "opus-webm",
|
|
236
|
+
sampleRate: 24e3,
|
|
237
|
+
channels: 1
|
|
238
|
+
});
|
|
239
|
+
try {
|
|
240
|
+
const spans = [];
|
|
241
|
+
let elapsed = 0;
|
|
242
|
+
let appending = Promise.resolve();
|
|
243
|
+
for (const [index, segment] of plan.segments.entries()) {
|
|
244
|
+
if (current.cancelled) return;
|
|
245
|
+
setState({ phase: "generating", done: index, total: plan.segments.length });
|
|
246
|
+
const handle = synthesize({ text: segment.text }, (event) => {
|
|
247
|
+
if (event.kind !== "chunk" || current.cancelled) return;
|
|
248
|
+
const { chunk } = event;
|
|
249
|
+
spans.push(spokenSpanForChunk(chunk, segment, elapsed));
|
|
250
|
+
elapsed += chunk.durationSec;
|
|
251
|
+
const samples = new Float32Array(chunk.pcm);
|
|
252
|
+
appending = appending.then(() => encoder.append([samples]));
|
|
253
|
+
});
|
|
254
|
+
current.handle = handle;
|
|
255
|
+
const result = await handle.done;
|
|
256
|
+
if (current.cancelled) return;
|
|
257
|
+
if (!result.ok) throw new Error(result.error.message);
|
|
258
|
+
}
|
|
259
|
+
await appending;
|
|
260
|
+
if (current.cancelled) return;
|
|
261
|
+
setState({ phase: "saving" });
|
|
262
|
+
const audio = await encoder.finish();
|
|
263
|
+
const savePlan = buildNarrationSavePlan({
|
|
264
|
+
script: plan.script,
|
|
265
|
+
alignment: buildTtsAlignment(plan.script, spans, elapsed),
|
|
266
|
+
durationSec: elapsed,
|
|
267
|
+
// Squisq's recorder extensions carry the dot (".webm").
|
|
268
|
+
audioExt: `.${encoder.extension}`,
|
|
269
|
+
cameraExt: null,
|
|
270
|
+
generator: TTS_NARRATION_GENERATOR
|
|
271
|
+
});
|
|
272
|
+
await executeNarrationSave(
|
|
273
|
+
savePlan,
|
|
274
|
+
{ audioBlob: audio, audioMime: encoder.mimeType, cameraBlob: null, cameraMime: null },
|
|
275
|
+
{
|
|
276
|
+
mediaProvider,
|
|
277
|
+
container: workspaceContainer,
|
|
278
|
+
getMarkdownSource: () => sourceRef.current,
|
|
279
|
+
setMarkdownSource,
|
|
280
|
+
bumpMediaRevision
|
|
281
|
+
}
|
|
282
|
+
);
|
|
283
|
+
if (current.cancelled) return;
|
|
284
|
+
run.current = null;
|
|
285
|
+
setState({ phase: "done", durationSec: elapsed });
|
|
286
|
+
} finally {
|
|
287
|
+
if (current.cancelled) await encoder.cancel().catch(() => void 0);
|
|
288
|
+
}
|
|
289
|
+
})().catch((error) => {
|
|
290
|
+
if (current.cancelled) return;
|
|
291
|
+
run.current = null;
|
|
292
|
+
setState({
|
|
293
|
+
phase: "error",
|
|
294
|
+
message: error instanceof Error ? error.message : "Generating narration failed."
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
}, [bumpMediaRevision, mediaProvider, setMarkdownSource, speech, workspaceContainer]);
|
|
298
|
+
const reset = useCallback(() => {
|
|
299
|
+
if (!run.current) setState({ phase: "idle" });
|
|
300
|
+
}, []);
|
|
301
|
+
return {
|
|
302
|
+
state,
|
|
303
|
+
available: Boolean(speech.synthesize && mediaProvider),
|
|
304
|
+
generate,
|
|
305
|
+
cancel,
|
|
306
|
+
reset
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export {
|
|
311
|
+
planSpeech,
|
|
312
|
+
hasDocumentNarration,
|
|
313
|
+
useGenerateNarration
|
|
314
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// src/Ai/ai-status.ts
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
function useAiStatus(ai) {
|
|
4
|
+
const [status, setStatus] = useState(null);
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
let active = true;
|
|
7
|
+
const unsubscribe = ai.onStatus((next) => {
|
|
8
|
+
if (active) setStatus(next);
|
|
9
|
+
});
|
|
10
|
+
void ai.status().then(
|
|
11
|
+
(initial) => {
|
|
12
|
+
if (active) setStatus((current) => current ?? initial);
|
|
13
|
+
},
|
|
14
|
+
() => {
|
|
15
|
+
if (active) {
|
|
16
|
+
setStatus({
|
|
17
|
+
kind: "error",
|
|
18
|
+
error: { code: "unknown", message: "AI status is unavailable." },
|
|
19
|
+
retryable: true
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
return () => {
|
|
25
|
+
active = false;
|
|
26
|
+
unsubscribe();
|
|
27
|
+
};
|
|
28
|
+
}, [ai]);
|
|
29
|
+
return status;
|
|
30
|
+
}
|
|
31
|
+
function unavailableSentence(status) {
|
|
32
|
+
if (!status) return "Checking AI connection\u2026";
|
|
33
|
+
if (status.kind === "connecting") return "AI is getting ready\u2026";
|
|
34
|
+
if (status.kind === "error") return status.error.message;
|
|
35
|
+
if (status.kind === "ready" && !status.model) return "Add a model in Settings to use AI.";
|
|
36
|
+
if (status.kind === "unavailable") {
|
|
37
|
+
if (status.reason === "opt-out") return "Turn on AI features in Settings.";
|
|
38
|
+
if (status.reason === "disconnected") return "Built-in AI is starting\u2026";
|
|
39
|
+
if (status.reason === "not-installed") return "Built-in AI is not available in this build.";
|
|
40
|
+
if (status.reason === "not-running") return "Built-in AI is restarting\u2026";
|
|
41
|
+
return "AI is not available in this build.";
|
|
42
|
+
}
|
|
43
|
+
return "";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export {
|
|
47
|
+
useAiStatus,
|
|
48
|
+
unavailableSentence
|
|
49
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// src/Speech/speech-input-provider.ts
|
|
2
|
+
function toReadiness(readiness) {
|
|
3
|
+
return {
|
|
4
|
+
state: readiness.state,
|
|
5
|
+
...readiness.reason ? { reason: readiness.reason } : {}
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
function abortable(promise, signal) {
|
|
9
|
+
if (signal.aborted) return Promise.reject(new DOMException("Aborted", "AbortError"));
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const onAbort = () => reject(new DOMException("Aborted", "AbortError"));
|
|
12
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
13
|
+
promise.then(
|
|
14
|
+
(value) => {
|
|
15
|
+
signal.removeEventListener("abort", onAbort);
|
|
16
|
+
resolve(value);
|
|
17
|
+
},
|
|
18
|
+
(error) => {
|
|
19
|
+
signal.removeEventListener("abort", onAbort);
|
|
20
|
+
reject(error);
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function createSpeechInputProvider(speech, options) {
|
|
26
|
+
const transcribe = speech.transcribe;
|
|
27
|
+
return {
|
|
28
|
+
id: "docblocks-dictation",
|
|
29
|
+
label: "Dictation",
|
|
30
|
+
async status() {
|
|
31
|
+
if (!transcribe) return { state: "unavailable" };
|
|
32
|
+
return toReadiness((await speech.status()).stt);
|
|
33
|
+
},
|
|
34
|
+
onStatus(listener) {
|
|
35
|
+
return speech.onStatus((status) => listener(toReadiness(status.stt)));
|
|
36
|
+
},
|
|
37
|
+
async prepare(signal) {
|
|
38
|
+
const result = await abortable(speech.prepare("stt"), signal);
|
|
39
|
+
if (!result.ok) throw new Error(result.error.message);
|
|
40
|
+
},
|
|
41
|
+
requestSetup: options.onRequestSetup,
|
|
42
|
+
async transcribe(wav, { prompt, language, signal }) {
|
|
43
|
+
if (!transcribe) throw new Error("Dictation is not available.");
|
|
44
|
+
const result = await abortable(
|
|
45
|
+
transcribe({
|
|
46
|
+
audio: wav,
|
|
47
|
+
mimeType: "audio/wav",
|
|
48
|
+
...prompt ? { prompt } : {},
|
|
49
|
+
...language ? { language } : {}
|
|
50
|
+
}),
|
|
51
|
+
signal
|
|
52
|
+
);
|
|
53
|
+
if (!result.ok) throw new Error(result.error.message);
|
|
54
|
+
return { text: result.value.text };
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export {
|
|
60
|
+
createSpeechInputProvider
|
|
61
|
+
};
|
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
// src/Export/image-save.ts
|
|
2
|
-
function createImageSaveOutput(adapter) {
|
|
3
|
-
return async (blob, filename) => {
|
|
4
|
-
const target = await adapter.pickTarget(filename, null);
|
|
5
|
-
if (!target) return false;
|
|
6
|
-
return await adapter.saveBlob(blob, filename, target) !== null;
|
|
7
|
-
};
|
|
8
|
-
}
|
|
9
|
-
|
|
10
1
|
// src/Export/export-destination.ts
|
|
11
2
|
function updateExportTargetExtension(targetPath, suggestedFilename) {
|
|
12
3
|
const nextExtension = extensionOf(suggestedFilename);
|
|
@@ -36,6 +27,5 @@ function firstSuffixIndex(value) {
|
|
|
36
27
|
}
|
|
37
28
|
|
|
38
29
|
export {
|
|
39
|
-
createImageSaveOutput,
|
|
40
30
|
updateExportTargetExtension
|
|
41
31
|
};
|