@agent-native/core 0.157.0 → 0.157.1
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/templates/analytics/server/jobs/analytics-bigquery-backfill.ts +19 -4
- package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +7 -2
- package/corpus/templates/assets/app/global.css +4 -0
- package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +1 -2
- package/corpus/templates/brain/app/routes/sources.tsx +1 -2
- package/corpus/templates/calendar/app/hooks/use-bookings.ts +12 -2
- package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +64 -2
- package/corpus/templates/calendar/server/handlers/bookings.ts +219 -93
- package/corpus/templates/calendar/server/plugins/auth.ts +0 -2
- package/corpus/templates/clips/actions/create-folder.ts +7 -1
- package/corpus/templates/clips/actions/create-meeting.ts +6 -2
- package/corpus/templates/clips/actions/create-recording.ts +5 -3
- package/corpus/templates/clips/actions/finalize-recording.ts +97 -65
- package/corpus/templates/clips/actions/import-loom-recording.ts +5 -3
- package/corpus/templates/clips/actions/request-recording-access.ts +272 -0
- package/corpus/templates/clips/actions/stitch-recordings.ts +5 -3
- package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +38 -63
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +75 -75
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +3 -21
- package/corpus/templates/clips/app/components/sharing/share-ui.tsx +13 -30
- package/corpus/templates/clips/app/i18n/en-US.ts +10 -0
- package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +5 -5
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +9 -16
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +135 -18
- package/corpus/templates/clips/changelog/2026-08-14-long-resumable-uploads-now-abort-cleanly-and-show-errors.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-14-standardized-share-popovers.md +6 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +11 -3
- package/corpus/templates/clips/chrome-extension/src/github-preview-content.ts +37 -8
- package/corpus/templates/clips/chrome-extension/src/github-preview.ts +3 -3
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +8 -25
- package/corpus/templates/clips/desktop/src/app.tsx +24 -0
- package/corpus/templates/clips/desktop/src/lib/media-capture-constraints.ts +42 -13
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +11 -18
- package/corpus/templates/clips/desktop/src/overlays/onboarding.tsx +1 -0
- package/corpus/templates/clips/desktop/src/shared/config.ts +1 -0
- package/corpus/templates/clips/desktop/src/styles.css +24 -9
- package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +3 -0
- package/corpus/templates/clips/desktop/src-tauri/src/config.rs +8 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +154 -176
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +115 -31
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +7 -0
- package/corpus/templates/clips/server/db/index.ts +1 -0
- package/corpus/templates/clips/server/db/schema.ts +1 -0
- package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +6 -1
- package/corpus/templates/clips/server/lib/emails.ts +23 -0
- package/corpus/templates/clips/server/lib/public-agent-context.ts +55 -0
- package/corpus/templates/clips/server/lib/recordings.ts +2 -1
- package/corpus/templates/clips/server/lib/resumable-upload-cleanup.ts +49 -0
- package/corpus/templates/clips/server/lib/s3-upload-provider.ts +34 -7
- package/corpus/templates/clips/server/lib/upload-interruption.ts +15 -1
- package/corpus/templates/clips/server/lib/upload-lease.ts +94 -9
- package/corpus/templates/clips/server/routes/api/agent-context.json.get.ts +82 -7
- package/corpus/templates/clips/server/routes/api/public-recording.get.ts +1 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +71 -12
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/interrupt.post.ts +4 -3
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +19 -0
- package/corpus/templates/clips/server/routes/api/uploads/reap.post.ts +8 -1
- package/corpus/templates/clips/shared/recording-link.ts +4 -0
- package/corpus/templates/clips/shared/share-loader-response.ts +2 -1
- package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +7 -16
- package/corpus/templates/factory/AGENTS.md +5 -1
- package/corpus/templates/factory/README.md +6 -2
- package/corpus/templates/factory/actions/view-screen.ts +15 -0
- package/corpus/templates/factory/app/components/factory/FactoryAgentsView.tsx +58 -189
- package/corpus/templates/factory/app/components/layout/Header.tsx +1 -0
- package/corpus/templates/factory/app/components/layout/Layout.tsx +1 -1
- package/corpus/templates/factory/app/components/layout/Sidebar.tsx +7 -0
- package/corpus/templates/factory/app/hooks/use-navigation-state.ts +3 -0
- package/corpus/templates/factory/app/i18n/en-US.ts +3 -1
- package/corpus/templates/factory/app/routes/agents.tsx +9 -0
- package/corpus/templates/mail/server/plugins/auth.ts +0 -2
- package/corpus/templates/plan/app/components/plan/LocalCodebasePicker.tsx +50 -14
- package/corpus/templates/plan/app/pages/PlansPage.tsx +9 -19
- package/corpus/templates/slides/app/components/editor/HistoryPanel.tsx +7 -1
- package/corpus/templates/slides/app/context/DeckContext.tsx +71 -22
- package/corpus/templates/slides/app/lib/editor-shortcuts.ts +46 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +13 -0
- package/dist/client/AgentPanel.d.ts +13 -1
- package/dist/client/AgentPanel.js +12 -10
- package/dist/client/AssistantChat.js +25 -5
- package/dist/client/HighlightedCodeBlock.js +12 -2
- package/dist/client/agent-chat/index.d.ts +1 -0
- package/dist/client/agent-chat/index.js +1 -0
- package/dist/client/chat/markdown-renderer.js +16 -3
- package/dist/client/chat-first/apps-rail.js +1 -1
- package/dist/client/components/ui/message-scroller.js +1 -1
- package/dist/client/settings/AccountSettingsCard.js +48 -19
- package/dist/client/sharing/ShareButton.d.ts +1 -2
- package/dist/client/sharing/ShareButton.js +5 -41
- package/dist/client/sharing/ShareDialog.js +5 -4
- package/dist/client/use-chat-threads.js +10 -14
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/localization/default-messages.d.ts +1 -0
- package/dist/localization/default-messages.js +1 -0
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
- package/dist/provider-api/actions/provider-api.d.ts +15 -15
- package/dist/server/auth-marketing.d.ts +1 -0
- package/dist/server/auth-marketing.js +0 -2
- package/dist/server/auth.d.ts +1 -0
- package/dist/server/auth.js +18 -0
- package/dist/server/onboarding-html.d.ts +1 -0
- package/dist/server/onboarding-html.js +2 -88
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/shared/markdown-block-split.d.ts +16 -0
- package/dist/shared/markdown-block-split.js +81 -5
- package/dist/styles/agent-native.css +13 -0
- package/dist/voice/voice-cleanup-prompt.js +1 -0
- package/package.json +2 -2
- package/corpus/templates/clips/shared/mic-audio-cleanup.ts +0 -350
|
@@ -22,7 +22,6 @@ import {
|
|
|
22
22
|
isLikelyPhoneMicLabel,
|
|
23
23
|
type AudioInputFallback,
|
|
24
24
|
} from "@shared/media-device-selection";
|
|
25
|
-
import { createMicAudioCleanup } from "@shared/mic-audio-cleanup";
|
|
26
25
|
import { scheduleReadyChime } from "@shared/recording-audio";
|
|
27
26
|
import {
|
|
28
27
|
SCREEN_CAPTURE_FRAME_RATE,
|
|
@@ -435,14 +434,9 @@ async function getCameraStream(deviceId: string): Promise<MediaStream> {
|
|
|
435
434
|
}
|
|
436
435
|
}
|
|
437
436
|
|
|
438
|
-
//
|
|
439
|
-
//
|
|
440
|
-
//
|
|
441
|
-
// the signal and audibly degrades it. We can't detect "is this device doing
|
|
442
|
-
// its own processing" directly, so we use the device label as a heuristic and
|
|
443
|
-
// ask the browser to SKIP its own noise suppression / AGC for those devices
|
|
444
|
-
// (`{ ideal: false }` — a best-effort request, not a hard requirement). Echo
|
|
445
|
-
// cancellation stays on for everything.
|
|
437
|
+
// Let the browser apply its built-in voice processing. Device labels are not a
|
|
438
|
+
// reliable signal for whether a hardware path already processed its audio, so
|
|
439
|
+
// do not branch capture quality on a label heuristic.
|
|
446
440
|
async function chooseFallbackMicDevice(
|
|
447
441
|
requestedLabel: string,
|
|
448
442
|
avoidDeviceIds: string[] = [],
|
|
@@ -459,13 +453,12 @@ async function chooseFallbackMicDevice(
|
|
|
459
453
|
|
|
460
454
|
function voiceFocusedAudioConstraints(
|
|
461
455
|
deviceId: string,
|
|
462
|
-
|
|
456
|
+
_deviceLabel = "",
|
|
463
457
|
): MediaTrackConstraints {
|
|
464
|
-
const skipRedundantProcessing = isLikelyPhoneMicLabel(deviceLabel);
|
|
465
458
|
const audio: MediaTrackConstraints = {
|
|
466
459
|
echoCancellation: true,
|
|
467
|
-
noiseSuppression:
|
|
468
|
-
autoGainControl:
|
|
460
|
+
noiseSuppression: true,
|
|
461
|
+
autoGainControl: true,
|
|
469
462
|
channelCount: 1,
|
|
470
463
|
};
|
|
471
464
|
if (deviceId) audio.deviceId = { exact: deviceId };
|
|
@@ -811,20 +804,11 @@ async function createMixedAudio(
|
|
|
811
804
|
const audioContext = new AudioContext();
|
|
812
805
|
await audioContext.resume().catch(() => undefined);
|
|
813
806
|
const destination = audioContext.createMediaStreamDestination();
|
|
814
|
-
const micCleanup: Array<{ stop: () => void }> = [];
|
|
815
807
|
for (const input of audioInputs) {
|
|
816
|
-
let track = input.track;
|
|
817
|
-
if (input.isMicrophone) {
|
|
818
|
-
const cleanup = createMicAudioCleanup(new MediaStream([track]), {
|
|
819
|
-
audioContext,
|
|
820
|
-
});
|
|
821
|
-
micCleanup.push(cleanup);
|
|
822
|
-
track = cleanup.stream.getAudioTracks()[0] ?? track;
|
|
823
|
-
}
|
|
824
808
|
// One source per track (not per stream) so each input is isolated in the
|
|
825
809
|
// mix graph and can be detached independently.
|
|
826
810
|
const source = audioContext.createMediaStreamSource(
|
|
827
|
-
new MediaStream([track]),
|
|
811
|
+
new MediaStream([input.track]),
|
|
828
812
|
);
|
|
829
813
|
source.connect(destination);
|
|
830
814
|
// A mixed input track can end mid-recording — most commonly the shared
|
|
@@ -832,7 +816,7 @@ async function createMixedAudio(
|
|
|
832
816
|
// just that dead source so the mixed output keeps carrying the surviving
|
|
833
817
|
// inputs (the microphone). Without this the whole mixed destination can go
|
|
834
818
|
// silent, dropping both the tab audio and the mic for the rest of the clip.
|
|
835
|
-
track.addEventListener("ended", () => {
|
|
819
|
+
input.track.addEventListener("ended", () => {
|
|
836
820
|
try {
|
|
837
821
|
source.disconnect();
|
|
838
822
|
} catch {
|
|
@@ -847,7 +831,6 @@ async function createMixedAudio(
|
|
|
847
831
|
cleanup() {
|
|
848
832
|
if (cleanedUp) return;
|
|
849
833
|
cleanedUp = true;
|
|
850
|
-
for (const cleanup of micCleanup) cleanup.stop();
|
|
851
834
|
},
|
|
852
835
|
};
|
|
853
836
|
}
|
|
@@ -977,6 +977,7 @@ export function App() {
|
|
|
977
977
|
);
|
|
978
978
|
const localRecordingMode: LocalRecordingMode =
|
|
979
979
|
featureConfig?.localRecordingMode ?? "off";
|
|
980
|
+
const voiceCleanupEnabled = featureConfig?.voiceCleanupEnabled !== false;
|
|
980
981
|
|
|
981
982
|
const [pendingUploads, setPendingUploads] = useState<PendingDesktopUpload[]>(
|
|
982
983
|
[],
|
|
@@ -3160,6 +3161,7 @@ export function App() {
|
|
|
3160
3161
|
cameraOn,
|
|
3161
3162
|
micOn,
|
|
3162
3163
|
systemAudioOn,
|
|
3164
|
+
voiceCleanupEnabled,
|
|
3163
3165
|
localRecordingMode,
|
|
3164
3166
|
preAcquiredCameraStream,
|
|
3165
3167
|
preAcquiredDisplayStream: options?.resumeCapture?.displayStream ?? null,
|
|
@@ -5391,6 +5393,7 @@ function Setup({
|
|
|
5391
5393
|
const featureConfig = useFeatureConfig();
|
|
5392
5394
|
const updateStatus = useUpdateStatus();
|
|
5393
5395
|
const voiceEnabled = featureConfig?.voiceEnabled !== false;
|
|
5396
|
+
const voiceCleanupEnabled = featureConfig?.voiceCleanupEnabled !== false;
|
|
5394
5397
|
const meetingsEnabled = featureConfig?.meetingsEnabled !== false;
|
|
5395
5398
|
const launchAtLoginEnabled = featureConfig?.launchAtLoginEnabled !== false;
|
|
5396
5399
|
const autoHidePopoverEnabled = featureConfig?.autoHidePopoverEnabled === true;
|
|
@@ -5540,6 +5543,15 @@ function Setup({
|
|
|
5540
5543
|
);
|
|
5541
5544
|
}
|
|
5542
5545
|
|
|
5546
|
+
function setVoiceCleanupEnabled(enabled: boolean) {
|
|
5547
|
+
if (!featureConfig) return;
|
|
5548
|
+
invoke("set_feature_config", {
|
|
5549
|
+
config: { ...featureConfig, voiceCleanupEnabled: enabled },
|
|
5550
|
+
}).catch((err) =>
|
|
5551
|
+
console.error("[settings] set_feature_config failed", err),
|
|
5552
|
+
);
|
|
5553
|
+
}
|
|
5554
|
+
|
|
5543
5555
|
function setMeetingsEnabled(enabled: boolean) {
|
|
5544
5556
|
if (!featureConfig) return;
|
|
5545
5557
|
invoke("set_feature_config", {
|
|
@@ -7258,6 +7270,18 @@ function Setup({
|
|
|
7258
7270
|
title="Capture"
|
|
7259
7271
|
description="Choose how recordings are saved and how the tray opens them."
|
|
7260
7272
|
>
|
|
7273
|
+
<DesktopSettingsRow
|
|
7274
|
+
label="Voice cleanup"
|
|
7275
|
+
description="Reduce steady background noise in microphone recordings."
|
|
7276
|
+
control={
|
|
7277
|
+
<Switch
|
|
7278
|
+
on={voiceCleanupEnabled}
|
|
7279
|
+
onChange={setVoiceCleanupEnabled}
|
|
7280
|
+
disabled={captureControlsLocked}
|
|
7281
|
+
label="Use voice cleanup"
|
|
7282
|
+
/>
|
|
7283
|
+
}
|
|
7284
|
+
/>
|
|
7261
7285
|
<DesktopSettingsRow
|
|
7262
7286
|
label="Rewind"
|
|
7263
7287
|
description={rewindStatusPresentation.title}
|
|
@@ -13,12 +13,22 @@ export const VOICE_FOCUSED_AUDIO_CONSTRAINTS: MediaTrackConstraints = {
|
|
|
13
13
|
channelCount: { ideal: 1 },
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
const RAW_AUDIO_CONSTRAINTS: MediaTrackConstraints = {
|
|
17
|
+
echoCancellation: false,
|
|
18
|
+
noiseSuppression: false,
|
|
19
|
+
autoGainControl: false,
|
|
20
|
+
channelCount: { ideal: 1 },
|
|
21
|
+
};
|
|
22
|
+
|
|
16
23
|
export function voiceFocusedAudioConstraints(
|
|
17
24
|
deviceId?: string | null,
|
|
25
|
+
voiceCleanupEnabled = true,
|
|
18
26
|
): MediaTrackConstraints {
|
|
19
27
|
const id = deviceId?.trim();
|
|
20
28
|
return {
|
|
21
|
-
...
|
|
29
|
+
...(voiceCleanupEnabled
|
|
30
|
+
? VOICE_FOCUSED_AUDIO_CONSTRAINTS
|
|
31
|
+
: RAW_AUDIO_CONSTRAINTS),
|
|
22
32
|
...(id ? { deviceId: { exact: id } } : {}),
|
|
23
33
|
};
|
|
24
34
|
}
|
|
@@ -73,9 +83,10 @@ function stopStream(stream: MediaStream): void {
|
|
|
73
83
|
|
|
74
84
|
async function getVoiceFocusedAudioStream(
|
|
75
85
|
deviceId?: string | null,
|
|
86
|
+
voiceCleanupEnabled = true,
|
|
76
87
|
): Promise<MediaStream> {
|
|
77
88
|
return navigator.mediaDevices.getUserMedia({
|
|
78
|
-
audio: voiceFocusedAudioConstraints(deviceId),
|
|
89
|
+
audio: voiceFocusedAudioConstraints(deviceId, voiceCleanupEnabled),
|
|
79
90
|
video: false,
|
|
80
91
|
});
|
|
81
92
|
}
|
|
@@ -101,6 +112,7 @@ async function fallbackAudioInput(
|
|
|
101
112
|
async function tryFallbackAudioInput(
|
|
102
113
|
savedLabel: string | null | undefined,
|
|
103
114
|
avoidDeviceIds: Array<string | null | undefined>,
|
|
115
|
+
voiceCleanupEnabled = true,
|
|
104
116
|
): Promise<MediaStream | null> {
|
|
105
117
|
const fallback = await fallbackAudioInput(savedLabel, avoidDeviceIds);
|
|
106
118
|
if (!fallback) return null;
|
|
@@ -109,7 +121,10 @@ async function tryFallbackAudioInput(
|
|
|
109
121
|
`[clips-recorder] selected mic unavailable; retrying ${fallback.reason} microphone`,
|
|
110
122
|
{ deviceId: fallback.deviceId, label: fallback.label },
|
|
111
123
|
);
|
|
112
|
-
return await getVoiceFocusedAudioStream(
|
|
124
|
+
return await getVoiceFocusedAudioStream(
|
|
125
|
+
fallback.deviceId,
|
|
126
|
+
voiceCleanupEnabled,
|
|
127
|
+
);
|
|
113
128
|
} catch (fallbackErr) {
|
|
114
129
|
if (!isMediaConstraintFailure(fallbackErr)) throw fallbackErr;
|
|
115
130
|
console.warn(
|
|
@@ -122,9 +137,10 @@ async function tryFallbackAudioInput(
|
|
|
122
137
|
|
|
123
138
|
async function getDefaultAudioStreamWithBasicFallback(
|
|
124
139
|
reason: unknown,
|
|
140
|
+
voiceCleanupEnabled = true,
|
|
125
141
|
): Promise<MediaStream> {
|
|
126
142
|
try {
|
|
127
|
-
return await getVoiceFocusedAudioStream();
|
|
143
|
+
return await getVoiceFocusedAudioStream(undefined, voiceCleanupEnabled);
|
|
128
144
|
} catch (fallbackErr) {
|
|
129
145
|
if (!isMediaConstraintFailure(fallbackErr)) throw fallbackErr;
|
|
130
146
|
console.warn(
|
|
@@ -132,7 +148,7 @@ async function getDefaultAudioStreamWithBasicFallback(
|
|
|
132
148
|
fallbackErr,
|
|
133
149
|
);
|
|
134
150
|
return navigator.mediaDevices.getUserMedia({
|
|
135
|
-
audio: true,
|
|
151
|
+
audio: voiceCleanupEnabled ? true : RAW_AUDIO_CONSTRAINTS,
|
|
136
152
|
video: false,
|
|
137
153
|
});
|
|
138
154
|
} finally {
|
|
@@ -147,13 +163,16 @@ async function getDefaultAudioStreamWithBasicFallback(
|
|
|
147
163
|
|
|
148
164
|
async function replacePhoneDefaultMicIfPossible(
|
|
149
165
|
stream: MediaStream,
|
|
166
|
+
voiceCleanupEnabled = true,
|
|
150
167
|
): Promise<MediaStream> {
|
|
151
168
|
const track = stream.getAudioTracks()[0];
|
|
152
169
|
if (!track || !isLikelyPhoneMicLabel(track.label)) return stream;
|
|
153
170
|
const settings = track.getSettings?.();
|
|
154
|
-
const replacement = await tryFallbackAudioInput(
|
|
155
|
-
|
|
156
|
-
|
|
171
|
+
const replacement = await tryFallbackAudioInput(
|
|
172
|
+
null,
|
|
173
|
+
[settings?.deviceId ?? null],
|
|
174
|
+
voiceCleanupEnabled,
|
|
175
|
+
);
|
|
157
176
|
if (!replacement) return stream;
|
|
158
177
|
console.warn(
|
|
159
178
|
"[clips-recorder] default mic resolved to a phone-like input; using explicit fallback mic",
|
|
@@ -198,12 +217,15 @@ export async function getCameraStreamWithFallback(
|
|
|
198
217
|
export async function getAudioStreamWithFallback(
|
|
199
218
|
deviceId?: string | null,
|
|
200
219
|
savedLabel?: string | null,
|
|
220
|
+
voiceCleanupEnabled = true,
|
|
201
221
|
): Promise<MediaStream> {
|
|
202
222
|
const id = deviceId?.trim();
|
|
203
223
|
|
|
204
224
|
try {
|
|
205
|
-
const stream = await getVoiceFocusedAudioStream(id);
|
|
206
|
-
return id
|
|
225
|
+
const stream = await getVoiceFocusedAudioStream(id, voiceCleanupEnabled);
|
|
226
|
+
return id
|
|
227
|
+
? stream
|
|
228
|
+
: replacePhoneDefaultMicIfPossible(stream, voiceCleanupEnabled);
|
|
207
229
|
} catch (err) {
|
|
208
230
|
let deviceGone = false;
|
|
209
231
|
if (id && err instanceof DOMException && err.name === "NotAllowedError") {
|
|
@@ -227,11 +249,18 @@ export async function getAudioStreamWithFallback(
|
|
|
227
249
|
}
|
|
228
250
|
if (!isMediaConstraintFailure(err) && !deviceGone) throw err;
|
|
229
251
|
if (id) {
|
|
230
|
-
const fallback = await tryFallbackAudioInput(
|
|
252
|
+
const fallback = await tryFallbackAudioInput(
|
|
253
|
+
savedLabel,
|
|
254
|
+
[id],
|
|
255
|
+
voiceCleanupEnabled,
|
|
256
|
+
);
|
|
231
257
|
if (fallback) return fallback;
|
|
232
258
|
}
|
|
233
259
|
|
|
234
|
-
const defaultStream = await getDefaultAudioStreamWithBasicFallback(
|
|
235
|
-
|
|
260
|
+
const defaultStream = await getDefaultAudioStreamWithBasicFallback(
|
|
261
|
+
err,
|
|
262
|
+
voiceCleanupEnabled,
|
|
263
|
+
);
|
|
264
|
+
return replacePhoneDefaultMicIfPossible(defaultStream, voiceCleanupEnabled);
|
|
236
265
|
}
|
|
237
266
|
}
|
|
@@ -55,10 +55,6 @@ import {
|
|
|
55
55
|
waitForAcceptedRecordingAfterFinalizeError,
|
|
56
56
|
waitForReadyRecordingAfterFinalizeError,
|
|
57
57
|
} from "../../../shared/finalize-recovery";
|
|
58
|
-
import {
|
|
59
|
-
createMicAudioCleanup,
|
|
60
|
-
type MicAudioCleanupHandle,
|
|
61
|
-
} from "../../../shared/mic-audio-cleanup";
|
|
62
58
|
import type { LocalRecordingMode } from "../shared/config";
|
|
63
59
|
import { createAudioCue, type AudioCue } from "./audio-cue";
|
|
64
60
|
import { createCameraCompositeStream } from "./camera-composite";
|
|
@@ -239,6 +235,8 @@ export interface StartParams {
|
|
|
239
235
|
cameraOn: boolean;
|
|
240
236
|
/** Record + transcribe system/desktop audio. Default true. */
|
|
241
237
|
systemAudioOn?: boolean;
|
|
238
|
+
/** Apply the browser/native voice cleanup path to microphone audio. */
|
|
239
|
+
voiceCleanupEnabled?: boolean;
|
|
242
240
|
/** Cancels or bounds the pre-record capture setup. */
|
|
243
241
|
signal?: AbortSignal;
|
|
244
242
|
localRecordingMode?: LocalRecordingMode;
|
|
@@ -548,9 +546,9 @@ interface RecordingAudio {
|
|
|
548
546
|
}
|
|
549
547
|
|
|
550
548
|
/**
|
|
551
|
-
* Build the audio track(s) for the recording.
|
|
552
|
-
*
|
|
553
|
-
*
|
|
549
|
+
* Build the audio track(s) for the recording. Browser capture requests its
|
|
550
|
+
* built-in voice processing; this graph only combines raw source tracks when
|
|
551
|
+
* MediaRecorder needs one mixed track.
|
|
554
552
|
*/
|
|
555
553
|
function buildRecordingAudio(
|
|
556
554
|
micTracks: MediaStreamTrack[],
|
|
@@ -571,22 +569,13 @@ function buildRecordingAudio(
|
|
|
571
569
|
}
|
|
572
570
|
const ctx: AudioContext = new AudioCtx();
|
|
573
571
|
const destination = ctx.createMediaStreamDestination();
|
|
574
|
-
const
|
|
575
|
-
const cleanedMicTracks = micTracks.map((track) => {
|
|
576
|
-
const cleanup = createMicAudioCleanup(new MediaStream([track]), {
|
|
577
|
-
audioContext: ctx,
|
|
578
|
-
});
|
|
579
|
-
micCleanup.push(cleanup);
|
|
580
|
-
return cleanup.stream.getAudioTracks()[0] ?? track;
|
|
581
|
-
});
|
|
582
|
-
for (const tracks of [cleanedMicTracks, systemTracks]) {
|
|
572
|
+
for (const tracks of [micTracks, systemTracks]) {
|
|
583
573
|
if (!tracks.length) continue;
|
|
584
574
|
ctx.createMediaStreamSource(new MediaStream(tracks)).connect(destination);
|
|
585
575
|
}
|
|
586
576
|
return {
|
|
587
577
|
tracks: destination.stream.getAudioTracks(),
|
|
588
578
|
cleanup() {
|
|
589
|
-
for (const cleanup of micCleanup) cleanup.stop();
|
|
590
579
|
ctx.close().catch(() => {});
|
|
591
580
|
},
|
|
592
581
|
};
|
|
@@ -4307,7 +4296,11 @@ async function startRecordingInner(
|
|
|
4307
4296
|
const audioStreamPromise: Promise<MediaStream> | null = resumedAudioStream
|
|
4308
4297
|
? Promise.resolve(resumedAudioStream)
|
|
4309
4298
|
: wantsAudio
|
|
4310
|
-
? getAudioStreamWithFallback(
|
|
4299
|
+
? getAudioStreamWithFallback(
|
|
4300
|
+
params.micId,
|
|
4301
|
+
params.micLabel,
|
|
4302
|
+
params.voiceCleanupEnabled,
|
|
4303
|
+
)
|
|
4311
4304
|
: null;
|
|
4312
4305
|
|
|
4313
4306
|
// getDisplayMedia can remain pending in a long-lived WebKit tray webview.
|
|
@@ -120,6 +120,7 @@ export interface FeatureConfig {
|
|
|
120
120
|
clipsEnabled: boolean;
|
|
121
121
|
meetingsEnabled: boolean;
|
|
122
122
|
voiceEnabled: boolean;
|
|
123
|
+
voiceCleanupEnabled: boolean;
|
|
123
124
|
launchAtLoginEnabled: boolean;
|
|
124
125
|
autoHidePopoverEnabled: boolean;
|
|
125
126
|
meetingTranscriptionMode: "manual" | "ask" | "auto";
|
|
@@ -6074,9 +6074,14 @@ body[data-clips-route="recording-pill"] #root {
|
|
|
6074
6074
|
/* No fill, and the stroke is painted OUTWARD with an outset box-shadow so it
|
|
6075
6075
|
lands just outside the captured rect — never inside the recorded video. */
|
|
6076
6076
|
box-shadow:
|
|
6077
|
-
|
|
6078
|
-
0 0 0
|
|
6079
|
-
|
|
6077
|
+
/* guard:allow-raw-color — recording frame uses fixed capture-safe contrast. */
|
|
6078
|
+
0 0 0 1px rgba(255, 255, 255, 0.96),
|
|
6079
|
+
/* guard:allow-raw-color — recording frame uses fixed capture-safe contrast. */
|
|
6080
|
+
0 0 0 3px rgba(220, 38, 38, 0.98),
|
|
6081
|
+
/* guard:allow-raw-color — recording frame uses fixed capture-safe contrast. */
|
|
6082
|
+
0 0 0 6px rgba(17, 24, 39, 0.78),
|
|
6083
|
+
/* guard:allow-raw-color — recording frame uses fixed capture-safe contrast. */
|
|
6084
|
+
0 0 30px 8px rgba(220, 38, 38, 0.28);
|
|
6080
6085
|
animation: region-record-border-pulse 1.8s ease-in-out infinite;
|
|
6081
6086
|
}
|
|
6082
6087
|
|
|
@@ -6086,15 +6091,25 @@ body[data-clips-route="recording-pill"] #root {
|
|
|
6086
6091
|
0%,
|
|
6087
6092
|
100% {
|
|
6088
6093
|
box-shadow:
|
|
6089
|
-
|
|
6090
|
-
0 0 0
|
|
6091
|
-
|
|
6094
|
+
/* guard:allow-raw-color — recording frame uses fixed capture-safe contrast. */
|
|
6095
|
+
0 0 0 1px rgba(255, 255, 255, 0.98),
|
|
6096
|
+
/* guard:allow-raw-color — recording frame uses fixed capture-safe contrast. */
|
|
6097
|
+
0 0 0 3px rgba(220, 38, 38, 1),
|
|
6098
|
+
/* guard:allow-raw-color — recording frame uses fixed capture-safe contrast. */
|
|
6099
|
+
0 0 0 6px rgba(17, 24, 39, 0.76),
|
|
6100
|
+
/* guard:allow-raw-color — recording frame uses fixed capture-safe contrast. */
|
|
6101
|
+
0 0 24px 7px rgba(220, 38, 38, 0.22);
|
|
6092
6102
|
}
|
|
6093
6103
|
50% {
|
|
6094
6104
|
box-shadow:
|
|
6095
|
-
|
|
6096
|
-
0 0 0
|
|
6097
|
-
|
|
6105
|
+
/* guard:allow-raw-color — recording frame uses fixed capture-safe contrast. */
|
|
6106
|
+
0 0 0 1px rgba(255, 255, 255, 1),
|
|
6107
|
+
/* guard:allow-raw-color — recording frame uses fixed capture-safe contrast. */
|
|
6108
|
+
0 0 0 3px rgba(220, 38, 38, 1),
|
|
6109
|
+
/* guard:allow-raw-color — recording frame uses fixed capture-safe contrast. */
|
|
6110
|
+
0 0 0 6px rgba(17, 24, 39, 0.88),
|
|
6111
|
+
/* guard:allow-raw-color — recording frame uses fixed capture-safe contrast. */
|
|
6112
|
+
0 0 34px 10px rgba(220, 38, 38, 0.36);
|
|
6098
6113
|
}
|
|
6099
6114
|
}
|
|
6100
6115
|
|
|
@@ -143,6 +143,9 @@ screencapturekit = { version = "2.0.0", features = ["macos_15_0"] }
|
|
|
143
143
|
# AudioBufferList type so we can hand SCK's CMSampleBuffer audio bytes to
|
|
144
144
|
# AVAudioPCMBuffer for the speech recognizer.
|
|
145
145
|
objc2-core-audio-types = "0.3"
|
|
146
|
+
# Pure-Rust RNNoise-derived microphone denoising for the realtime
|
|
147
|
+
# ScreenCaptureKit mixer. Keep the CLI/dasp features out of the desktop bundle.
|
|
148
|
+
nnnoiseless = { version = "0.5.2", default-features = false }
|
|
146
149
|
# `metal` builds whisper.cpp's ggml-metal backend and links it in (the sys
|
|
147
150
|
# crate's build script embeds the compiled metallib, so no app-bundle/runtime
|
|
148
151
|
# asset changes are needed). Without it, whisper.cpp decode runs CPU-only.
|
|
@@ -111,6 +111,8 @@ pub struct FeatureConfig {
|
|
|
111
111
|
pub clips_enabled: bool,
|
|
112
112
|
pub meetings_enabled: bool,
|
|
113
113
|
pub voice_enabled: bool,
|
|
114
|
+
#[serde(default = "default_voice_cleanup_enabled")]
|
|
115
|
+
pub voice_cleanup_enabled: bool,
|
|
114
116
|
#[serde(default = "default_launch_at_login_enabled")]
|
|
115
117
|
pub launch_at_login_enabled: bool,
|
|
116
118
|
#[serde(default)]
|
|
@@ -160,6 +162,10 @@ fn default_launch_at_login_enabled() -> bool {
|
|
|
160
162
|
true
|
|
161
163
|
}
|
|
162
164
|
|
|
165
|
+
fn default_voice_cleanup_enabled() -> bool {
|
|
166
|
+
true
|
|
167
|
+
}
|
|
168
|
+
|
|
163
169
|
fn default_meeting_transcription_mode() -> MeetingTranscriptionMode {
|
|
164
170
|
MeetingTranscriptionMode::Ask
|
|
165
171
|
}
|
|
@@ -223,6 +229,7 @@ impl Default for FeatureConfig {
|
|
|
223
229
|
clips_enabled: true,
|
|
224
230
|
meetings_enabled: true,
|
|
225
231
|
voice_enabled: true,
|
|
232
|
+
voice_cleanup_enabled: default_voice_cleanup_enabled(),
|
|
226
233
|
launch_at_login_enabled: true,
|
|
227
234
|
auto_hide_popover_enabled: false,
|
|
228
235
|
meeting_transcription_mode: default_meeting_transcription_mode(),
|
|
@@ -436,6 +443,7 @@ mod tests {
|
|
|
436
443
|
.unwrap();
|
|
437
444
|
|
|
438
445
|
assert_eq!(config.whisper_model_id, "base");
|
|
446
|
+
assert!(config.voice_cleanup_enabled);
|
|
439
447
|
}
|
|
440
448
|
|
|
441
449
|
#[test]
|