wip: incremental ASR
This commit is contained in:
1 parent
0b44d7e1bf
commit
65855ace6c
2 files changed
+73
-19
No files matched your search
+17
-14
@@ -309,8 +309,10 @@ export default function asrExtension(pi: ExtensionAPI) {
|
||||
* gives no key-release event for hold-to-talk without the Kitty keyboard protocol.
|
||||
* Shared module state so the /dictate command and the ctrl+shift+d shortcut drive one session.
|
||||
*/
|
||||
let active: { rec: MicRecording; submit: boolean } | null = null;
|
||||
let active: { rec: MicRecording; submit: boolean; base: string } | null = null;
|
||||
let starting = false;
|
||||
/** Compose the composer text from the user's pre-existing draft + the dictated tail (idempotent: never appended). */
|
||||
const joinText = (base: string, t: string): string => { const tail = t.trim(); return base ? (tail ? `${base} ${tail}` : base) : tail; };
|
||||
|
||||
async function resolveDevice(d?: string | null): Promise<string | null> {
|
||||
if (!d) return null;
|
||||
@@ -325,23 +327,20 @@ export default function asrExtension(pi: ExtensionAPI) {
|
||||
async function toggleDictation(ctx: ExtensionContext, opts: { device?: string | null; language?: string; submit?: boolean } = {}): Promise<void> {
|
||||
if (!ctx.hasUI) { ctx.ui.notify("dictation needs the interactive TUI", "error"); return; }
|
||||
|
||||
// second press while live => stop, flush, paste the final transcript.
|
||||
// second press while live => stop, flush, finalise the composer with the deduped transcript.
|
||||
if (active) {
|
||||
const { rec, submit } = active; active = null;
|
||||
const { rec, submit, base } = active; active = null;
|
||||
ctx.ui.setStatus("asr", undefined);
|
||||
try {
|
||||
const res = await rec.stop();
|
||||
const text = res.text.trim();
|
||||
if (rec.error) { ctx.ui.notify(`dictation error: ${rec.error}`, "error"); return; }
|
||||
if (!text) { ctx.ui.notify("no speech detected (speak a little longer, or run asr_health)", "warning"); return; }
|
||||
if (submit) pi.sendUserMessage(text);
|
||||
else {
|
||||
const cur = (ctx.ui.getEditorText() ?? "").replace(/\s+$/, "");
|
||||
ctx.ui.setEditorText(cur ? `${cur} ${text}` : text);
|
||||
}
|
||||
if (rec.error) { ctx.ui.setEditorText(base); ctx.ui.notify(`dictation error: ${rec.error}`, "error"); return; }
|
||||
if (!text) { ctx.ui.setEditorText(base); ctx.ui.notify("no speech detected (speak a little longer, or run asr_health)", "warning"); return; }
|
||||
if (submit) { ctx.ui.setEditorText(base); pi.sendUserMessage(text); }
|
||||
else ctx.ui.setEditorText(joinText(base, text)); // identical to the last live paint — replace, not append
|
||||
ctx.ui.notify(`dictated ${wordCount(text)} words (${res.seconds}s) → ${submit ? "sent" : "prompt"}`, "info");
|
||||
} catch (e: any) {
|
||||
rec.cancel();
|
||||
rec.cancel(); ctx.ui.setEditorText(base);
|
||||
ctx.ui.notify(`dictation failed: ${e?.message ?? e}`, "error");
|
||||
}
|
||||
return;
|
||||
@@ -354,13 +353,16 @@ export default function asrExtension(pi: ExtensionAPI) {
|
||||
try { device = await resolveDevice(opts.device); }
|
||||
catch (e: any) { starting = false; ctx.ui.notify(`dictation: ${e?.message ?? e}`, "error"); return; }
|
||||
|
||||
const base = (ctx.ui.getEditorText() ?? "").replace(/\s+$/, "");
|
||||
const client = new RealtimeAsrClient({ language, punctuation: true });
|
||||
ctx.ui.setStatus("asr", "● listening …");
|
||||
let rec: MicRecording;
|
||||
try {
|
||||
rec = await client.recordMic({
|
||||
device,
|
||||
onDelta: (t) => { if (active) ctx.ui.setStatus("asr", `● ${t || "…listening"}`); },
|
||||
// stream word-by-word into the composer: setEditorText REPLACES (never appends), and the client's
|
||||
// liveText is a per-segment cumulative partial, so each paint just overwrites the growing tail — no dupes.
|
||||
onLive: (live) => { if (active) { const tail = live.trim(); ctx.ui.setEditorText(joinText(base, tail)); ctx.ui.setStatus("asr", `● ${tail.slice(-70) || "…listening"}`); } },
|
||||
});
|
||||
} catch (e: any) {
|
||||
starting = false; ctx.ui.setStatus("asr", undefined);
|
||||
@@ -368,8 +370,9 @@ export default function asrExtension(pi: ExtensionAPI) {
|
||||
return;
|
||||
}
|
||||
starting = false;
|
||||
active = { rec, submit: !!opts.submit };
|
||||
ctx.ui.notify(`🎙 recording${device ? ` from “${device}”` : " (default mic)"} — press ctrl+shift+d or /dictate again to stop`, "info");
|
||||
active = { rec, submit: !!opts.submit, base };
|
||||
ctx.ui.setEditorText(joinText(base, rec.liveText())); // show current state immediately (deltas are cumulative, so nothing is lost)
|
||||
ctx.ui.notify(`🎙 recording${device ? ` from “${device}”` : " (default mic)"} — speech streams into the prompt; ctrl+shift+d or /dictate to stop`, "info");
|
||||
}
|
||||
|
||||
pi.registerShortcut("ctrl+shift+d", {
|
||||
|
||||
Reference in new issue
Block a user