@agent-native/core 0.76.12 → 0.76.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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +12 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +7 -4
- package/corpus/templates/clips/app/components/recorder/camera-visualizer.tsx +88 -10
- package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +64 -0
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +94 -9
- package/corpus/templates/clips/app/lib/camera-blur.ts +341 -0
- package/corpus/templates/clips/app/lib/camera-composite.ts +6 -1
- package/corpus/templates/clips/app/lib/recorder-preferences.ts +15 -0
- package/corpus/templates/clips/app/routes/record.tsx +13 -0
- package/corpus/templates/clips/desktop/src/app.tsx +101 -69
- package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +109 -11
- package/corpus/templates/clips/desktop/src/styles.css +19 -4
- package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +1 -1
- package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +143 -86
- package/corpus/templates/clips/package.json +1 -0
- package/corpus/templates/clips/vite.config.ts +38 -1
- package/dist/agent/production-agent.d.ts +7 -4
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +7 -4
- package/dist/agent/production-agent.js.map +1 -1
- package/package.json +1 -1
|
@@ -295,7 +295,7 @@ function installAuthFetchInterceptor(): void {
|
|
|
295
295
|
}
|
|
296
296
|
|
|
297
297
|
type ByokVoiceProvider = Extract<VoiceProvider, "gemini" | "groq">;
|
|
298
|
-
type VoiceProviderMode = "native" | "builder" | "byok";
|
|
298
|
+
type VoiceProviderMode = "native" | "whisper" | "builder" | "byok";
|
|
299
299
|
type MacosPrivacyPane =
|
|
300
300
|
| "camera"
|
|
301
301
|
| "microphone"
|
|
@@ -368,6 +368,7 @@ function isByokVoiceProvider(value: VoiceProvider): value is ByokVoiceProvider {
|
|
|
368
368
|
function voiceProviderMode(value: VoiceProvider): VoiceProviderMode {
|
|
369
369
|
if (isByokVoiceProvider(value)) return "byok";
|
|
370
370
|
if (value === "builder" || value === "builder-gemini") return "builder";
|
|
371
|
+
if (value === "whisper") return "whisper";
|
|
371
372
|
return "native";
|
|
372
373
|
}
|
|
373
374
|
|
|
@@ -378,6 +379,7 @@ function normalizeVoiceProvider(value: string): VoiceProvider {
|
|
|
378
379
|
if (value === "macos-native" && !isMacPlatform()) return "browser";
|
|
379
380
|
return value === "browser" ||
|
|
380
381
|
value === "macos-native" ||
|
|
382
|
+
value === "whisper" ||
|
|
381
383
|
value === "builder-gemini" ||
|
|
382
384
|
value === "gemini" ||
|
|
383
385
|
value === "groq"
|
|
@@ -2987,7 +2989,11 @@ function Setup({
|
|
|
2987
2989
|
}).catch((err) =>
|
|
2988
2990
|
console.error("[settings] set_feature_config failed", err),
|
|
2989
2991
|
);
|
|
2990
|
-
if (enabled)
|
|
2992
|
+
if (enabled) {
|
|
2993
|
+
triggerWhisperDownload();
|
|
2994
|
+
} else if (voiceProvider === "whisper") {
|
|
2995
|
+
onVoiceProviderChange(nativeVoiceProvider());
|
|
2996
|
+
}
|
|
2991
2997
|
}
|
|
2992
2998
|
|
|
2993
2999
|
function setLaunchAtLoginEnabled(enabled: boolean) {
|
|
@@ -3223,6 +3229,7 @@ function Setup({
|
|
|
3223
3229
|
native: isMacPlatform()
|
|
3224
3230
|
? "Uses macOS on-device speech recognition for the fastest free dictation."
|
|
3225
3231
|
: "Uses the browser's built-in speech recognition when available.",
|
|
3232
|
+
whisper: "Uses the local Whisper model for offline AI transcription.",
|
|
3226
3233
|
builder:
|
|
3227
3234
|
"Uses Builder.io for fast cleanup. No separate provider key needed.",
|
|
3228
3235
|
byok: "Use your own provider key for cleanup.",
|
|
@@ -3245,6 +3252,9 @@ function Setup({
|
|
|
3245
3252
|
setApiKeyMessage(null);
|
|
3246
3253
|
if (mode === "native") {
|
|
3247
3254
|
onVoiceProviderChange(nativeVoiceProvider());
|
|
3255
|
+
} else if (mode === "whisper") {
|
|
3256
|
+
onVoiceProviderChange("whisper");
|
|
3257
|
+
if (!whisperModelEnabled) setWhisperModelEnabled(true);
|
|
3248
3258
|
} else if (mode === "builder") {
|
|
3249
3259
|
onVoiceProviderChange("builder-gemini");
|
|
3250
3260
|
} else {
|
|
@@ -3333,6 +3343,7 @@ function Setup({
|
|
|
3333
3343
|
const providerWarning: string | null = (() => {
|
|
3334
3344
|
if (providerStatusLoading || !providerStatus) return null;
|
|
3335
3345
|
if (selectedMode === "native") return null;
|
|
3346
|
+
if (selectedMode === "whisper") return null;
|
|
3336
3347
|
if (selectedMode === "builder") {
|
|
3337
3348
|
return providerStatus.builder
|
|
3338
3349
|
? null
|
|
@@ -3358,6 +3369,8 @@ function Setup({
|
|
|
3358
3369
|
<h2>Settings</h2>
|
|
3359
3370
|
</div>
|
|
3360
3371
|
|
|
3372
|
+
<div className="setup-section-heading">General</div>
|
|
3373
|
+
|
|
3361
3374
|
<div className="setup-section">
|
|
3362
3375
|
<SettingLabel
|
|
3363
3376
|
label="Clips server URL"
|
|
@@ -3419,6 +3432,23 @@ function Setup({
|
|
|
3419
3432
|
</div>
|
|
3420
3433
|
</div>
|
|
3421
3434
|
|
|
3435
|
+
<div className="setup-section-heading">Permissions</div>
|
|
3436
|
+
|
|
3437
|
+
<div className="setup-section">
|
|
3438
|
+
<ReadinessPanel
|
|
3439
|
+
mode="screen-camera"
|
|
3440
|
+
cameraOn={true}
|
|
3441
|
+
micOn={true}
|
|
3442
|
+
includeVoicePaste={voiceEnabled}
|
|
3443
|
+
includeFnMonitoring={fnShortcutSelected}
|
|
3444
|
+
open={readinessOpen}
|
|
3445
|
+
onOpenChange={setReadinessOpen}
|
|
3446
|
+
onOpenPermission={openPrivacySettings}
|
|
3447
|
+
/>
|
|
3448
|
+
</div>
|
|
3449
|
+
|
|
3450
|
+
<div className="setup-section-heading">Recording</div>
|
|
3451
|
+
|
|
3422
3452
|
<details className="setup-advanced">
|
|
3423
3453
|
<summary className="setup-advanced-summary">Advanced recording</summary>
|
|
3424
3454
|
<div className="setup-advanced-body">
|
|
@@ -3502,19 +3532,26 @@ function Setup({
|
|
|
3502
3532
|
</details>
|
|
3503
3533
|
|
|
3504
3534
|
<div className="setup-section">
|
|
3505
|
-
<
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3535
|
+
<SettingLabel
|
|
3536
|
+
label="Open Clips shortcut"
|
|
3537
|
+
hint="Optional extra global shortcut for opening the tray popover. Cmd+Shift+L remains available."
|
|
3538
|
+
/>
|
|
3539
|
+
<ShortcutRecorder
|
|
3540
|
+
value={popoverCustomShortcut}
|
|
3541
|
+
placeholder="Record shortcut"
|
|
3542
|
+
onChange={onPopoverCustomShortcutChange}
|
|
3543
|
+
/>
|
|
3544
|
+
<p className="setup-hint">
|
|
3545
|
+
Use a modifier combination like Cmd+Shift+K. Leave empty to use only
|
|
3546
|
+
Cmd+Shift+L.
|
|
3547
|
+
</p>
|
|
3548
|
+
{shortcutRegistrationError ? (
|
|
3549
|
+
<p className="setup-warning">{shortcutRegistrationError}</p>
|
|
3550
|
+
) : null}
|
|
3516
3551
|
</div>
|
|
3517
3552
|
|
|
3553
|
+
<div className="setup-section-heading">Meetings</div>
|
|
3554
|
+
|
|
3518
3555
|
<div className="setup-section">
|
|
3519
3556
|
<div className="setup-toggle-row">
|
|
3520
3557
|
<SettingLabel
|
|
@@ -3557,25 +3594,6 @@ function Setup({
|
|
|
3557
3594
|
</p>
|
|
3558
3595
|
</div>
|
|
3559
3596
|
|
|
3560
|
-
<div className="setup-section">
|
|
3561
|
-
<div className="setup-toggle-row">
|
|
3562
|
-
<SettingLabel
|
|
3563
|
-
label="Whisper model"
|
|
3564
|
-
hint="Local AI model for offline meeting transcription. Captures both your mic and other speakers — no API key required."
|
|
3565
|
-
/>
|
|
3566
|
-
<Switch
|
|
3567
|
-
on={whisperModelEnabled}
|
|
3568
|
-
onChange={setWhisperModelEnabled}
|
|
3569
|
-
label="Enable Whisper model"
|
|
3570
|
-
/>
|
|
3571
|
-
</div>
|
|
3572
|
-
<WhisperModelStatusRow
|
|
3573
|
-
status={whisperStatus}
|
|
3574
|
-
enabled={whisperModelEnabled}
|
|
3575
|
-
onDownload={triggerWhisperDownload}
|
|
3576
|
-
/>
|
|
3577
|
-
</div>
|
|
3578
|
-
|
|
3579
3597
|
<div className="setup-section">
|
|
3580
3598
|
<div className="setup-toggle-row">
|
|
3581
3599
|
<SettingLabel
|
|
@@ -3592,40 +3610,41 @@ function Setup({
|
|
|
3592
3610
|
</>
|
|
3593
3611
|
) : null}
|
|
3594
3612
|
|
|
3613
|
+
<div className="setup-section-heading">Whisper</div>
|
|
3614
|
+
|
|
3595
3615
|
<div className="setup-section">
|
|
3596
|
-
<
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3616
|
+
<div className="setup-toggle-row">
|
|
3617
|
+
<SettingLabel
|
|
3618
|
+
label="Whisper model"
|
|
3619
|
+
hint="Local AI model for offline transcription (dictation and meetings). No API key required."
|
|
3620
|
+
/>
|
|
3621
|
+
<Switch
|
|
3622
|
+
on={whisperModelEnabled}
|
|
3623
|
+
onChange={setWhisperModelEnabled}
|
|
3624
|
+
label="Enable Whisper model"
|
|
3625
|
+
/>
|
|
3626
|
+
</div>
|
|
3627
|
+
<WhisperModelStatusRow
|
|
3628
|
+
status={whisperStatus}
|
|
3629
|
+
enabled={whisperModelEnabled}
|
|
3630
|
+
onDownload={triggerWhisperDownload}
|
|
3604
3631
|
/>
|
|
3605
|
-
<p className="setup-hint">
|
|
3606
|
-
Use a modifier combination like Cmd+Shift+K. Leave empty to use only
|
|
3607
|
-
Cmd+Shift+L.
|
|
3608
|
-
</p>
|
|
3609
|
-
{shortcutRegistrationError ? (
|
|
3610
|
-
<p className="setup-warning">{shortcutRegistrationError}</p>
|
|
3611
|
-
) : null}
|
|
3612
3632
|
</div>
|
|
3613
3633
|
|
|
3634
|
+
<div className="setup-section-heading">Dictation</div>
|
|
3635
|
+
|
|
3614
3636
|
<div className="setup-section">
|
|
3615
|
-
<
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
onOpenChange={setReadinessOpen}
|
|
3627
|
-
onOpenPermission={openPrivacySettings}
|
|
3628
|
-
/>
|
|
3637
|
+
<div className="setup-toggle-row">
|
|
3638
|
+
<SettingLabel
|
|
3639
|
+
label="Voice dictation"
|
|
3640
|
+
hint="Speak to type anywhere on your Mac. Turn off to disable globally and remove the keyboard shortcuts."
|
|
3641
|
+
/>
|
|
3642
|
+
<Switch
|
|
3643
|
+
on={voiceEnabled}
|
|
3644
|
+
onChange={setVoiceEnabled}
|
|
3645
|
+
label="Enable voice dictation"
|
|
3646
|
+
/>
|
|
3647
|
+
</div>
|
|
3629
3648
|
</div>
|
|
3630
3649
|
|
|
3631
3650
|
{voiceEnabled ? (
|
|
@@ -3645,10 +3664,21 @@ function Setup({
|
|
|
3645
3664
|
}
|
|
3646
3665
|
>
|
|
3647
3666
|
<option value="native">On-device (free, fast)</option>
|
|
3667
|
+
<option value="whisper" disabled={!whisperModelEnabled}>
|
|
3668
|
+
{whisperModelEnabled
|
|
3669
|
+
? "Local Whisper (offline AI)"
|
|
3670
|
+
: "Local Whisper — enable Whisper model first"}
|
|
3671
|
+
</option>
|
|
3648
3672
|
<option value="builder">Builder.io</option>
|
|
3649
3673
|
<option value="byok">Add your own key</option>
|
|
3650
3674
|
</select>
|
|
3651
3675
|
<p className="setup-hint">{providerHint[selectedMode]}</p>
|
|
3676
|
+
{selectedMode === "whisper" && !whisperModelEnabled ? (
|
|
3677
|
+
<p className="setup-warning">
|
|
3678
|
+
Whisper model is disabled. Enable it in the Whisper section
|
|
3679
|
+
above.
|
|
3680
|
+
</p>
|
|
3681
|
+
) : null}
|
|
3652
3682
|
{providerWarning ? (
|
|
3653
3683
|
<p className="setup-warning">{providerWarning}</p>
|
|
3654
3684
|
) : null}
|
|
@@ -3697,9 +3727,10 @@ function Setup({
|
|
|
3697
3727
|
}}
|
|
3698
3728
|
placeholder={
|
|
3699
3729
|
providerStatus?.[byokProvider]
|
|
3700
|
-
? "
|
|
3701
|
-
: `Paste ${keyForByokProvider(byokProvider)}`
|
|
3730
|
+
? "Key is saved — paste to rotate"
|
|
3731
|
+
: `Paste ${keyForByokProvider(byokProvider)} here`
|
|
3702
3732
|
}
|
|
3733
|
+
className="setup-key-input"
|
|
3703
3734
|
/>
|
|
3704
3735
|
<button
|
|
3705
3736
|
type="button"
|
|
@@ -3733,7 +3764,7 @@ function Setup({
|
|
|
3733
3764
|
</div>
|
|
3734
3765
|
) : null}
|
|
3735
3766
|
|
|
3736
|
-
{selectedMode !== "native" ? (
|
|
3767
|
+
{selectedMode !== "native" && selectedMode !== "whisper" ? (
|
|
3737
3768
|
<div className="setup-section">
|
|
3738
3769
|
<SettingLabel
|
|
3739
3770
|
label="Custom instructions"
|
|
@@ -3819,7 +3850,10 @@ function Setup({
|
|
|
3819
3850
|
</div>
|
|
3820
3851
|
</>
|
|
3821
3852
|
) : null}
|
|
3822
|
-
|
|
3853
|
+
|
|
3854
|
+
<div className="setup-section-heading">Debug</div>
|
|
3855
|
+
|
|
3856
|
+
<div className="setup-account setup-account--no-border">
|
|
3823
3857
|
<button
|
|
3824
3858
|
type="button"
|
|
3825
3859
|
className="link-button"
|
|
@@ -3906,10 +3940,8 @@ function WhisperModelStatusRow({
|
|
|
3906
3940
|
return (
|
|
3907
3941
|
<div className="whisper-status whisper-status-ready">
|
|
3908
3942
|
<IconCircleCheck size={13} className="whisper-status-icon" />
|
|
3909
|
-
<span>
|
|
3910
|
-
|
|
3911
|
-
<span className="whisper-status-path">{status.path}</span>
|
|
3912
|
-
</span>
|
|
3943
|
+
<span>Ready · {status.totalMb} MB</span>
|
|
3944
|
+
<span className="whisper-status-path">{status.path}</span>
|
|
3913
3945
|
</div>
|
|
3914
3946
|
);
|
|
3915
3947
|
}
|
|
@@ -32,6 +32,7 @@ export type VoiceProvider =
|
|
|
32
32
|
| "auto"
|
|
33
33
|
| "browser"
|
|
34
34
|
| "macos-native"
|
|
35
|
+
| "whisper"
|
|
35
36
|
| "builder-gemini"
|
|
36
37
|
| "builder"
|
|
37
38
|
| "gemini"
|
|
@@ -82,8 +83,10 @@ interface VoiceSession {
|
|
|
82
83
|
// webkitSpeechRecognition (works in Safari and Chromium WebViews,
|
|
83
84
|
// broken in Tauri WKWebView). "native" sessions drive Apple's
|
|
84
85
|
// SFSpeechRecognizer + AVAudioEngine through Tauri commands —
|
|
85
|
-
// on-device, real-time partials, free, macOS-only.
|
|
86
|
-
|
|
86
|
+
// on-device, real-time partials, free, macOS-only. "whisper" sessions
|
|
87
|
+
// drive the local whisper.cpp engine via audio_transcription_* commands,
|
|
88
|
+
// mic-only (captureSystem: false), no API key required.
|
|
89
|
+
kind: "server" | "browser" | "native" | "whisper";
|
|
87
90
|
// server-only fields
|
|
88
91
|
stream: MediaStream | null;
|
|
89
92
|
recorder: MediaRecorder | null;
|
|
@@ -496,6 +499,7 @@ export function installDesktopVoiceDictation(
|
|
|
496
499
|
const resolveProvider = async (): Promise<
|
|
497
500
|
| { kind: "browser"; cleanupProvider?: ServerVoiceProvider }
|
|
498
501
|
| { kind: "native"; cleanupProvider?: ServerVoiceProvider }
|
|
502
|
+
| { kind: "whisper"; cleanupProvider?: ServerVoiceProvider }
|
|
499
503
|
| {
|
|
500
504
|
kind: "server";
|
|
501
505
|
providerPref: ServerVoiceProvider;
|
|
@@ -515,6 +519,7 @@ export function installDesktopVoiceDictation(
|
|
|
515
519
|
return { kind: "browser" };
|
|
516
520
|
}
|
|
517
521
|
if (provider === "macos-native") return { kind: "native" };
|
|
522
|
+
if (provider === "whisper") return { kind: "whisper" };
|
|
518
523
|
if (provider !== "auto") {
|
|
519
524
|
const cleanupProvider =
|
|
520
525
|
provider === "builder" ? "builder-gemini" : provider;
|
|
@@ -605,6 +610,8 @@ export function installDesktopVoiceDictation(
|
|
|
605
610
|
await startBrowser(resolved.cleanupProvider);
|
|
606
611
|
} else if (resolved.kind === "native") {
|
|
607
612
|
await startNative(resolved.cleanupProvider);
|
|
613
|
+
} else if (resolved.kind === "whisper") {
|
|
614
|
+
await startWhisper(resolved.cleanupProvider);
|
|
608
615
|
} else {
|
|
609
616
|
await startServer(resolved.providerPref);
|
|
610
617
|
}
|
|
@@ -943,6 +950,80 @@ export function installDesktopVoiceDictation(
|
|
|
943
950
|
}
|
|
944
951
|
};
|
|
945
952
|
|
|
953
|
+
/**
|
|
954
|
+
* Whisper path: local whisper.cpp engine via audio_transcription_* Tauri
|
|
955
|
+
* commands. Mic-only (captureSystem: false) — same linger/finalize flow
|
|
956
|
+
* as the native path, same voice:*-transcript events from Rust.
|
|
957
|
+
*/
|
|
958
|
+
const startWhisper = async (cleanupProvider?: ServerVoiceProvider) => {
|
|
959
|
+
console.log(
|
|
960
|
+
"[voice-dictation] startWhisper: invoke audio_transcription_start",
|
|
961
|
+
);
|
|
962
|
+
try {
|
|
963
|
+
// Open the mic BEFORE showing the bar so audio is capturing by the time
|
|
964
|
+
// the user sees the recording state and starts speaking. The inverse order
|
|
965
|
+
// (bar first, then start) causes the mic to open ~100-300ms late and the
|
|
966
|
+
// first spoken words are lost inside audio_transcription_start's
|
|
967
|
+
// ensure_model + create_state + start_raw_mic_capture sequence.
|
|
968
|
+
await invoke("audio_transcription_start", {
|
|
969
|
+
meetingId: null,
|
|
970
|
+
locale: navigator.language || "en-US",
|
|
971
|
+
micDeviceId: concreteMediaDeviceId(micDeviceId) || null,
|
|
972
|
+
micDeviceLabel: micDeviceLabel || null,
|
|
973
|
+
captureSystem: false,
|
|
974
|
+
});
|
|
975
|
+
console.log("[voice-dictation] audio_transcription_start ok");
|
|
976
|
+
if (disposed || stopRequestedBeforeReady) {
|
|
977
|
+
invoke("audio_transcription_stop").catch(() => {});
|
|
978
|
+
abortPendingStart();
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
await invoke("show_flow_bar");
|
|
982
|
+
if (disposed || stopRequestedBeforeReady) {
|
|
983
|
+
invoke("audio_transcription_stop").catch(() => {});
|
|
984
|
+
abortPendingStart();
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
setFlowState("recording");
|
|
988
|
+
emit("voice:partial-transcript", { text: "" }).catch(() => {});
|
|
989
|
+
const next: VoiceSession = {
|
|
990
|
+
kind: "whisper",
|
|
991
|
+
stream: null,
|
|
992
|
+
recorder: null,
|
|
993
|
+
chunks: [],
|
|
994
|
+
audioContext: null,
|
|
995
|
+
analyser: null,
|
|
996
|
+
raf: null,
|
|
997
|
+
mimeType: "",
|
|
998
|
+
recognition: null,
|
|
999
|
+
browserTranscript: "",
|
|
1000
|
+
lastResultAt: 0,
|
|
1001
|
+
startedAt: Date.now(),
|
|
1002
|
+
stopping: false,
|
|
1003
|
+
transcribeAbort: null,
|
|
1004
|
+
cancelled: false,
|
|
1005
|
+
cleanupProvider: cleanupProvider ?? null,
|
|
1006
|
+
};
|
|
1007
|
+
session = next;
|
|
1008
|
+
startInFlight = false;
|
|
1009
|
+
startSyntheticMeter(next);
|
|
1010
|
+
if (stopRequestedBeforeReady) {
|
|
1011
|
+
stop();
|
|
1012
|
+
}
|
|
1013
|
+
} catch (err) {
|
|
1014
|
+
console.error("[voice-dictation] startWhisper failed", err);
|
|
1015
|
+
startInFlight = false;
|
|
1016
|
+
stopRequestedBeforeReady = false;
|
|
1017
|
+
session = null;
|
|
1018
|
+
setFlowState("error");
|
|
1019
|
+
window.setTimeout(() => {
|
|
1020
|
+
if (disposed || session) return;
|
|
1021
|
+
setFlowState("idle");
|
|
1022
|
+
invoke("hide_flow_bar").catch(() => {});
|
|
1023
|
+
}, 800);
|
|
1024
|
+
}
|
|
1025
|
+
};
|
|
1026
|
+
|
|
946
1027
|
/**
|
|
947
1028
|
* Browser-path: real-time on-device transcription via WKWebView's
|
|
948
1029
|
* webkitSpeechRecognition. No server round-trip — text is ready the
|
|
@@ -1172,6 +1253,13 @@ export function installDesktopVoiceDictation(
|
|
|
1172
1253
|
invoke("native_speech_cancel").catch((err) => {
|
|
1173
1254
|
console.warn("[voice-dictation] native_speech_cancel failed:", err);
|
|
1174
1255
|
});
|
|
1256
|
+
} else if (current.kind === "whisper") {
|
|
1257
|
+
invoke("audio_transcription_stop").catch((err) => {
|
|
1258
|
+
console.warn(
|
|
1259
|
+
"[voice-dictation] audio_transcription_stop (cancel) failed:",
|
|
1260
|
+
err,
|
|
1261
|
+
);
|
|
1262
|
+
});
|
|
1175
1263
|
} else {
|
|
1176
1264
|
try {
|
|
1177
1265
|
current.recognition?.abort();
|
|
@@ -1225,6 +1313,8 @@ export function installDesktopVoiceDictation(
|
|
|
1225
1313
|
}
|
|
1226
1314
|
} else if (current.kind === "native") {
|
|
1227
1315
|
invoke("native_speech_cancel").catch(() => {});
|
|
1316
|
+
} else if (current.kind === "whisper") {
|
|
1317
|
+
invoke("audio_transcription_stop").catch(() => {});
|
|
1228
1318
|
}
|
|
1229
1319
|
cleanup(current);
|
|
1230
1320
|
return;
|
|
@@ -1232,15 +1322,19 @@ export function installDesktopVoiceDictation(
|
|
|
1232
1322
|
try {
|
|
1233
1323
|
if (current.kind === "server") {
|
|
1234
1324
|
current.recorder?.stop();
|
|
1235
|
-
} else if (current.kind === "native") {
|
|
1236
|
-
// NATIVE PATH: dismiss the pill *immediately* (snappy UX)
|
|
1237
|
-
// leave the transcript chip lingering. Tell Rust to
|
|
1238
|
-
// so
|
|
1325
|
+
} else if (current.kind === "native" || current.kind === "whisper") {
|
|
1326
|
+
// NATIVE / WHISPER PATH: dismiss the pill *immediately* (snappy UX)
|
|
1327
|
+
// but leave the transcript chip lingering. Tell Rust to end the
|
|
1328
|
+
// engine so it can deliver its final hypothesis. When
|
|
1239
1329
|
// `voice:final-transcript` lands (or after a safety timeout),
|
|
1240
1330
|
// paste the text and let the chip sit for ~1s with the final
|
|
1241
1331
|
// word visible — like a notification fading — then dismiss.
|
|
1242
|
-
|
|
1243
|
-
|
|
1332
|
+
const stopCmd =
|
|
1333
|
+
current.kind === "whisper"
|
|
1334
|
+
? "audio_transcription_stop"
|
|
1335
|
+
: "native_speech_stop";
|
|
1336
|
+
invoke(stopCmd).catch((err) => {
|
|
1337
|
+
console.warn(`[voice-dictation] ${stopCmd} failed:`, err);
|
|
1244
1338
|
});
|
|
1245
1339
|
// Pill goes RIGHT NOW. The flow-bar window stays open (we'll
|
|
1246
1340
|
// hide it after the linger) but renders only the transcript
|
|
@@ -1576,7 +1670,8 @@ export function installDesktopVoiceDictation(
|
|
|
1576
1670
|
// don't re-emit it here.
|
|
1577
1671
|
onPartialTranscript(({ text }) => {
|
|
1578
1672
|
const current = session;
|
|
1579
|
-
if (!current || current.kind !== "native")
|
|
1673
|
+
if (!current || (current.kind !== "native" && current.kind !== "whisper"))
|
|
1674
|
+
return;
|
|
1580
1675
|
if (current.cancelled || current.stopping) return;
|
|
1581
1676
|
current.browserTranscript = text.trim();
|
|
1582
1677
|
})
|
|
@@ -1590,7 +1685,9 @@ export function installDesktopVoiceDictation(
|
|
|
1590
1685
|
// late-arriving final from the previous session would otherwise
|
|
1591
1686
|
// overwrite the new session's transcript with stale text.
|
|
1592
1687
|
const current =
|
|
1593
|
-
lingeringSession &&
|
|
1688
|
+
lingeringSession &&
|
|
1689
|
+
(lingeringSession.kind === "native" ||
|
|
1690
|
+
lingeringSession.kind === "whisper")
|
|
1594
1691
|
? lingeringSession
|
|
1595
1692
|
: null;
|
|
1596
1693
|
if (!current) return;
|
|
@@ -1607,7 +1704,8 @@ export function installDesktopVoiceDictation(
|
|
|
1607
1704
|
onSpeechError(({ error }) => {
|
|
1608
1705
|
const current = session;
|
|
1609
1706
|
console.error("[voice-dictation] native speech error:", error);
|
|
1610
|
-
if (!current || current.kind !== "native")
|
|
1707
|
+
if (!current || (current.kind !== "native" && current.kind !== "whisper"))
|
|
1708
|
+
return;
|
|
1611
1709
|
setFlowState("error");
|
|
1612
1710
|
window.setTimeout(() => {
|
|
1613
1711
|
if (!disposed && session === current) cleanup(current);
|
|
@@ -1314,6 +1314,17 @@ body[data-clips-route="recording-pill"] #root {
|
|
|
1314
1314
|
gap: 6px;
|
|
1315
1315
|
}
|
|
1316
1316
|
|
|
1317
|
+
.setup-section-heading {
|
|
1318
|
+
font-size: 10px;
|
|
1319
|
+
font-weight: 700;
|
|
1320
|
+
letter-spacing: 0.07em;
|
|
1321
|
+
text-transform: uppercase;
|
|
1322
|
+
color: var(--fg-muted);
|
|
1323
|
+
padding-top: 6px;
|
|
1324
|
+
border-top: 1px solid var(--border);
|
|
1325
|
+
margin-top: 4px;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1317
1328
|
.setup-advanced {
|
|
1318
1329
|
border: 1px solid var(--border);
|
|
1319
1330
|
border-radius: var(--radius-sm);
|
|
@@ -1638,6 +1649,11 @@ body[data-clips-route="recording-pill"] #root {
|
|
|
1638
1649
|
font-size: 12px;
|
|
1639
1650
|
}
|
|
1640
1651
|
|
|
1652
|
+
.setup-account--no-border {
|
|
1653
|
+
border-top: none;
|
|
1654
|
+
padding-top: 0;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1641
1657
|
.setup-account-email {
|
|
1642
1658
|
color: var(--fg-muted);
|
|
1643
1659
|
overflow: hidden;
|
|
@@ -4200,9 +4216,9 @@ body[data-clips-route="recording-pill"] #root {
|
|
|
4200
4216
|
|
|
4201
4217
|
.whisper-status {
|
|
4202
4218
|
display: flex;
|
|
4203
|
-
align-items:
|
|
4219
|
+
align-items: center;
|
|
4204
4220
|
flex-wrap: wrap;
|
|
4205
|
-
gap: 6px;
|
|
4221
|
+
gap: 4px 6px;
|
|
4206
4222
|
margin-top: 6px;
|
|
4207
4223
|
font-size: 11px;
|
|
4208
4224
|
line-height: 1.4;
|
|
@@ -4237,8 +4253,7 @@ body[data-clips-route="recording-pill"] #root {
|
|
|
4237
4253
|
}
|
|
4238
4254
|
|
|
4239
4255
|
.whisper-status-path {
|
|
4240
|
-
|
|
4241
|
-
margin-top: 2px;
|
|
4256
|
+
flex-basis: 100%;
|
|
4242
4257
|
font-size: 10px;
|
|
4243
4258
|
color: var(--fg-subtle);
|
|
4244
4259
|
word-break: break-all;
|
|
@@ -69,7 +69,7 @@ libc = "0.2"
|
|
|
69
69
|
[target."cfg(target_os = \"macos\")".dependencies]
|
|
70
70
|
core-foundation = "0.10"
|
|
71
71
|
core-graphics = "0.25"
|
|
72
|
-
objc2 = "0.6"
|
|
72
|
+
objc2 = { version = "0.6", features = ["exception"] }
|
|
73
73
|
# Native on-device dictation via SFSpeechRecognizer + AVAudioEngine.
|
|
74
74
|
# Powers the "browser" voice-dictation provider (free, instant, no API
|
|
75
75
|
# key, no server round-trip — same engine WisprFlow uses for offline).
|