@agent-native/core 0.114.13 → 0.114.16
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/bin/agent-native.js +35 -14
- package/bin/launcher.js +34 -0
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +21 -0
- package/corpus/core/bin/agent-native.js +35 -14
- package/corpus/core/bin/launcher.js +34 -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/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/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/library/bulk-action-toolbar.tsx +25 -8
- package/corpus/templates/clips/app/components/library/library-grid.tsx +118 -9
- package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -1
- package/corpus/templates/clips/app/components/library/recording-card.tsx +10 -8
- package/corpus/templates/clips/app/components/player/video-player.tsx +10 -4
- package/corpus/templates/clips/app/i18n/en-US.ts +6 -0
- package/corpus/templates/clips/app/routes/_app.trash.tsx +16 -1
- 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-meeting-notes-stop-when-calls-end.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +90 -78
- 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/shared/config.ts +1 -0
- package/corpus/templates/clips/desktop/src/styles.css +80 -7
- 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/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/content/app/components/editor/database/DatabaseView.tsx +20 -38
- package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +40 -1
- 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/pages/DesignSystemSetup.tsx +9 -1
- package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +9 -1
- 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/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/struct-routes.d.ts +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/observability/routes.d.ts +5 -5
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- 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/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/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
|
@@ -1,116 +1,205 @@
|
|
|
1
|
-
//! Whisper model
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
//! Resolves where the `ggml-base.bin` model lives, downloads it from
|
|
5
|
-
//! HuggingFace on first use, and verifies the download against a pinned
|
|
6
|
-
//! SHA-256 + byte size so a corrupted, truncated, or tampered file is rejected
|
|
7
|
-
//! rather than loaded
|
|
8
|
-
|
|
9
|
-
use std::path::PathBuf;
|
|
1
|
+
//! Local Whisper model catalog, download, and integrity verification.
|
|
2
|
+
|
|
3
|
+
use std::path::{Path, PathBuf};
|
|
10
4
|
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
|
11
5
|
|
|
12
6
|
use serde::Serialize;
|
|
13
7
|
use tauri::{AppHandle, Emitter, Manager};
|
|
14
8
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
9
|
+
#[derive(Debug, Clone, Copy, Serialize)]
|
|
10
|
+
#[serde(rename_all = "camelCase")]
|
|
11
|
+
pub struct WhisperModel {
|
|
12
|
+
pub id: &'static str,
|
|
13
|
+
pub title: &'static str,
|
|
14
|
+
pub description: &'static str,
|
|
15
|
+
pub url: &'static str,
|
|
16
|
+
pub filename: &'static str,
|
|
17
|
+
pub sha256: &'static str,
|
|
18
|
+
pub size_bytes: u64,
|
|
19
|
+
pub size_mb: u64,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const TINY_MODEL: WhisperModel = WhisperModel {
|
|
23
|
+
id: "tiny",
|
|
24
|
+
title: "Tiny",
|
|
25
|
+
description: "Transcribes instantly and uses very little space. May miss some words or struggle with accents, but great if speed is all that matters.",
|
|
26
|
+
url: "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.bin",
|
|
27
|
+
filename: "ggml-tiny.bin",
|
|
28
|
+
sha256: "be07e048e1e599ad46341c8d2a135645097a538221678b7acdd1b1919c6e1b21",
|
|
29
|
+
size_bytes: 77_691_713,
|
|
30
|
+
size_mb: 74,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const BASE_MODEL: WhisperModel = WhisperModel {
|
|
34
|
+
id: "base",
|
|
35
|
+
title: "Base",
|
|
36
|
+
description: "A great everyday choice. Transcribes quickly and gets most things right for dictation and meetings.",
|
|
37
|
+
url: "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.bin",
|
|
38
|
+
filename: "ggml-base.bin",
|
|
39
|
+
sha256: "60ed5bc3dd14eea856493d334349b405782ddcaf0028d4b5df4088345fba2efe",
|
|
40
|
+
size_bytes: 147_951_465,
|
|
41
|
+
size_mb: 141,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const SMALL_MODEL: WhisperModel = WhisperModel {
|
|
45
|
+
id: "small",
|
|
46
|
+
title: "Small",
|
|
47
|
+
description: "More accurate than Base, especially with accents or background noise. Takes a bit longer and uses more space.",
|
|
48
|
+
url: "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-small.bin",
|
|
49
|
+
filename: "ggml-small.bin",
|
|
50
|
+
sha256: "1be3a9b2063867b937e64e2ec7483364a79917e157fa98c5d94b5c1fffea987b",
|
|
51
|
+
size_bytes: 487_601_967,
|
|
52
|
+
size_mb: 488,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const LARGE_V3_TURBO_MODEL: WhisperModel = WhisperModel {
|
|
56
|
+
id: "large-v3-turbo",
|
|
57
|
+
title: "Large v3 Turbo",
|
|
58
|
+
description: "The most accurate option. Catches subtle speech and complex vocabulary well. Best on newer Macs; requires a larger one-time download.",
|
|
59
|
+
url: "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3-turbo.bin",
|
|
60
|
+
filename: "ggml-large-v3-turbo.bin",
|
|
61
|
+
sha256: "1fc70f774d38eb169993ac391eea357ef47c88757ef72ee5943879b7e8e2bc69",
|
|
62
|
+
size_bytes: 1_624_555_275,
|
|
63
|
+
size_mb: 1549,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const SUPPORTED_MODELS: &[WhisperModel] =
|
|
67
|
+
&[TINY_MODEL, BASE_MODEL, SMALL_MODEL, LARGE_V3_TURBO_MODEL];
|
|
20
68
|
|
|
21
|
-
// Global download-in-flight state so the status command and concurrent callers
|
|
22
|
-
// can inspect without re-checking the filesystem.
|
|
23
69
|
static DOWNLOADING: AtomicBool = AtomicBool::new(false);
|
|
24
70
|
static DOWNLOADED_BYTES: AtomicU64 = AtomicU64::new(0);
|
|
25
71
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Internal helpers
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
29
76
|
pub(crate) fn custom_model_override() -> bool {
|
|
30
77
|
std::env::var("CLIPS_WHISPER_MODEL")
|
|
31
78
|
.map(|v| !v.trim().is_empty())
|
|
32
79
|
.unwrap_or(false)
|
|
33
80
|
}
|
|
34
81
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
82
|
+
pub(crate) fn is_supported_model_id(id: &str) -> bool {
|
|
83
|
+
SUPPORTED_MODELS.iter().any(|m| m.id == id)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fn find_model(id: &str) -> Result<&'static WhisperModel, String> {
|
|
87
|
+
SUPPORTED_MODELS
|
|
88
|
+
.iter()
|
|
89
|
+
.find(|m| m.id == id)
|
|
90
|
+
.ok_or_else(|| format!("unsupported Whisper model: {id}"))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// Returns the directory where model files are stored, creating it if needed.
|
|
94
|
+
fn models_dir(app: &AppHandle) -> Result<PathBuf, String> {
|
|
43
95
|
let dir = app
|
|
44
96
|
.path()
|
|
45
97
|
.app_data_dir()
|
|
46
98
|
.map_err(|e| format!("no app_data_dir: {e}"))?
|
|
47
99
|
.join("models");
|
|
48
100
|
std::fs::create_dir_all(&dir).map_err(|e| format!("mkdir models: {e}"))?;
|
|
49
|
-
Ok(dir
|
|
101
|
+
Ok(dir)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// Resolve the full path for a catalog model on disk.
|
|
105
|
+
fn model_path(dir: &Path, model: &WhisperModel) -> PathBuf {
|
|
106
|
+
dir.join(model.filename)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
fn is_complete_on_disk(path: &Path, model: &WhisperModel) -> bool {
|
|
110
|
+
std::fs::metadata(path)
|
|
111
|
+
.map(|m| m.len() == model.size_bytes)
|
|
112
|
+
.unwrap_or(false)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
fn downloaded_mb(bytes: u64, model: &WhisperModel) -> u64 {
|
|
116
|
+
bytes.saturating_mul(model.size_mb) / model.size_bytes
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Public: resolve model file path (honors CLIPS_WHISPER_MODEL env override)
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
pub fn model_file(app: &AppHandle) -> Result<PathBuf, String> {
|
|
124
|
+
if let Ok(path) = std::env::var("CLIPS_WHISPER_MODEL") {
|
|
125
|
+
if !path.trim().is_empty() {
|
|
126
|
+
return Ok(PathBuf::from(path));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
let config = crate::config::feature_config(app);
|
|
130
|
+
let model = find_model(&config.whisper_model_id)?;
|
|
131
|
+
Ok(model_path(&models_dir(app)?, model))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// Tauri commands
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
#[tauri::command]
|
|
139
|
+
pub async fn whisper_models() -> Vec<WhisperModel> {
|
|
140
|
+
SUPPORTED_MODELS.to_vec()
|
|
50
141
|
}
|
|
51
142
|
|
|
52
|
-
/// Model status returned to the frontend settings UI.
|
|
53
143
|
#[derive(Debug, Clone, Serialize)]
|
|
54
144
|
#[serde(rename_all = "camelCase")]
|
|
55
145
|
pub struct ModelStatus {
|
|
56
|
-
/// One of: "disabled" | "missing" | "downloading" | "ready"
|
|
57
146
|
pub state: String,
|
|
58
|
-
/// Absolute path where the model file lives (or will live).
|
|
59
147
|
pub path: String,
|
|
60
|
-
/// How many MB have been downloaded so far (only meaningful during "downloading").
|
|
61
148
|
pub downloaded_mb: u64,
|
|
62
|
-
/// Total model size in MB.
|
|
63
149
|
pub total_mb: u64,
|
|
64
150
|
}
|
|
65
151
|
|
|
66
|
-
/// Return the current model state without triggering a download.
|
|
67
152
|
#[tauri::command]
|
|
68
153
|
pub async fn whisper_model_status(app: AppHandle) -> Result<ModelStatus, String> {
|
|
69
|
-
|
|
70
|
-
|
|
154
|
+
if custom_model_override() {
|
|
155
|
+
let path = model_file(&app)?;
|
|
156
|
+
let ready = path.exists();
|
|
157
|
+
return Ok(ModelStatus {
|
|
158
|
+
state: if ready { "ready" } else { "missing" }.into(),
|
|
159
|
+
path: path.to_string_lossy().into(),
|
|
160
|
+
downloaded_mb: 0,
|
|
161
|
+
total_mb: 0,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
71
165
|
let config = crate::config::feature_config(&app);
|
|
166
|
+
let model = find_model(&config.whisper_model_id)?;
|
|
167
|
+
let dir = models_dir(&app)?;
|
|
168
|
+
let path = model_path(&dir, model);
|
|
169
|
+
let path_str = path.to_string_lossy().into_owned();
|
|
72
170
|
|
|
73
171
|
if !config.whisper_model_enabled {
|
|
74
172
|
return Ok(ModelStatus {
|
|
75
173
|
state: "disabled".into(),
|
|
76
174
|
path: path_str,
|
|
77
175
|
downloaded_mb: 0,
|
|
78
|
-
total_mb:
|
|
176
|
+
total_mb: model.size_mb,
|
|
79
177
|
});
|
|
80
178
|
}
|
|
81
179
|
if DOWNLOADING.load(Ordering::Relaxed) {
|
|
82
|
-
let downloaded_mb = DOWNLOADED_BYTES.load(Ordering::Relaxed) / (1024 * 1024);
|
|
83
180
|
return Ok(ModelStatus {
|
|
84
181
|
state: "downloading".into(),
|
|
85
182
|
path: path_str,
|
|
86
|
-
downloaded_mb,
|
|
87
|
-
total_mb:
|
|
183
|
+
downloaded_mb: downloaded_mb(DOWNLOADED_BYTES.load(Ordering::Relaxed), model),
|
|
184
|
+
total_mb: model.size_mb,
|
|
88
185
|
});
|
|
89
186
|
}
|
|
90
|
-
let
|
|
91
|
-
Ok(m) if m.len() == MODEL_SIZE || custom_model_override() => "ready",
|
|
92
|
-
_ => "missing",
|
|
93
|
-
};
|
|
187
|
+
let ready = is_complete_on_disk(&path, model);
|
|
94
188
|
Ok(ModelStatus {
|
|
95
|
-
state:
|
|
189
|
+
state: if ready { "ready" } else { "missing" }.into(),
|
|
96
190
|
path: path_str,
|
|
97
|
-
downloaded_mb: if
|
|
98
|
-
total_mb:
|
|
191
|
+
downloaded_mb: if ready { model.size_mb } else { 0 },
|
|
192
|
+
total_mb: model.size_mb,
|
|
99
193
|
})
|
|
100
194
|
}
|
|
101
195
|
|
|
102
|
-
/// Spawn a background download. Idempotent — no-ops if already downloading or
|
|
103
|
-
/// already present. Emits `whisper:model-progress`, `whisper:model-ready`, or
|
|
104
|
-
/// `whisper:model-error` as the download progresses.
|
|
105
196
|
#[tauri::command]
|
|
106
197
|
pub async fn whisper_model_download(app: AppHandle) -> Result<(), String> {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if let Ok(m) = std::fs::metadata(&path) {
|
|
113
|
-
if m.len() == MODEL_SIZE || custom_model_override() {
|
|
198
|
+
let path = model_file(&app)?;
|
|
199
|
+
if let Ok(meta) = std::fs::metadata(&path) {
|
|
200
|
+
let config = crate::config::feature_config(&app);
|
|
201
|
+
if let Ok(model) = find_model(&config.whisper_model_id) {
|
|
202
|
+
if meta.len() == model.size_bytes || custom_model_override() {
|
|
114
203
|
let _ = app.emit("whisper:model-ready", ());
|
|
115
204
|
return Ok(());
|
|
116
205
|
}
|
|
@@ -130,164 +219,196 @@ pub async fn whisper_model_download(app: AppHandle) -> Result<(), String> {
|
|
|
130
219
|
Ok(())
|
|
131
220
|
}
|
|
132
221
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
222
|
+
#[tauri::command]
|
|
223
|
+
pub async fn whisper_downloaded_models(app: AppHandle) -> Vec<String> {
|
|
224
|
+
let Ok(dir) = models_dir(&app) else {
|
|
225
|
+
return vec![];
|
|
226
|
+
};
|
|
227
|
+
SUPPORTED_MODELS
|
|
228
|
+
.iter()
|
|
229
|
+
.filter(|m| is_complete_on_disk(&model_path(&dir, m), m))
|
|
230
|
+
.map(|m| m.id.to_string())
|
|
231
|
+
.collect()
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
#[tauri::command]
|
|
235
|
+
pub async fn whisper_model_delete(app: AppHandle, model_id: String) -> Result<(), String> {
|
|
236
|
+
if custom_model_override() {
|
|
237
|
+
return Err("Cannot manage models while CLIPS_WHISPER_MODEL is set.".to_string());
|
|
238
|
+
}
|
|
239
|
+
if crate::config::feature_config(&app).whisper_model_id == model_id {
|
|
240
|
+
return Err("Cannot delete the model that is currently selected.".to_string());
|
|
241
|
+
}
|
|
242
|
+
let model = find_model(&model_id)?;
|
|
243
|
+
let path = model_path(&models_dir(&app)?, model);
|
|
244
|
+
if path.exists() {
|
|
245
|
+
std::fs::remove_file(&path).map_err(|e| format!("delete model: {e}"))?;
|
|
246
|
+
}
|
|
247
|
+
let _ = app.emit(
|
|
248
|
+
"whisper:model-deleted",
|
|
249
|
+
serde_json::json!({ "modelId": model_id }),
|
|
250
|
+
);
|
|
251
|
+
Ok(())
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
// Download logic (called by whisper_model_download and lib.rs startup prewarm)
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
|
|
258
|
+
pub(crate) async fn ensure_model(app: &AppHandle) -> Result<PathBuf, String> {
|
|
142
259
|
let path = model_file(app)?;
|
|
143
260
|
let custom = custom_model_override();
|
|
144
261
|
|
|
145
|
-
if custom
|
|
262
|
+
if custom {
|
|
263
|
+
if path.exists() {
|
|
264
|
+
eprintln!("[whisper] using custom model at {}", path.display());
|
|
265
|
+
return Ok(path);
|
|
266
|
+
}
|
|
146
267
|
return Err(format!(
|
|
147
|
-
"CLIPS_WHISPER_MODEL
|
|
268
|
+
"CLIPS_WHISPER_MODEL points to '{}' but the file does not exist.",
|
|
148
269
|
path.display()
|
|
149
270
|
));
|
|
150
271
|
}
|
|
151
272
|
|
|
273
|
+
let config = crate::config::feature_config(app);
|
|
274
|
+
let model = find_model(&config.whisper_model_id)?;
|
|
275
|
+
|
|
276
|
+
if is_complete_on_disk(&path, model) {
|
|
277
|
+
eprintln!("[whisper] model found at {}", path.display());
|
|
278
|
+
return Ok(path);
|
|
279
|
+
}
|
|
152
280
|
if path.exists() {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
match std::fs::metadata(&path) {
|
|
158
|
-
Ok(m) if m.len() == MODEL_SIZE => {
|
|
159
|
-
eprintln!("[whisper] model found at {}", path.display());
|
|
160
|
-
return Ok(path);
|
|
161
|
-
}
|
|
162
|
-
Ok(m) => {
|
|
163
|
-
eprintln!(
|
|
164
|
-
"[whisper] cached model size {} != expected {} — re-downloading",
|
|
165
|
-
m.len(),
|
|
166
|
-
MODEL_SIZE
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
Err(e) => return Err(format!("stat model: {e}")),
|
|
170
|
-
}
|
|
281
|
+
eprintln!(
|
|
282
|
+
"[whisper] cached model has wrong size — re-downloading {}",
|
|
283
|
+
path.display()
|
|
284
|
+
);
|
|
171
285
|
}
|
|
172
286
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
287
|
+
loop {
|
|
288
|
+
if DOWNLOADING
|
|
289
|
+
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
|
|
290
|
+
.is_ok()
|
|
291
|
+
{
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
eprintln!("[whisper] waiting for in-progress download…");
|
|
296
|
+
while DOWNLOADING.load(Ordering::SeqCst) {
|
|
178
297
|
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
|
179
|
-
if !DOWNLOADING.load(Ordering::SeqCst) {
|
|
180
|
-
break;
|
|
181
|
-
}
|
|
182
298
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if custom {
|
|
186
|
-
return Ok(path);
|
|
187
|
-
}
|
|
188
|
-
if let Ok(m) = std::fs::metadata(&path) {
|
|
189
|
-
if m.len() == MODEL_SIZE {
|
|
190
|
-
return Ok(path);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
299
|
+
if is_complete_on_disk(&path, model) {
|
|
300
|
+
return Ok(path);
|
|
193
301
|
}
|
|
194
302
|
}
|
|
195
|
-
// Guard against concurrent downloads.
|
|
196
|
-
if DOWNLOADING
|
|
197
|
-
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
|
|
198
|
-
.is_err()
|
|
199
|
-
{
|
|
200
|
-
return Err("Whisper model download already in progress — please wait.".to_string());
|
|
201
|
-
}
|
|
202
303
|
DOWNLOADED_BYTES.store(0, Ordering::Relaxed);
|
|
203
304
|
|
|
204
|
-
let result = do_download(app, &path,
|
|
305
|
+
let result = do_download(app, &path, model).await;
|
|
205
306
|
DOWNLOADING.store(false, Ordering::SeqCst);
|
|
206
307
|
result
|
|
207
308
|
}
|
|
208
309
|
|
|
209
|
-
async fn do_download(
|
|
310
|
+
async fn do_download(
|
|
311
|
+
app: &AppHandle,
|
|
312
|
+
path: &Path,
|
|
313
|
+
model: &WhisperModel,
|
|
314
|
+
) -> Result<PathBuf, String> {
|
|
210
315
|
eprintln!(
|
|
211
|
-
"[whisper]
|
|
212
|
-
|
|
213
|
-
MODEL_URL
|
|
316
|
+
"[whisper] downloading {} (~{} MB)",
|
|
317
|
+
model.url, model.size_mb
|
|
214
318
|
);
|
|
215
|
-
let mut
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if !resp.status().is_success() {
|
|
221
|
-
let msg = format!("model download HTTP {}", resp.status());
|
|
222
|
-
eprintln!("[whisper] {msg}");
|
|
223
|
-
return Err(msg);
|
|
319
|
+
let mut response = reqwest::get(model.url)
|
|
320
|
+
.await
|
|
321
|
+
.map_err(|e| format!("model download failed: {e}"))?;
|
|
322
|
+
if !response.status().is_success() {
|
|
323
|
+
return Err(format!("model download HTTP {}", response.status()));
|
|
224
324
|
}
|
|
225
325
|
|
|
226
|
-
// Stream body to a temp file, hashing as we go. Keeps memory flat
|
|
227
|
-
// (no 142 MB heap spike) and lets us verify before the rename.
|
|
228
326
|
use sha2::{Digest, Sha256};
|
|
229
327
|
use std::io::Write as _;
|
|
230
328
|
|
|
231
329
|
let tmp = path.with_extension("bin.tmp");
|
|
232
|
-
let mut file = std::fs::File::create(&tmp).map_err(|e| format!("create
|
|
330
|
+
let mut file = std::fs::File::create(&tmp).map_err(|e| format!("create tmp: {e}"))?;
|
|
233
331
|
let mut hasher = Sha256::new();
|
|
234
|
-
let mut total
|
|
235
|
-
let mut last_progress
|
|
332
|
+
let mut total = 0_u64;
|
|
333
|
+
let mut last_progress = 0_u64;
|
|
236
334
|
|
|
237
|
-
while let Some(chunk) =
|
|
335
|
+
while let Some(chunk) = response
|
|
238
336
|
.chunk()
|
|
239
337
|
.await
|
|
240
|
-
.map_err(|e| format!("
|
|
338
|
+
.map_err(|e| format!("download body: {e}"))?
|
|
241
339
|
{
|
|
242
|
-
|
|
243
|
-
hasher.update(&chunk);
|
|
244
|
-
}
|
|
340
|
+
hasher.update(&chunk);
|
|
245
341
|
total += chunk.len() as u64;
|
|
246
342
|
DOWNLOADED_BYTES.store(total, Ordering::Relaxed);
|
|
247
343
|
|
|
248
344
|
if let Err(e) = file.write_all(&chunk) {
|
|
249
345
|
let _ = std::fs::remove_file(&tmp);
|
|
250
|
-
|
|
251
|
-
eprintln!("[whisper] {msg}");
|
|
252
|
-
return Err(msg);
|
|
346
|
+
return Err(format!("write tmp: {e}"));
|
|
253
347
|
}
|
|
254
348
|
|
|
255
|
-
// Emit progress + log every ~16 MB.
|
|
256
349
|
if total - last_progress >= 16 * 1024 * 1024 {
|
|
257
350
|
last_progress = total;
|
|
258
|
-
let
|
|
259
|
-
eprintln!("[whisper]
|
|
351
|
+
let mb = downloaded_mb(total, model);
|
|
352
|
+
eprintln!("[whisper] {mb} / {} MB", model.size_mb);
|
|
260
353
|
let _ = app.emit(
|
|
261
354
|
"whisper:model-progress",
|
|
262
|
-
serde_json::json!({ "downloadedMb":
|
|
355
|
+
serde_json::json!({ "downloadedMb": mb, "totalMb": model.size_mb }),
|
|
263
356
|
);
|
|
264
357
|
}
|
|
265
358
|
}
|
|
266
|
-
file.flush().map_err(|e| format!("flush
|
|
359
|
+
file.flush().map_err(|e| format!("flush tmp: {e}"))?;
|
|
267
360
|
drop(file);
|
|
268
361
|
|
|
269
|
-
if
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
eprintln!("[whisper] model checksum verified (sha256 {MODEL_SHA256})");
|
|
362
|
+
if total != model.size_bytes {
|
|
363
|
+
let _ = std::fs::remove_file(&tmp);
|
|
364
|
+
return Err(format!(
|
|
365
|
+
"size mismatch: got {total} bytes, expected {}",
|
|
366
|
+
model.size_bytes
|
|
367
|
+
));
|
|
368
|
+
}
|
|
369
|
+
let digest: String = hasher
|
|
370
|
+
.finalize()
|
|
371
|
+
.iter()
|
|
372
|
+
.map(|b| format!("{b:02x}"))
|
|
373
|
+
.collect();
|
|
374
|
+
if digest != model.sha256 {
|
|
375
|
+
let _ = std::fs::remove_file(&tmp);
|
|
376
|
+
return Err(format!(
|
|
377
|
+
"checksum mismatch: got {digest}, expected {}",
|
|
378
|
+
model.sha256
|
|
379
|
+
));
|
|
288
380
|
}
|
|
289
381
|
|
|
290
382
|
std::fs::rename(&tmp, path).map_err(|e| format!("rename model: {e}"))?;
|
|
291
|
-
eprintln!("[whisper]
|
|
292
|
-
Ok(path.
|
|
383
|
+
eprintln!("[whisper] saved → {}", path.display());
|
|
384
|
+
Ok(path.to_path_buf())
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
#[cfg(test)]
|
|
388
|
+
mod tests {
|
|
389
|
+
use super::*;
|
|
390
|
+
|
|
391
|
+
#[test]
|
|
392
|
+
fn supported_models_have_unique_ids_and_filenames() {
|
|
393
|
+
for (i, model) in SUPPORTED_MODELS.iter().enumerate() {
|
|
394
|
+
assert!(model.size_bytes > 0);
|
|
395
|
+
assert_eq!(model.sha256.len(), 64, "bad sha256 for {}", model.id);
|
|
396
|
+
assert!(!model.title.is_empty());
|
|
397
|
+
assert!(!model.description.is_empty());
|
|
398
|
+
for other in SUPPORTED_MODELS.iter().skip(i + 1) {
|
|
399
|
+
assert_ne!(model.id, other.id);
|
|
400
|
+
assert_ne!(model.filename, other.filename);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
#[test]
|
|
406
|
+
fn base_model_is_default() {
|
|
407
|
+
assert!(is_supported_model_id("base"));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
#[test]
|
|
411
|
+
fn unknown_model_id_is_rejected() {
|
|
412
|
+
assert!(find_model("does-not-exist").is_err());
|
|
413
|
+
}
|
|
293
414
|
}
|
|
@@ -134,6 +134,7 @@ pub async fn whisper_transcription_reset_timeline() -> Result<(), String> {
|
|
|
134
134
|
|
|
135
135
|
#[cfg(target_os = "macos")]
|
|
136
136
|
mod macos {
|
|
137
|
+
use std::path::PathBuf;
|
|
137
138
|
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
|
138
139
|
use std::sync::{Arc, Mutex, OnceLock};
|
|
139
140
|
use std::time::{Duration, Instant};
|
|
@@ -189,13 +190,20 @@ mod macos {
|
|
|
189
190
|
// the first call takes effect.
|
|
190
191
|
whisper_rs::install_logging_hooks();
|
|
191
192
|
|
|
192
|
-
|
|
193
|
+
struct CachedContext {
|
|
194
|
+
model_path: PathBuf,
|
|
195
|
+
context: Arc<WhisperContext>,
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
static CTX: OnceLock<Mutex<Option<CachedContext>>> = OnceLock::new();
|
|
193
199
|
let slot = CTX.get_or_init(|| Mutex::new(None));
|
|
194
200
|
let mut guard = slot.lock().map_err(|e| e.to_string())?;
|
|
195
|
-
if let Some(ctx) = guard.as_ref() {
|
|
196
|
-
return Ok(ctx.clone());
|
|
197
|
-
}
|
|
198
201
|
let path = model_file(app)?;
|
|
202
|
+
if let Some(cached) = guard.as_ref() {
|
|
203
|
+
if cached.model_path == path {
|
|
204
|
+
return Ok(cached.context.clone());
|
|
205
|
+
}
|
|
206
|
+
}
|
|
199
207
|
let path_str = path
|
|
200
208
|
.to_str()
|
|
201
209
|
.ok_or_else(|| "model path is not valid UTF-8".to_string())?;
|
|
@@ -208,9 +216,12 @@ mod macos {
|
|
|
208
216
|
params.use_gpu(cfg!(target_arch = "aarch64"));
|
|
209
217
|
let ctx = WhisperContext::new_with_params(path_str, params)
|
|
210
218
|
.map_err(|e| format!("whisper model load failed: {e}"))?;
|
|
211
|
-
let
|
|
212
|
-
*guard = Some(
|
|
213
|
-
|
|
219
|
+
let context = Arc::new(ctx);
|
|
220
|
+
*guard = Some(CachedContext {
|
|
221
|
+
model_path: path,
|
|
222
|
+
context: context.clone(),
|
|
223
|
+
});
|
|
224
|
+
Ok(context)
|
|
214
225
|
}
|
|
215
226
|
|
|
216
227
|
pub fn prewarm(app: &AppHandle) -> Result<(), String> {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
## Recording defaults
|
|
8
8
|
|
|
9
|
-
- New recordings default to **
|
|
9
|
+
- New recordings default to **public** visibility so anyone with the link can watch them. Organization admins can choose a different default.
|
|
10
10
|
- Default playback speed for viewers is **1.2x** (creator can override per video).
|
|
11
11
|
- Animated GIF thumbnails are enabled by default on new recordings — the first few seconds play as a hover preview.
|
|
12
12
|
|
|
@@ -839,6 +839,17 @@ const migrations = runMigrations(
|
|
|
839
839
|
name: "clips-meetings-share-transcript",
|
|
840
840
|
sql: `ALTER TABLE clips_meetings ADD COLUMN IF NOT EXISTS share_transcript INTEGER NOT NULL DEFAULT 0`,
|
|
841
841
|
},
|
|
842
|
+
{
|
|
843
|
+
version: 50,
|
|
844
|
+
name: "clips-public-organization-default",
|
|
845
|
+
// Earlier releases persisted the old private default into org rows.
|
|
846
|
+
// Normalize that state once; the org setting remains an explicit override.
|
|
847
|
+
// guard:allow-unscoped — startup migration normalizes legacy defaults across organizations.
|
|
848
|
+
sql: [
|
|
849
|
+
`UPDATE workspaces SET default_visibility = 'public' WHERE default_visibility = 'private' AND updated_at = created_at`,
|
|
850
|
+
`UPDATE organization_settings SET default_visibility = 'public' WHERE default_visibility = 'private' AND updated_at = created_at`,
|
|
851
|
+
].join("; "),
|
|
852
|
+
},
|
|
842
853
|
],
|
|
843
854
|
{ table: "clips_migrations" },
|
|
844
855
|
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const CLIPS_BUILDER_CREDITS_STATE_KEY = "clips-builder-credits";
|
|
2
2
|
|
|
3
3
|
export const BUILDER_CREDITS_UPGRADE_URL =
|
|
4
|
-
"https://builder.io/account/subscription?signupSource=agent-native&agentNativeConnectSource=clips_builder_credits&agentNativeFlow=connect_llm&framework=agent-native";
|
|
4
|
+
"https://builder.io/account/subscription?signupSource=agent-native&agentNativeConnectSource=clips_builder_credits&agentNativeFlow=connect_llm&framework=agent-native&utm_source=agent-native&utm_medium=product&utm_campaign=onboarding&utm_content=clips_builder_credits";
|
|
5
5
|
|
|
6
6
|
export const BUILDER_CREDIT_FEATURES = [
|
|
7
7
|
"backup-transcription",
|