@agent-native/core 0.114.12 → 0.114.15
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 +2 -2
- package/corpus/core/CHANGELOG.md +21 -0
- package/corpus/core/docs/content/deployment.mdx +1 -1
- package/corpus/core/docs/content/frames.mdx +1 -1
- package/corpus/core/docs/content/integrations.mdx +27 -27
- package/corpus/core/docs/content/pr-visual-recap.mdx +17 -13
- package/corpus/core/docs/content/workspace-management.mdx +1 -1
- package/corpus/core/package.json +2 -1
- package/corpus/core/src/agent/engine/builder-engine.ts +4 -0
- package/corpus/core/src/application-state/index.ts +3 -0
- package/corpus/core/src/application-state/script-helpers.ts +12 -1
- package/corpus/core/src/application-state/store.ts +216 -15
- package/corpus/core/src/client/AgentPanel.tsx +10 -4
- package/corpus/core/src/client/ConnectBuilderCard.tsx +5 -1
- package/corpus/core/src/client/chat/markdown-renderer.tsx +3 -5
- package/corpus/core/src/client/error-format.ts +2 -1
- package/corpus/core/src/client/index.ts +1 -0
- package/corpus/core/src/client/integrations/IntegrationsPanel.tsx +2 -18
- package/corpus/core/src/client/resources/McpIntegrationDialog.tsx +43 -32
- package/corpus/core/src/client/resources/mcp-integration-catalog.ts +4 -2
- package/corpus/core/src/client/settings/BackgroundAgentSection.tsx +5 -1
- package/corpus/core/src/client/settings/SettingsPanel.tsx +4 -4
- package/corpus/core/src/client/settings/useBuilderStatus.ts +3 -0
- package/corpus/core/src/db/client.ts +44 -1
- package/corpus/core/src/localization/default-messages.ts +1 -7
- package/corpus/core/src/server/builder-browser.ts +4 -0
- package/corpus/core/src/server/builder-design-systems.ts +6 -1
- package/corpus/core/src/server/fusion-app.ts +5 -1
- package/corpus/core/src/shared/builder-link-tracking.ts +41 -0
- package/corpus/core/src/shared/index.ts +1 -0
- package/corpus/templates/analytics/AGENTS.md +10 -1
- package/corpus/templates/analytics/actions/save-dashboard-report-subscription.ts +10 -2
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +5 -4
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +20 -32
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/report-panel-window.ts +62 -0
- package/corpus/templates/analytics/changelog/2026-07-21-chart-series-controls-stay-open-while-moving-from-the-legend.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-21-dashboard-email-captures-complete-large-dashboards-in-chunks.md +6 -0
- package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +4 -4
- package/corpus/templates/analytics/server/jobs/dashboard-report.ts +1 -8
- package/corpus/templates/analytics/server/lib/dashboard-report-subscriptions.ts +39 -7
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +341 -251
- package/corpus/templates/analytics/server/lib/first-party-dashboard-repair.ts +114 -0
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +84 -6
- package/corpus/templates/analytics/server/plugins/db.ts +13 -0
- package/corpus/templates/clips/app/components/player/video-player.tsx +10 -4
- package/corpus/templates/clips/changelog/2026-07-21-choose-which-whisper-model-to-use-in-settings.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-21-fixed-organization-recording-visibility-default.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-21-live-dictation-text-now-appears-above-the-desktop-recording-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-21-meeting-notes-stop-when-calls-end.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +66 -75
- package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +2 -0
- package/corpus/templates/clips/desktop/src/hooks/useWhisperSettings.ts +172 -0
- package/corpus/templates/clips/desktop/src/lib/meeting-call-app.ts +41 -0
- package/corpus/templates/clips/desktop/src/lib/silence-events.ts +1 -0
- package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +14 -3
- package/corpus/templates/clips/desktop/src/overlays/flow-bar.tsx +25 -0
- package/corpus/templates/clips/desktop/src/shared/config.ts +1 -0
- package/corpus/templates/clips/desktop/src/styles.css +100 -7
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +3 -4
- package/corpus/templates/clips/desktop/src-tauri/src/config.rs +32 -0
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +3 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +11 -4
- package/corpus/templates/clips/desktop/src-tauri/src/silence_detector.rs +268 -20
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_model.rs +285 -164
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +18 -7
- package/corpus/templates/clips/learnings.defaults.md +1 -1
- package/corpus/templates/clips/server/plugins/db.ts +11 -0
- package/corpus/templates/clips/shared/builder-credits.ts +1 -1
- package/corpus/templates/design/AGENTS.md +7 -3
- package/corpus/templates/design/actions/begin-node-rewrite-request.ts +78 -0
- package/corpus/templates/design/actions/cancel-node-rewrite-request.ts +17 -7
- package/corpus/templates/design/actions/propose-node-rewrite.ts +26 -18
- package/corpus/templates/design/actions/resolve-node-rewrite.ts +67 -19
- package/corpus/templates/design/app/components/design/FusionAppBanner.tsx +9 -1
- package/corpus/templates/design/app/components/design/edit-panel/component-section.tsx +5 -1
- package/corpus/templates/design/app/components/visual-editor/NodeRewriteProposal.tsx +1 -4
- package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +2 -6
- package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +9 -1
- package/corpus/templates/design/shared/node-rewrite.ts +32 -0
- package/corpus/templates/forms/app/components/layout/Sidebar.tsx +6 -3
- package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +11 -18
- package/corpus/templates/forms/app/pages/FormsListPage.tsx +9 -5
- package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +9 -1
- package/corpus/templates/slides/app/context/DeckContext.tsx +18 -0
- package/corpus/templates/slides/changelog/2026-07-21-decks-list-now-refreshes-when-you-switch-organizations.md +6 -0
- package/corpus/templates/tasks/app/components/custom-fields/FieldCreateBar.tsx +13 -19
- package/corpus/templates/tasks/app/components/custom-fields/FieldListRow.tsx +27 -22
- package/corpus/templates/tasks/app/components/custom-fields/FieldsList.tsx +26 -22
- package/corpus/templates/tasks/app/components/custom-fields/FieldsListSkeleton.tsx +4 -1
- package/corpus/templates/tasks/app/components/custom-fields/FieldsPage.tsx +4 -2
- package/corpus/templates/tasks/app/components/custom-fields/editor/FieldEditorSidebar.tsx +12 -3
- package/corpus/templates/tasks/app/components/custom-fields/editor/FieldTitleSection.tsx +6 -3
- package/corpus/templates/tasks/app/components/custom-fields/editor/config/CurrencyConfigControl.tsx +5 -2
- package/corpus/templates/tasks/app/components/custom-fields/editor/config/NumberConfigControl.tsx +5 -2
- package/corpus/templates/tasks/app/components/custom-fields/editor/config/PercentConfigControl.tsx +4 -1
- package/corpus/templates/tasks/app/components/custom-fields/editor/config/SelectConfigControl.tsx +17 -7
- package/corpus/templates/tasks/app/components/custom-fields/editor/config/select-colors.ts +37 -9
- package/corpus/templates/tasks/app/components/custom-fields/field-types.ts +23 -0
- package/corpus/templates/tasks/app/components/inbox/InboxList.tsx +24 -16
- package/corpus/templates/tasks/app/components/inbox/InboxListPage.tsx +5 -3
- package/corpus/templates/tasks/app/components/inbox/InboxListRow.tsx +7 -5
- package/corpus/templates/tasks/app/components/layout/Header.tsx +10 -10
- package/corpus/templates/tasks/app/components/layout/Layout.tsx +11 -7
- package/corpus/templates/tasks/app/components/layout/Sidebar.tsx +17 -10
- package/corpus/templates/tasks/app/components/shared/AddListItemInput.tsx +14 -8
- package/corpus/templates/tasks/app/components/shared/AlertDialog.tsx +6 -2
- package/corpus/templates/tasks/app/components/shared/BulkDeleteDialog.tsx +13 -6
- package/corpus/templates/tasks/app/components/shared/ChipSelect.tsx +12 -5
- package/corpus/templates/tasks/app/components/shared/DeleteItemDialog.tsx +6 -6
- package/corpus/templates/tasks/app/components/shared/ListViewHeader.tsx +3 -1
- package/corpus/templates/tasks/app/components/shared/SidePanel.tsx +5 -2
- package/corpus/templates/tasks/app/components/shared/list/ListRowDragHandle.tsx +3 -1
- package/corpus/templates/tasks/app/components/shared/selection/ListSelectionBar.tsx +46 -20
- package/corpus/templates/tasks/app/components/shared/selection/ListSelectionHeaderToggle.tsx +3 -1
- package/corpus/templates/tasks/app/components/shared/selection/ListSelectionToolbar.tsx +10 -6
- package/corpus/templates/tasks/app/components/tasks/TaskList.tsx +24 -13
- package/corpus/templates/tasks/app/components/tasks/TaskListHeaderRow.tsx +4 -1
- package/corpus/templates/tasks/app/components/tasks/TaskListPage.tsx +5 -3
- package/corpus/templates/tasks/app/components/tasks/TaskListRow.tsx +15 -6
- package/corpus/templates/tasks/app/components/tasks/fields/TaskFieldsSidebar.tsx +11 -6
- package/corpus/templates/tasks/app/components/tasks/fields/TaskTitleSection.tsx +8 -3
- package/corpus/templates/tasks/app/components/tasks/fields/controls/RichTextValueControl.tsx +12 -6
- package/corpus/templates/tasks/app/components/tasks/fields/controls/SingleSelectValueControl.tsx +6 -1
- package/corpus/templates/tasks/app/i18n/en-US.ts +209 -0
- package/corpus/templates/tasks/app/i18n/index.ts +34 -0
- package/corpus/templates/tasks/app/root.tsx +34 -2
- package/corpus/templates/tasks/app/routes/extensions.$id.tsx +2 -1
- package/corpus/templates/tasks/app/routes/extensions._index.tsx +2 -1
- package/corpus/templates/tasks/app/routes/fields.tsx +2 -1
- package/corpus/templates/tasks/app/routes/inbox.tsx +2 -1
- package/corpus/templates/tasks/app/routes/tasks.tsx +2 -1
- package/corpus/templates/tasks/app/routes/team.tsx +6 -3
- package/corpus/templates/tasks/changelog/2026-07-20-tasks-is-now-available-in-10-additional-languages.md +6 -0
- package/dist/agent/engine/builder-engine.d.ts.map +1 -1
- package/dist/agent/engine/builder-engine.js +4 -0
- package/dist/agent/engine/builder-engine.js.map +1 -1
- package/dist/application-state/index.d.ts +2 -2
- package/dist/application-state/index.d.ts.map +1 -1
- package/dist/application-state/index.js +2 -2
- package/dist/application-state/index.js.map +1 -1
- package/dist/application-state/script-helpers.d.ts +3 -1
- package/dist/application-state/script-helpers.d.ts.map +1 -1
- package/dist/application-state/script-helpers.js +7 -1
- package/dist/application-state/script-helpers.js.map +1 -1
- package/dist/application-state/store.d.ts +7 -1
- package/dist/application-state/store.d.ts.map +1 -1
- package/dist/application-state/store.js +143 -17
- package/dist/application-state/store.js.map +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +10 -3
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
- package/dist/client/ConnectBuilderCard.js +5 -1
- package/dist/client/ConnectBuilderCard.js.map +1 -1
- package/dist/client/chat/markdown-renderer.d.ts.map +1 -1
- package/dist/client/chat/markdown-renderer.js +3 -2
- package/dist/client/chat/markdown-renderer.js.map +1 -1
- package/dist/client/error-format.d.ts +1 -1
- package/dist/client/error-format.d.ts.map +1 -1
- package/dist/client/error-format.js +1 -1
- package/dist/client/error-format.js.map +1 -1
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/integrations/IntegrationsPanel.d.ts.map +1 -1
- package/dist/client/integrations/IntegrationsPanel.js +3 -4
- package/dist/client/integrations/IntegrationsPanel.js.map +1 -1
- package/dist/client/resources/McpIntegrationDialog.d.ts.map +1 -1
- package/dist/client/resources/McpIntegrationDialog.js +21 -7
- package/dist/client/resources/McpIntegrationDialog.js.map +1 -1
- package/dist/client/resources/mcp-integration-catalog.d.ts.map +1 -1
- package/dist/client/resources/mcp-integration-catalog.js +2 -2
- package/dist/client/resources/mcp-integration-catalog.js.map +1 -1
- package/dist/client/settings/BackgroundAgentSection.d.ts.map +1 -1
- package/dist/client/settings/BackgroundAgentSection.js +5 -1
- package/dist/client/settings/BackgroundAgentSection.js.map +1 -1
- package/dist/client/settings/SettingsPanel.js +1 -1
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
- package/dist/client/settings/useBuilderStatus.js +2 -0
- package/dist/client/settings/useBuilderStatus.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/db/client.d.ts +7 -0
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +39 -1
- package/dist/db/client.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/localization/default-messages.d.ts +0 -6
- package/dist/localization/default-messages.d.ts.map +1 -1
- package/dist/localization/default-messages.js +1 -7
- package/dist/localization/default-messages.js.map +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +3 -3
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/builder-browser.d.ts.map +1 -1
- package/dist/server/builder-browser.js +4 -0
- package/dist/server/builder-browser.js.map +1 -1
- package/dist/server/builder-design-systems.d.ts.map +1 -1
- package/dist/server/builder-design-systems.js +6 -1
- package/dist/server/builder-design-systems.js.map +1 -1
- package/dist/server/fusion-app.d.ts.map +1 -1
- package/dist/server/fusion-app.js +2 -1
- package/dist/server/fusion-app.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/shared/builder-link-tracking.d.ts +14 -0
- package/dist/shared/builder-link-tracking.d.ts.map +1 -0
- package/dist/shared/builder-link-tracking.js +26 -0
- package/dist/shared/builder-link-tracking.js.map +1 -0
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/index.js +1 -0
- package/dist/shared/index.js.map +1 -1
- package/docs/content/deployment.mdx +1 -1
- package/docs/content/frames.mdx +1 -1
- package/docs/content/integrations.mdx +27 -27
- package/docs/content/pr-visual-recap.mdx +17 -13
- package/docs/content/workspace-management.mdx +1 -1
- package/package.json +3 -2
- package/src/agent/engine/builder-engine.ts +4 -0
- package/src/application-state/index.ts +3 -0
- package/src/application-state/script-helpers.ts +12 -1
- package/src/application-state/store.ts +216 -15
- package/src/client/AgentPanel.tsx +10 -4
- package/src/client/ConnectBuilderCard.tsx +5 -1
- package/src/client/chat/markdown-renderer.tsx +3 -5
- package/src/client/error-format.ts +2 -1
- package/src/client/index.ts +1 -0
- package/src/client/integrations/IntegrationsPanel.tsx +2 -18
- package/src/client/resources/McpIntegrationDialog.tsx +43 -32
- package/src/client/resources/mcp-integration-catalog.ts +4 -2
- package/src/client/settings/BackgroundAgentSection.tsx +5 -1
- package/src/client/settings/SettingsPanel.tsx +4 -4
- package/src/client/settings/useBuilderStatus.ts +3 -0
- package/src/db/client.ts +44 -1
- package/src/localization/default-messages.ts +1 -7
- package/src/server/builder-browser.ts +4 -0
- package/src/server/builder-design-systems.ts +6 -1
- package/src/server/fusion-app.ts +5 -1
- package/src/shared/builder-link-tracking.ts +41 -0
- package/src/shared/index.ts +1 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { invoke } from "@tauri-apps/api/core";
|
|
2
|
+
import { listen } from "@tauri-apps/api/event";
|
|
3
|
+
import { useCallback, useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
import type { VoiceProvider } from "../lib/voice-dictation";
|
|
6
|
+
import type { FeatureConfig } from "../shared/config";
|
|
7
|
+
|
|
8
|
+
export interface WhisperModelOption {
|
|
9
|
+
id: string;
|
|
10
|
+
title: string;
|
|
11
|
+
description: string;
|
|
12
|
+
url: string;
|
|
13
|
+
filename: string;
|
|
14
|
+
sha256: string;
|
|
15
|
+
sizeBytes: number;
|
|
16
|
+
sizeMb: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type WhisperModelState =
|
|
20
|
+
| "disabled"
|
|
21
|
+
| "missing"
|
|
22
|
+
| "downloading"
|
|
23
|
+
| "ready";
|
|
24
|
+
|
|
25
|
+
export interface WhisperModelStatus {
|
|
26
|
+
state: WhisperModelState;
|
|
27
|
+
path: string;
|
|
28
|
+
downloadedMb: number;
|
|
29
|
+
totalMb: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const WHISPER_STATUS_EVENTS = [
|
|
33
|
+
"whisper:model-progress",
|
|
34
|
+
"whisper:model-error",
|
|
35
|
+
"whisper:model-enabled-changed",
|
|
36
|
+
"whisper:model-selection-changed",
|
|
37
|
+
] as const;
|
|
38
|
+
|
|
39
|
+
export function useWhisperSettings(
|
|
40
|
+
featureConfig: FeatureConfig | null,
|
|
41
|
+
voiceProvider: VoiceProvider,
|
|
42
|
+
onVoiceProviderChange: (provider: VoiceProvider) => void,
|
|
43
|
+
nativeVoiceProvider: () => VoiceProvider,
|
|
44
|
+
) {
|
|
45
|
+
const [catalog, setCatalog] = useState<WhisperModelOption[]>([]);
|
|
46
|
+
const [downloadedModelIds, setDownloadedModelIds] = useState<string[]>([]);
|
|
47
|
+
const [status, setStatus] = useState<WhisperModelStatus | null>(null);
|
|
48
|
+
|
|
49
|
+
const enabled = featureConfig?.whisperModelEnabled !== false;
|
|
50
|
+
const modelId = featureConfig?.whisperModelId ?? "base";
|
|
51
|
+
const selectedModel = catalog.find((m) => m.id === modelId) ?? null;
|
|
52
|
+
const deletableModels = catalog.filter(
|
|
53
|
+
(m) => m.id !== modelId && downloadedModelIds.includes(m.id),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
invoke<WhisperModelOption[]>("whisper_models")
|
|
58
|
+
.then(setCatalog)
|
|
59
|
+
.catch(() => {});
|
|
60
|
+
}, []);
|
|
61
|
+
|
|
62
|
+
const refreshDownloaded = useCallback(() => {
|
|
63
|
+
invoke<string[]>("whisper_downloaded_models")
|
|
64
|
+
.then(setDownloadedModelIds)
|
|
65
|
+
.catch(() => {});
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
let cancelled = false;
|
|
70
|
+
const refreshStatus = () => {
|
|
71
|
+
invoke<WhisperModelStatus>("whisper_model_status")
|
|
72
|
+
.then((s) => {
|
|
73
|
+
if (!cancelled) setStatus(s);
|
|
74
|
+
})
|
|
75
|
+
.catch(() => {});
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
refreshStatus();
|
|
79
|
+
refreshDownloaded();
|
|
80
|
+
|
|
81
|
+
const unlistens: Array<() => void> = [];
|
|
82
|
+
const track = (p: Promise<() => void>) => {
|
|
83
|
+
p.then((u) => {
|
|
84
|
+
if (cancelled) {
|
|
85
|
+
try {
|
|
86
|
+
u();
|
|
87
|
+
} catch {
|
|
88
|
+
/* ignore */
|
|
89
|
+
}
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
unlistens.push(u);
|
|
93
|
+
}).catch(() => {});
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
for (const event of WHISPER_STATUS_EVENTS) {
|
|
97
|
+
track(listen(event, () => refreshStatus()));
|
|
98
|
+
}
|
|
99
|
+
track(
|
|
100
|
+
listen("whisper:model-ready", () => {
|
|
101
|
+
refreshStatus();
|
|
102
|
+
refreshDownloaded();
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
105
|
+
track(listen("whisper:model-deleted", () => refreshDownloaded()));
|
|
106
|
+
|
|
107
|
+
return () => {
|
|
108
|
+
cancelled = true;
|
|
109
|
+
unlistens.forEach((u) => {
|
|
110
|
+
try {
|
|
111
|
+
u();
|
|
112
|
+
} catch {
|
|
113
|
+
/* ignore */
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
}, [refreshDownloaded]);
|
|
118
|
+
|
|
119
|
+
function triggerDownload() {
|
|
120
|
+
invoke("whisper_model_download").catch(() => {});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function setEnabled(next: boolean) {
|
|
124
|
+
if (!featureConfig) return;
|
|
125
|
+
invoke("set_feature_config", {
|
|
126
|
+
config: { ...featureConfig, whisperModelEnabled: next },
|
|
127
|
+
})
|
|
128
|
+
.then(() => {
|
|
129
|
+
if (next) {
|
|
130
|
+
triggerDownload();
|
|
131
|
+
} else if (voiceProvider === "whisper") {
|
|
132
|
+
onVoiceProviderChange(nativeVoiceProvider());
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
.catch((err) =>
|
|
136
|
+
console.error("[whisper] set_feature_config failed", err),
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function setModelId(next: string) {
|
|
141
|
+
if (!featureConfig) return;
|
|
142
|
+
invoke("set_feature_config", {
|
|
143
|
+
config: { ...featureConfig, whisperModelId: next },
|
|
144
|
+
})
|
|
145
|
+
.then(() => {
|
|
146
|
+
if (enabled) triggerDownload();
|
|
147
|
+
})
|
|
148
|
+
.catch((err) =>
|
|
149
|
+
console.error("[whisper] set_feature_config failed", err),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function deleteModel(modelIdToDelete: string) {
|
|
154
|
+
invoke("whisper_model_delete", { modelId: modelIdToDelete })
|
|
155
|
+
.then(() => refreshDownloaded())
|
|
156
|
+
.catch(() => {});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
catalog,
|
|
161
|
+
downloadedModelIds,
|
|
162
|
+
status,
|
|
163
|
+
enabled,
|
|
164
|
+
modelId,
|
|
165
|
+
selectedModel,
|
|
166
|
+
deletableModels,
|
|
167
|
+
triggerDownload,
|
|
168
|
+
setEnabled,
|
|
169
|
+
setModelId,
|
|
170
|
+
deleteModel,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const NATIVE_CALL_APP_BUNDLE_IDS = [
|
|
2
|
+
"us.zoom.xos",
|
|
3
|
+
"us.zoom.ZoomClips",
|
|
4
|
+
"com.microsoft.teams2",
|
|
5
|
+
"com.microsoft.teams",
|
|
6
|
+
] as const;
|
|
7
|
+
|
|
8
|
+
const BROWSER_CALL_APP_BUNDLE_IDS = [
|
|
9
|
+
"com.google.Chrome",
|
|
10
|
+
"company.thebrowser.Browser",
|
|
11
|
+
"com.apple.Safari",
|
|
12
|
+
"org.mozilla.firefox",
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
function isHost(hostname: string, host: string): boolean {
|
|
16
|
+
return hostname === host || hostname.endsWith(`.${host}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Limit microphone-use monitoring to the app that can own this meeting.
|
|
21
|
+
* Browser bundles are included only when the calendar join URL identifies a
|
|
22
|
+
* browser-hosted provider, so another tab using the microphone cannot end a
|
|
23
|
+
* native Zoom or Teams recording.
|
|
24
|
+
*/
|
|
25
|
+
export function callAppBundleIdsForJoinUrl(joinUrl?: string | null): string[] {
|
|
26
|
+
if (!joinUrl) return [...NATIVE_CALL_APP_BUNDLE_IDS];
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const hostname = new URL(joinUrl).hostname.toLowerCase();
|
|
30
|
+
if (isHost(hostname, "meet.google.com")) {
|
|
31
|
+
return [...BROWSER_CALL_APP_BUNDLE_IDS];
|
|
32
|
+
}
|
|
33
|
+
if (isHost(hostname, "teams.microsoft.com")) {
|
|
34
|
+
return [...NATIVE_CALL_APP_BUNDLE_IDS, ...BROWSER_CALL_APP_BUNDLE_IDS];
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
// Native Zoom and Teams remain the safe fallback for malformed URLs.
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return [...NATIVE_CALL_APP_BUNDLE_IDS];
|
|
41
|
+
}
|
|
@@ -1445,6 +1445,9 @@ export function installDesktopVoiceDictation(
|
|
|
1445
1445
|
// the tail because Web Speech only marks a segment as `isFinal`
|
|
1446
1446
|
// after a confidence-threshold pass.
|
|
1447
1447
|
next.browserTranscript = (finalSoFar + interim).trim();
|
|
1448
|
+
emit("voice:dictation-preview", {
|
|
1449
|
+
text: next.browserTranscript,
|
|
1450
|
+
}).catch(() => {});
|
|
1448
1451
|
};
|
|
1449
1452
|
recognition.onerror = (ev) => {
|
|
1450
1453
|
if (ev.error !== "no-speech" && ev.error !== "aborted") {
|
|
@@ -2038,15 +2041,16 @@ export function installDesktopVoiceDictation(
|
|
|
2038
2041
|
|
|
2039
2042
|
// Native (SFSpeechRecognizer) event subscriptions. These are always
|
|
2040
2043
|
// installed — the events only fire when the Rust side has an active
|
|
2041
|
-
// session, so subscribing on non-native sessions is harmless.
|
|
2042
|
-
// flow-bar listens to `voice:partial-transcript` independently so we
|
|
2043
|
-
// don't re-emit it here.
|
|
2044
|
+
// session, so subscribing on non-native sessions is harmless.
|
|
2044
2045
|
onPartialTranscript(({ text }) => {
|
|
2045
2046
|
const current = session;
|
|
2046
2047
|
if (!current || (current.kind !== "native" && current.kind !== "whisper"))
|
|
2047
2048
|
return;
|
|
2048
2049
|
if (current.cancelled || current.stopping) return;
|
|
2049
2050
|
setInterimTranscript(current, text);
|
|
2051
|
+
emit("voice:dictation-preview", {
|
|
2052
|
+
text: current.browserTranscript,
|
|
2053
|
+
}).catch(() => {});
|
|
2050
2054
|
})
|
|
2051
2055
|
.then((u) => unlistens.push(u))
|
|
2052
2056
|
.catch(() => {});
|
|
@@ -2065,6 +2069,13 @@ export function installDesktopVoiceDictation(
|
|
|
2065
2069
|
if (!current) return;
|
|
2066
2070
|
if (current.cancelled) return;
|
|
2067
2071
|
appendFinalTranscript(current, text);
|
|
2072
|
+
const supersededLingeringSession =
|
|
2073
|
+
current === lingeringSession && session !== null && session !== current;
|
|
2074
|
+
if (!supersededLingeringSession) {
|
|
2075
|
+
emit("voice:dictation-preview", {
|
|
2076
|
+
text: current.browserTranscript,
|
|
2077
|
+
}).catch(() => {});
|
|
2078
|
+
}
|
|
2068
2079
|
if (current === lingeringSession) {
|
|
2069
2080
|
current.onNativeFinalize?.();
|
|
2070
2081
|
}
|
|
@@ -16,6 +16,7 @@ type FlowState = "idle" | "recording" | "processing" | "complete" | "error";
|
|
|
16
16
|
* Events:
|
|
17
17
|
* - `voice:state-change` { state: "idle"|"recording"|"processing"|"complete"|"error" }
|
|
18
18
|
* - `voice:audio-level` { level: number } (0-1) for waveform visualization
|
|
19
|
+
* - `voice:dictation-preview` { text: string }
|
|
19
20
|
*/
|
|
20
21
|
export function FlowBar() {
|
|
21
22
|
// Default to "recording" not "idle" — there's a race between the Rust
|
|
@@ -23,6 +24,8 @@ export function FlowBar() {
|
|
|
23
24
|
// "idle" caused the bar to flash an "EN" language pill that never went
|
|
24
25
|
// away if the start event was missed.
|
|
25
26
|
const [state, setState] = useState<FlowState>("recording");
|
|
27
|
+
const [transcript, setTranscript] = useState("");
|
|
28
|
+
const transcriptRef = useRef<HTMLDivElement | null>(null);
|
|
26
29
|
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
|
27
30
|
const levelRef = useRef(0);
|
|
28
31
|
const rafRef = useRef<number | null>(null);
|
|
@@ -48,6 +51,7 @@ export function FlowBar() {
|
|
|
48
51
|
trackListen(
|
|
49
52
|
listen<{ state: FlowState }>("voice:state-change", (ev) => {
|
|
50
53
|
setState(ev.payload.state);
|
|
54
|
+
if (ev.payload.state === "recording") setTranscript("");
|
|
51
55
|
}),
|
|
52
56
|
);
|
|
53
57
|
|
|
@@ -57,6 +61,12 @@ export function FlowBar() {
|
|
|
57
61
|
}),
|
|
58
62
|
);
|
|
59
63
|
|
|
64
|
+
trackListen(
|
|
65
|
+
listen<{ text: string }>("voice:dictation-preview", (ev) => {
|
|
66
|
+
setTranscript(ev.payload.text.trim());
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
|
|
60
70
|
return () => {
|
|
61
71
|
stopped = true;
|
|
62
72
|
unlistens.forEach((u) => {
|
|
@@ -70,6 +80,11 @@ export function FlowBar() {
|
|
|
70
80
|
};
|
|
71
81
|
}, []);
|
|
72
82
|
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
const preview = transcriptRef.current;
|
|
85
|
+
if (preview) preview.scrollTop = preview.scrollHeight;
|
|
86
|
+
}, [transcript]);
|
|
87
|
+
|
|
73
88
|
// Waveform canvas rendering loop — only runs during the "recording" state.
|
|
74
89
|
useEffect(() => {
|
|
75
90
|
if (state !== "recording") {
|
|
@@ -151,6 +166,16 @@ export function FlowBar() {
|
|
|
151
166
|
|
|
152
167
|
return (
|
|
153
168
|
<div className="flow-bar-root">
|
|
169
|
+
{transcript ? (
|
|
170
|
+
<div
|
|
171
|
+
ref={transcriptRef}
|
|
172
|
+
className="flow-bar-transcript-preview"
|
|
173
|
+
aria-live="polite"
|
|
174
|
+
>
|
|
175
|
+
{transcript}
|
|
176
|
+
</div>
|
|
177
|
+
) : null}
|
|
178
|
+
|
|
154
179
|
{/* Pill is ALWAYS mounted — when state goes idle we fade the
|
|
155
180
|
opacity to 0 (see CSS) instead of removing it from the DOM.
|
|
156
181
|
Inner content keeps its last frame rendered during the fade
|
|
@@ -4905,13 +4905,33 @@ body[data-clips-route="recording-pill"] #root {
|
|
|
4905
4905
|
position: fixed;
|
|
4906
4906
|
inset: var(--overlay-shadow-gutter);
|
|
4907
4907
|
display: flex;
|
|
4908
|
+
flex-direction: column;
|
|
4908
4909
|
align-items: center;
|
|
4909
4910
|
justify-content: flex-end;
|
|
4911
|
+
gap: 6px;
|
|
4910
4912
|
padding-bottom: 8px;
|
|
4911
4913
|
background: transparent;
|
|
4912
4914
|
pointer-events: none;
|
|
4913
4915
|
}
|
|
4914
4916
|
|
|
4917
|
+
.flow-bar-transcript-preview {
|
|
4918
|
+
max-width: 560px;
|
|
4919
|
+
max-height: 90px;
|
|
4920
|
+
padding: 5px 10px;
|
|
4921
|
+
overflow: hidden;
|
|
4922
|
+
border: 1px solid rgba(255, 255, 255, 0.06);
|
|
4923
|
+
border-radius: 10px;
|
|
4924
|
+
background: rgba(18, 18, 20, 0.92);
|
|
4925
|
+
color: rgba(255, 255, 255, 0.9);
|
|
4926
|
+
font-size: 12px;
|
|
4927
|
+
font-weight: 500;
|
|
4928
|
+
line-height: 18px;
|
|
4929
|
+
text-align: center;
|
|
4930
|
+
word-break: break-word;
|
|
4931
|
+
backdrop-filter: blur(20px);
|
|
4932
|
+
-webkit-backdrop-filter: blur(20px);
|
|
4933
|
+
}
|
|
4934
|
+
|
|
4915
4935
|
.flow-bar {
|
|
4916
4936
|
display: flex;
|
|
4917
4937
|
align-items: center;
|
|
@@ -5580,23 +5600,39 @@ body[data-clips-route="recording-pill"] #root {
|
|
|
5580
5600
|
}
|
|
5581
5601
|
|
|
5582
5602
|
.whisper-progress-bar {
|
|
5583
|
-
height: 4px;
|
|
5584
5603
|
width: 100%;
|
|
5604
|
+
height: 4px;
|
|
5605
|
+
margin-top: 4px;
|
|
5606
|
+
border: none;
|
|
5585
5607
|
border-radius: 999px;
|
|
5586
|
-
|
|
5608
|
+
appearance: none;
|
|
5587
5609
|
overflow: hidden;
|
|
5588
|
-
|
|
5610
|
+
background: var(--surface-strong);
|
|
5611
|
+
color: #16a34a;
|
|
5589
5612
|
}
|
|
5590
5613
|
|
|
5591
|
-
.whisper-progress-
|
|
5592
|
-
|
|
5614
|
+
.whisper-progress-bar::-webkit-progress-bar {
|
|
5615
|
+
border-radius: inherit;
|
|
5616
|
+
background: var(--surface-strong);
|
|
5617
|
+
}
|
|
5618
|
+
|
|
5619
|
+
.whisper-progress-bar::-webkit-progress-value {
|
|
5620
|
+
border-radius: inherit;
|
|
5621
|
+
background: #16a34a;
|
|
5622
|
+
}
|
|
5623
|
+
|
|
5624
|
+
.whisper-progress-bar::-moz-progress-bar {
|
|
5593
5625
|
border-radius: inherit;
|
|
5594
5626
|
background: #16a34a;
|
|
5595
|
-
transition: width 300ms ease;
|
|
5596
5627
|
}
|
|
5597
5628
|
|
|
5598
5629
|
@media (prefers-color-scheme: dark) {
|
|
5599
|
-
.whisper-progress-
|
|
5630
|
+
.whisper-progress-bar {
|
|
5631
|
+
color: #4ade80;
|
|
5632
|
+
}
|
|
5633
|
+
|
|
5634
|
+
.whisper-progress-bar::-webkit-progress-value,
|
|
5635
|
+
.whisper-progress-bar::-moz-progress-bar {
|
|
5600
5636
|
background: #4ade80;
|
|
5601
5637
|
}
|
|
5602
5638
|
}
|
|
@@ -5624,6 +5660,63 @@ body[data-clips-route="recording-pill"] #root {
|
|
|
5624
5660
|
background: var(--surface-hover);
|
|
5625
5661
|
}
|
|
5626
5662
|
|
|
5663
|
+
.whisper-other-models {
|
|
5664
|
+
display: flex;
|
|
5665
|
+
flex-direction: column;
|
|
5666
|
+
gap: 4px;
|
|
5667
|
+
margin-top: 6px;
|
|
5668
|
+
}
|
|
5669
|
+
|
|
5670
|
+
.whisper-other-model-row {
|
|
5671
|
+
display: flex;
|
|
5672
|
+
align-items: center;
|
|
5673
|
+
justify-content: space-between;
|
|
5674
|
+
gap: 8px;
|
|
5675
|
+
padding: 5px 8px;
|
|
5676
|
+
border: 1px solid var(--border);
|
|
5677
|
+
border-radius: var(--radius-sm);
|
|
5678
|
+
background: var(--surface);
|
|
5679
|
+
}
|
|
5680
|
+
|
|
5681
|
+
.whisper-other-model-name {
|
|
5682
|
+
font-size: 11px;
|
|
5683
|
+
color: var(--fg-muted);
|
|
5684
|
+
}
|
|
5685
|
+
|
|
5686
|
+
.whisper-delete-btn {
|
|
5687
|
+
display: inline-flex;
|
|
5688
|
+
align-items: center;
|
|
5689
|
+
gap: 4px;
|
|
5690
|
+
flex-shrink: 0;
|
|
5691
|
+
height: 22px;
|
|
5692
|
+
padding: 0 8px;
|
|
5693
|
+
border: 1px solid var(--border-strong);
|
|
5694
|
+
border-radius: var(--radius-sm);
|
|
5695
|
+
background: var(--surface);
|
|
5696
|
+
color: var(--fg-muted);
|
|
5697
|
+
cursor: pointer;
|
|
5698
|
+
font-size: 11px;
|
|
5699
|
+
font-weight: 600;
|
|
5700
|
+
transition:
|
|
5701
|
+
background 120ms,
|
|
5702
|
+
color 120ms,
|
|
5703
|
+
border-color 120ms;
|
|
5704
|
+
}
|
|
5705
|
+
|
|
5706
|
+
.whisper-delete-btn:hover {
|
|
5707
|
+
background: rgba(239, 68, 68, 0.1);
|
|
5708
|
+
border-color: rgba(239, 68, 68, 0.4);
|
|
5709
|
+
color: #ef4444;
|
|
5710
|
+
}
|
|
5711
|
+
|
|
5712
|
+
@media (prefers-color-scheme: dark) {
|
|
5713
|
+
.whisper-delete-btn:hover {
|
|
5714
|
+
background: rgba(239, 68, 68, 0.15);
|
|
5715
|
+
border-color: rgba(239, 68, 68, 0.5);
|
|
5716
|
+
color: #f87171;
|
|
5717
|
+
}
|
|
5718
|
+
}
|
|
5719
|
+
|
|
5627
5720
|
/* ---- */
|
|
5628
5721
|
|
|
5629
5722
|
.pill-error {
|
|
@@ -1506,10 +1506,9 @@ pub async fn show_flow_bar(app: AppHandle) -> Result<(), String> {
|
|
|
1506
1506
|
|
|
1507
1507
|
let (mx, my, mw, mh) = tray_monitor_physical_rect(&app);
|
|
1508
1508
|
let scale = overlay_scale_factor(&app);
|
|
1509
|
-
//
|
|
1510
|
-
|
|
1511
|
-
let
|
|
1512
|
-
let content_h: u32 = (56.0 * scale).round() as u32;
|
|
1509
|
+
// Wide + tall enough for a 5-line transcript preview above the pill.
|
|
1510
|
+
let content_w: u32 = (640.0 * scale).round() as u32;
|
|
1511
|
+
let content_h: u32 = (160.0 * scale).round() as u32;
|
|
1513
1512
|
let bottom_margin: i32 = (14.0 * scale).round() as i32;
|
|
1514
1513
|
let gutter = overlay_shadow_gutter_physical(&app);
|
|
1515
1514
|
let w: u32 = content_w + gutter * 2;
|
|
@@ -135,6 +135,8 @@ pub struct FeatureConfig {
|
|
|
135
135
|
pub onboarding_complete: bool,
|
|
136
136
|
#[serde(default = "default_whisper_model_enabled")]
|
|
137
137
|
pub whisper_model_enabled: bool,
|
|
138
|
+
#[serde(default = "default_whisper_model_id")]
|
|
139
|
+
pub whisper_model_id: String,
|
|
138
140
|
}
|
|
139
141
|
|
|
140
142
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
@@ -170,6 +172,10 @@ fn default_whisper_model_enabled() -> bool {
|
|
|
170
172
|
true
|
|
171
173
|
}
|
|
172
174
|
|
|
175
|
+
fn default_whisper_model_id() -> String {
|
|
176
|
+
"base".to_string()
|
|
177
|
+
}
|
|
178
|
+
|
|
173
179
|
fn default_screen_memory_retention_hours() -> u32 {
|
|
174
180
|
8
|
|
175
181
|
}
|
|
@@ -227,6 +233,7 @@ impl Default for FeatureConfig {
|
|
|
227
233
|
screen_memory: ScreenMemoryConfig::default(),
|
|
228
234
|
onboarding_complete: false,
|
|
229
235
|
whisper_model_enabled: default_whisper_model_enabled(),
|
|
236
|
+
whisper_model_id: default_whisper_model_id(),
|
|
230
237
|
}
|
|
231
238
|
}
|
|
232
239
|
}
|
|
@@ -333,6 +340,12 @@ pub async fn get_feature_config(app: AppHandle) -> Result<FeatureConfig, String>
|
|
|
333
340
|
/// Save feature config to disk and emit a change event.
|
|
334
341
|
#[tauri::command]
|
|
335
342
|
pub async fn set_feature_config(app: AppHandle, config: FeatureConfig) -> Result<(), String> {
|
|
343
|
+
if !crate::whisper_model::is_supported_model_id(&config.whisper_model_id) {
|
|
344
|
+
return Err(format!(
|
|
345
|
+
"unsupported Whisper model: {}",
|
|
346
|
+
config.whisper_model_id
|
|
347
|
+
));
|
|
348
|
+
}
|
|
336
349
|
let previous = load_config(&app);
|
|
337
350
|
if (crate::rewind_clip::is_active(&app) || crate::util::is_recording_active(&app))
|
|
338
351
|
&& rewind_capture_contract_changed(&previous.screen_memory, &config.screen_memory)
|
|
@@ -366,6 +379,12 @@ pub async fn set_feature_config(app: AppHandle, config: FeatureConfig) -> Result
|
|
|
366
379
|
serde_json::json!({ "enabled": config.whisper_model_enabled }),
|
|
367
380
|
);
|
|
368
381
|
}
|
|
382
|
+
if previous.whisper_model_id != config.whisper_model_id {
|
|
383
|
+
let _ = app.emit(
|
|
384
|
+
"whisper:model-selection-changed",
|
|
385
|
+
serde_json::json!({ "modelId": config.whisper_model_id }),
|
|
386
|
+
);
|
|
387
|
+
}
|
|
369
388
|
crate::screen_memory::sync_from_config(&app, &config);
|
|
370
389
|
let _ = app.emit("app:feature-config-changed", config);
|
|
371
390
|
Ok(())
|
|
@@ -406,6 +425,19 @@ mod tests {
|
|
|
406
425
|
.contains(&"com.1password.1password".to_string()));
|
|
407
426
|
}
|
|
408
427
|
|
|
428
|
+
#[test]
|
|
429
|
+
fn feature_config_defaults_to_base_whisper_model_for_legacy_json() {
|
|
430
|
+
let config: FeatureConfig = serde_json::from_value(serde_json::json!({
|
|
431
|
+
"clipsEnabled": true,
|
|
432
|
+
"meetingsEnabled": true,
|
|
433
|
+
"voiceEnabled": true,
|
|
434
|
+
"onboardingComplete": true
|
|
435
|
+
}))
|
|
436
|
+
.unwrap();
|
|
437
|
+
|
|
438
|
+
assert_eq!(config.whisper_model_id, "base");
|
|
439
|
+
}
|
|
440
|
+
|
|
409
441
|
#[test]
|
|
410
442
|
fn rewind_enums_use_stable_kebab_case_values() {
|
|
411
443
|
assert_eq!(
|
|
@@ -231,8 +231,11 @@ pub fn run() {
|
|
|
231
231
|
shortcuts::set_fn_shortcut_enabled,
|
|
232
232
|
shortcuts::set_dictation_escape_active,
|
|
233
233
|
// whisper model management
|
|
234
|
+
whisper_model::whisper_models,
|
|
234
235
|
whisper_model::whisper_model_status,
|
|
235
236
|
whisper_model::whisper_model_download,
|
|
237
|
+
whisper_model::whisper_downloaded_models,
|
|
238
|
+
whisper_model::whisper_model_delete,
|
|
236
239
|
// permission status (silent checks for all TCC permissions)
|
|
237
240
|
permission_status::check_permission_statuses,
|
|
238
241
|
// persistent log file (production debugging)
|
|
@@ -2465,8 +2465,15 @@ impl LiveAudioMixer {
|
|
|
2465
2465
|
}
|
|
2466
2466
|
|
|
2467
2467
|
/// Emit mixed sample buffers covering `out_pos..safe_end` in
|
|
2468
|
-
/// `MIX_CHUNK_FRAMES` chunks:
|
|
2469
|
-
/// LPCM `CMSampleBuffer`s with contiguous PTS.
|
|
2468
|
+
/// `MIX_CHUNK_FRAMES` chunks: average system + mic per frame, clamp, wrap
|
|
2469
|
+
/// as LPCM `CMSampleBuffer`s with contiguous PTS.
|
|
2470
|
+
///
|
|
2471
|
+
/// Each source is weighted at 0.5 before summing so that two full-scale
|
|
2472
|
+
/// signals (which occurs when a USB audio interface with software monitoring
|
|
2473
|
+
/// routes the mic back through system audio) can never exceed ±1.0 and
|
|
2474
|
+
/// hard-clip. The standard SCK pipeline applies the same 0.5×L + 0.5×R
|
|
2475
|
+
/// pan-downmix for the same reason; loudnorm restores the target loudness
|
|
2476
|
+
/// in post-processing.
|
|
2470
2477
|
fn drain_ready(
|
|
2471
2478
|
&mut self,
|
|
2472
2479
|
flush: bool,
|
|
@@ -2488,8 +2495,8 @@ impl LiveAudioMixer {
|
|
|
2488
2495
|
let frame = a + f as i64;
|
|
2489
2496
|
let (sl, sr) = self.system.sample_at(frame);
|
|
2490
2497
|
let (ml, mr) = self.mic.sample_at(frame);
|
|
2491
|
-
interleaved[f * 2] = (sl + ml).clamp(-1.0, 1.0);
|
|
2492
|
-
interleaved[f * 2 + 1] = (sr + mr).clamp(-1.0, 1.0);
|
|
2498
|
+
interleaved[f * 2] = (sl * 0.5 + ml * 0.5).clamp(-1.0, 1.0);
|
|
2499
|
+
interleaved[f * 2 + 1] = (sr * 0.5 + mr * 0.5).clamp(-1.0, 1.0);
|
|
2493
2500
|
}
|
|
2494
2501
|
emitted.push(self.build_sample_buffer(&interleaved, a)?);
|
|
2495
2502
|
a = b;
|