@pheem49/mint 1.8.2 → 1.10.0
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/Cargo.lock +55 -3
- package/Cargo.toml +2 -1
- package/Release_Note.md +297 -107
- package/bin/mint +0 -0
- package/crates/mint-cli/Cargo.toml +1 -0
- package/crates/mint-cli/src/actions.rs +233 -0
- package/crates/mint-cli/src/agent.rs +382 -127
- package/crates/mint-cli/src/gmail.rs +22 -2
- package/crates/mint-cli/src/image.rs +8 -8
- package/crates/mint-cli/src/interactive.rs +2888 -0
- package/crates/mint-cli/src/main.rs +391 -2506
- package/crates/mint-cli/src/markdown.rs +294 -0
- package/crates/mint-cli/src/onboard.rs +152 -109
- package/crates/mint-cli/src/setup.rs +143 -143
- package/crates/mint-cli/src/updater.rs +48 -6
- package/crates/mint-core/Cargo.toml +1 -0
- package/crates/mint-core/src/agent_loop.rs +1 -0
- package/crates/mint-core/src/api_server.rs +431 -65
- package/crates/mint-core/src/browser.rs +630 -102
- package/crates/mint-core/src/channels.rs +105 -6
- package/crates/mint-core/src/chat.rs +174 -11
- package/crates/mint-core/src/code_tools.rs +47 -31
- package/crates/mint-core/src/config.rs +302 -17
- package/crates/mint-core/src/image_gen.rs +299 -38
- package/crates/mint-core/src/lib.rs +10 -7
- package/crates/mint-core/src/mcp.rs +119 -6
- package/crates/mint-core/src/memory.rs +6 -1
- package/crates/mint-core/src/orchestration.rs +237 -96
- package/crates/mint-core/src/pictures.rs +67 -5
- package/crates/mint-core/src/semantic.rs +1 -1
- package/crates/mint-core/src/shell.rs +1 -1
- package/crates/mint-core/src/skills.rs +128 -25
- package/crates/mint-core/src/video_gen.rs +235 -0
- package/package.json +8 -2
- package/shared-modules/linux-audio/fluidsynth2-static.json +9 -3
- package/shared-modules/linux-audio/fluidsynth2.json +8 -2
- package/shared-modules/linux-audio/lilv.json +26 -19
- package/shared-modules/linux-audio/lv2.json +0 -1
- package/shared-modules/luajit/luajit.json +2 -2
- package/skills-lock.json +41 -0
- package/src/renderer/index-web.html +1 -1
- package/src/renderer/index.html +1 -1
- package/src/renderer/shared/components/AgentActivityTable.tsx +25 -6
- package/src/renderer/shared/constants/slashCommands.ts +27 -0
- package/src/renderer/shared/platform.ts +3 -0
- package/src/renderer/shared/types.ts +4 -0
- package/src/renderer/shared/utils/approval.tsx +86 -7
- package/src/renderer/shared/utils/fileIcons.ts +135 -0
- package/src/renderer/shared/utils/markdown.tsx +217 -88
- package/src/renderer/shared/utils/slashCommandProcessor.ts +239 -0
- package/src/renderer/src/components/ChatPanel.tsx +486 -69
- package/src/renderer/src/components/DashboardSidebar.tsx +11 -2
- package/src/renderer/src/components/ImageStudioPanel.tsx +87 -4
- package/src/renderer/src/components/MintDashboard.tsx +195 -7
- package/src/renderer/src/components/PicturesLibrary.tsx +156 -15
- package/src/renderer/src/components/Settings/AgentsTab.tsx +21 -2
- package/src/renderer/src/components/Settings/AutomationTab.tsx +316 -18
- package/src/renderer/src/components/Settings/GeneralTab.tsx +795 -413
- package/src/renderer/src/components/Settings/PluginsTab.tsx +304 -70
- package/src/renderer/src/components/Settings/ThemeTab.tsx +6 -1
- package/src/renderer/src/components/SettingsWindow.tsx +106 -3
- package/src/renderer/src/components/VeoStudioPanel.tsx +549 -0
- package/src/renderer/src/components/WorkspacePanel.tsx +7 -125
- package/src/renderer/src/css/image-studio.css +106 -0
- package/src/renderer/src/css/settings.css +158 -0
- package/src/renderer/src/css/styles.css +368 -1
- package/src/renderer/src/css/veo-studio.css +626 -0
- package/src/renderer/src/index.css +1 -0
- package/src/renderer/src/tauri.ts +144 -17
- package/src/renderer/src-web/components/ChatPanel.tsx +385 -93
- package/src/renderer/src-web/components/DashboardSidebar.tsx +16 -1
- package/src/renderer/src-web/components/ImageStudioPanel.tsx +87 -4
- package/src/renderer/src-web/components/MintDashboard.tsx +156 -5
- package/src/renderer/src-web/components/PicturesLibrary.tsx +155 -15
- package/src/renderer/src-web/components/Settings/AgentsTab.tsx +21 -2
- package/src/renderer/src-web/components/Settings/AutomationTab.tsx +316 -18
- package/src/renderer/src-web/components/Settings/GeneralTab.tsx +795 -411
- package/src/renderer/src-web/components/Settings/PluginsTab.tsx +304 -70
- package/src/renderer/src-web/components/Settings/ThemeTab.tsx +6 -1
- package/src/renderer/src-web/components/SettingsWindow.tsx +110 -3
- package/src/renderer/src-web/components/VeoStudioPanel.tsx +549 -0
- package/src/renderer/src-web/css/image-studio.css +106 -0
- package/src/renderer/src-web/css/settings.css +158 -0
- package/src/renderer/src-web/css/styles.css +369 -1
- package/src/renderer/src-web/css/veo-studio.css +650 -0
- package/src/renderer/src-web/index.css +1 -0
- package/src/renderer/src-web/tauri.ts +135 -16
- package/src-tauri/src/desktop.rs +18 -16
- package/src-tauri/src/discord_rpc.rs +1 -1
- package/src-tauri/src/lib.rs +155 -25
- package/src-tauri/src/webhooks.rs +45 -31
- package/src-tauri/src/workflows.rs +1 -0
- package/src-tauri/tauri.conf.json +1 -1
- package/temp_gridgeist/AGENTS.md +8 -0
- package/temp_gridgeist/CHANGELOG.md +130 -0
- package/temp_gridgeist/LICENSE +21 -0
- package/temp_gridgeist/README.md +323 -0
- package/temp_gridgeist/README.th.md +325 -0
- package/temp_gridgeist/skills/gridgeist/SKILL.md +88 -0
- package/temp_gridgeist/skills/gridgeist/agents/openai.yaml +9 -0
- package/temp_gridgeist/skills/gridgeist/assets/gridgeist-small.svg +10 -0
- package/temp_gridgeist/skills/gridgeist/assets/gridgeist.png +0 -0
- package/temp_gridgeist/skills/gridgeist/assets/gridgeist.svg +18 -0
- package/temp_gridgeist/skills/gridgeist/references/design-language.md +88 -0
- package/temp_gridgeist/skills/gridgeist/references/review-checklist.md +128 -0
- package/temp_gridgeist/skills/gridgeist/references/system-contract.md +109 -0
|
@@ -2,6 +2,7 @@ use std::{
|
|
|
2
2
|
fs,
|
|
3
3
|
io::Cursor,
|
|
4
4
|
path::{Path, PathBuf},
|
|
5
|
+
process::Command,
|
|
5
6
|
time::{SystemTime, UNIX_EPOCH},
|
|
6
7
|
};
|
|
7
8
|
|
|
@@ -79,11 +80,10 @@ pub fn list_saved_pictures() -> Result<Vec<PictureEntry>, PictureError> {
|
|
|
79
80
|
.thumbnail_path
|
|
80
81
|
.as_ref()
|
|
81
82
|
.is_none_or(|thumbnail_path| !thumbnail_path.exists())
|
|
83
|
+
&& let Some(thumbnail_path) = create_thumbnail_from_file(&directory, &entry)
|
|
82
84
|
{
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
changed = true;
|
|
86
|
-
}
|
|
85
|
+
entry.thumbnail_path = Some(thumbnail_path);
|
|
86
|
+
changed = true;
|
|
87
87
|
}
|
|
88
88
|
entry.url = Some(file_url(&entry.path));
|
|
89
89
|
entry.thumbnail_url = entry.thumbnail_path.as_deref().map(file_url);
|
|
@@ -132,6 +132,11 @@ pub fn parse_data_uri(raw: &str) -> Option<(String, &'static str, Vec<u8>)> {
|
|
|
132
132
|
"image/jpeg" | "image/jpg" => "jpg",
|
|
133
133
|
"image/webp" => "webp",
|
|
134
134
|
"image/gif" => "gif",
|
|
135
|
+
"video/mp4" => "mp4",
|
|
136
|
+
"video/webm" => "webm",
|
|
137
|
+
"video/quicktime" => "mov",
|
|
138
|
+
"video/x-msvideo" => "avi",
|
|
139
|
+
"video/x-matroska" => "mkv",
|
|
135
140
|
_ => return None,
|
|
136
141
|
};
|
|
137
142
|
Some((mime_type, extension, STANDARD.decode(encoded).ok()?))
|
|
@@ -167,7 +172,13 @@ fn save_chat_images_to_directory(
|
|
|
167
172
|
let id = filename
|
|
168
173
|
.trim_end_matches(&format!(".{extension}"))
|
|
169
174
|
.to_owned();
|
|
170
|
-
let thumbnail_path =
|
|
175
|
+
let thumbnail_path = if mime_type.starts_with("image/") {
|
|
176
|
+
create_thumbnail_from_bytes(directory, &id, &bytes)
|
|
177
|
+
} else if mime_type.starts_with("video/") {
|
|
178
|
+
extract_video_thumbnail_from_bytes(directory, &id, &bytes)
|
|
179
|
+
} else {
|
|
180
|
+
None
|
|
181
|
+
};
|
|
171
182
|
let entry = PictureEntry {
|
|
172
183
|
id,
|
|
173
184
|
filename,
|
|
@@ -222,6 +233,57 @@ fn create_thumbnail_from_bytes(directory: &Path, id: &str, bytes: &[u8]) -> Opti
|
|
|
222
233
|
Some(path)
|
|
223
234
|
}
|
|
224
235
|
|
|
236
|
+
fn extract_video_thumbnail_from_bytes(directory: &Path, id: &str, bytes: &[u8]) -> Option<PathBuf> {
|
|
237
|
+
// Write video to temporary file
|
|
238
|
+
let temp_video = std::env::temp_dir().join(format!("mint-video-{}.tmp", id));
|
|
239
|
+
if fs::write(&temp_video, bytes).is_err() {
|
|
240
|
+
return None;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let temp_frame = std::env::temp_dir().join(format!("mint-frame-{}.png", id));
|
|
244
|
+
|
|
245
|
+
let res = (|| -> Option<PathBuf> {
|
|
246
|
+
let temp_video_str = temp_video.to_str()?;
|
|
247
|
+
let temp_frame_str = temp_frame.to_str()?;
|
|
248
|
+
|
|
249
|
+
let output = Command::new("ffmpeg")
|
|
250
|
+
.args(&["-i", temp_video_str])
|
|
251
|
+
.args(&["-vf", "select=eq(n\\,0)"])
|
|
252
|
+
.args(&["-vframes", "1"])
|
|
253
|
+
.args(&["-y", temp_frame_str])
|
|
254
|
+
.stderr(std::process::Stdio::null())
|
|
255
|
+
.stdout(std::process::Stdio::null())
|
|
256
|
+
.output()
|
|
257
|
+
.ok()?;
|
|
258
|
+
|
|
259
|
+
if !output.status.success() {
|
|
260
|
+
return None;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Load extracted frame and create thumbnail
|
|
264
|
+
let frame_bytes = fs::read(&temp_frame).ok()?;
|
|
265
|
+
let image = image::load_from_memory(&frame_bytes).ok()?;
|
|
266
|
+
let thumbnail = image.thumbnail(THUMBNAIL_MAX_SIZE, THUMBNAIL_MAX_SIZE);
|
|
267
|
+
|
|
268
|
+
let thumbnail_directory = thumbnails_directory(directory);
|
|
269
|
+
ensure_directory(&thumbnail_directory).ok()?;
|
|
270
|
+
let path = thumbnail_directory.join(format!("{id}.thumb.png"));
|
|
271
|
+
|
|
272
|
+
let mut encoded = Cursor::new(Vec::new());
|
|
273
|
+
thumbnail
|
|
274
|
+
.write_to(&mut encoded, image::ImageFormat::Png)
|
|
275
|
+
.ok()?;
|
|
276
|
+
fs::write(&path, encoded.into_inner()).ok()?;
|
|
277
|
+
Some(path)
|
|
278
|
+
})();
|
|
279
|
+
|
|
280
|
+
// Always clean up temp files to avoid leaking in /tmp
|
|
281
|
+
let _ = fs::remove_file(&temp_video);
|
|
282
|
+
let _ = fs::remove_file(&temp_frame);
|
|
283
|
+
|
|
284
|
+
res
|
|
285
|
+
}
|
|
286
|
+
|
|
225
287
|
fn read_index(directory: &Path) -> Result<Vec<PictureEntry>, PictureError> {
|
|
226
288
|
let path = directory.join("pictures.json");
|
|
227
289
|
if !path.exists() {
|
|
@@ -169,7 +169,7 @@ fn chunk_text(content: &str) -> Vec<(usize, usize, String)> {
|
|
|
169
169
|
while start < lines.len() {
|
|
170
170
|
let mut end = start;
|
|
171
171
|
let mut chars = 0;
|
|
172
|
-
while end < lines.len() && (end == start || chars + lines[end].len()
|
|
172
|
+
while end < lines.len() && (end == start || chars + lines[end].len() < MAX_CHARS) {
|
|
173
173
|
chars += lines[end].len() + 1;
|
|
174
174
|
end += 1;
|
|
175
175
|
}
|
|
@@ -53,10 +53,38 @@ pub fn learn_skill(path: &Path) -> Result<LearnedSkill, SkillError> {
|
|
|
53
53
|
Ok(MemoryStore::open_default()?.add_learned_skill(name, &path.to_string_lossy(), &content)?)
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
pub fn
|
|
56
|
+
pub fn load_agent_rules_file(file_path: &Path, list: &mut Vec<LearnedSkill>) {
|
|
57
|
+
if file_path.is_file() {
|
|
58
|
+
if let Ok(content) = fs::read_to_string(file_path) {
|
|
59
|
+
let name = file_path
|
|
60
|
+
.file_name()
|
|
61
|
+
.and_then(|n| n.to_str())
|
|
62
|
+
.unwrap_or("AGENTS.md")
|
|
63
|
+
.to_string();
|
|
64
|
+
let description = parse_skill_description(&content)
|
|
65
|
+
.or_else(|| Some("Workspace instructions and rules".to_string()));
|
|
66
|
+
list.push(LearnedSkill {
|
|
67
|
+
id: 0,
|
|
68
|
+
name,
|
|
69
|
+
source_path: file_path.to_string_lossy().to_string(),
|
|
70
|
+
content,
|
|
71
|
+
created_at: String::new(),
|
|
72
|
+
description,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
pub fn learned_skills_context(
|
|
79
|
+
workspace_root: Option<&Path>,
|
|
80
|
+
chat_id: Option<&str>,
|
|
81
|
+
) -> Result<String, SkillError> {
|
|
57
82
|
let mut skills = MemoryStore::open_default()?.learned_skills(20)?;
|
|
58
83
|
|
|
59
84
|
if let Some(home) = dirs::home_dir() {
|
|
85
|
+
let global_agents_path = home.join(".gemini").join("config").join("AGENTS.md");
|
|
86
|
+
load_agent_rules_file(&global_agents_path, &mut skills);
|
|
87
|
+
|
|
60
88
|
let global_skills_path = home.join(".config").join("mint").join("mint-skills");
|
|
61
89
|
if !global_skills_path.exists() {
|
|
62
90
|
let _ = std::fs::create_dir_all(&global_skills_path);
|
|
@@ -65,6 +93,12 @@ pub fn learned_skills_context(workspace_root: Option<&Path>) -> Result<String, S
|
|
|
65
93
|
}
|
|
66
94
|
|
|
67
95
|
if let Some(root) = workspace_root {
|
|
96
|
+
let workspace_agents_path1 = root.join(".agents").join("AGENTS.md");
|
|
97
|
+
load_agent_rules_file(&workspace_agents_path1, &mut skills);
|
|
98
|
+
|
|
99
|
+
let workspace_agents_path2 = root.join("AGENTS.md");
|
|
100
|
+
load_agent_rules_file(&workspace_agents_path2, &mut skills);
|
|
101
|
+
|
|
68
102
|
let workspace_skills_path1 = root.join(".agents").join("skills");
|
|
69
103
|
load_skills_from_dir(&workspace_skills_path1, &mut skills);
|
|
70
104
|
|
|
@@ -77,11 +111,53 @@ pub fn learned_skills_context(workspace_root: Option<&Path>) -> Result<String, S
|
|
|
77
111
|
unique_skills.insert(skill.name.clone(), skill);
|
|
78
112
|
}
|
|
79
113
|
|
|
80
|
-
let
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
114
|
+
let mut has_history = false;
|
|
115
|
+
if let Some(cid) = chat_id {
|
|
116
|
+
if let Ok(memory) = MemoryStore::open_default() {
|
|
117
|
+
if let Ok(interactions) = memory.recent_interactions_for_chat(cid, 1) {
|
|
118
|
+
has_history = !interactions.is_empty();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let mut formatted_skills = Vec::new();
|
|
124
|
+
for skill in unique_skills.into_values() {
|
|
125
|
+
let mut is_workspace_skill = false;
|
|
126
|
+
let mut rel_path = String::new();
|
|
127
|
+
if let Some(root) = workspace_root {
|
|
128
|
+
let skill_path = Path::new(&skill.source_path);
|
|
129
|
+
if let Ok(canonical_skill) = skill_path.canonicalize() {
|
|
130
|
+
if let Ok(canonical_root) = root.canonicalize() {
|
|
131
|
+
if canonical_skill.starts_with(&canonical_root) {
|
|
132
|
+
if let Ok(relative) = canonical_skill.strip_prefix(&canonical_root) {
|
|
133
|
+
is_workspace_skill = true;
|
|
134
|
+
rel_path = relative.to_string_lossy().to_string();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if is_workspace_skill {
|
|
142
|
+
let desc = skill
|
|
143
|
+
.description
|
|
144
|
+
.as_deref()
|
|
145
|
+
.unwrap_or("No description provided");
|
|
146
|
+
let status_msg = if has_history {
|
|
147
|
+
"READ. You have already read this skill in a previous turn of this conversation. Do NOT call `read_file` to read it again unless you need to re-verify its contents."
|
|
148
|
+
} else {
|
|
149
|
+
"UNREAD. You MUST execute the `read_file` tool with the path above to read this skill before starting the task."
|
|
150
|
+
};
|
|
151
|
+
formatted_skills.push(format!(
|
|
152
|
+
"Skill: {}\nDescription: {}\nPath: {}\nStatus: {}",
|
|
153
|
+
skill.name, desc, rel_path, status_msg
|
|
154
|
+
));
|
|
155
|
+
} else {
|
|
156
|
+
formatted_skills.push(format!("Skill: {}\n{}", skill.name, skill.content));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let value = formatted_skills.join("\n\n");
|
|
85
161
|
if value.len() <= MAX_CONTEXT_BYTES {
|
|
86
162
|
return Ok(value);
|
|
87
163
|
}
|
|
@@ -92,6 +168,31 @@ pub fn learned_skills_context(workspace_root: Option<&Path>) -> Result<String, S
|
|
|
92
168
|
Ok(format!("{}\n...<learned skills truncated>", &value[..end]))
|
|
93
169
|
}
|
|
94
170
|
|
|
171
|
+
pub fn parse_skill_description(content: &str) -> Option<String> {
|
|
172
|
+
let content = content.trim_start();
|
|
173
|
+
if !content.starts_with("---") {
|
|
174
|
+
return None;
|
|
175
|
+
}
|
|
176
|
+
let rest = &content[3..];
|
|
177
|
+
let end_idx = rest.find("---")?;
|
|
178
|
+
let frontmatter = &rest[..end_idx];
|
|
179
|
+
for line in frontmatter.lines() {
|
|
180
|
+
let line = line.trim();
|
|
181
|
+
if let Some(desc) = line.strip_prefix("description:") {
|
|
182
|
+
let mut val = desc.trim();
|
|
183
|
+
if (val.starts_with('"') && val.ends_with('"'))
|
|
184
|
+
|| (val.starts_with('\'') && val.ends_with('\''))
|
|
185
|
+
{
|
|
186
|
+
if val.len() >= 2 {
|
|
187
|
+
val = &val[1..val.len() - 1];
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return Some(val.to_string());
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
None
|
|
194
|
+
}
|
|
195
|
+
|
|
95
196
|
pub fn load_skills_from_dir(dir: &Path, list: &mut Vec<LearnedSkill>) {
|
|
96
197
|
if !dir.is_dir() {
|
|
97
198
|
return;
|
|
@@ -109,36 +210,38 @@ pub fn load_skills_from_dir(dir: &Path, list: &mut Vec<LearnedSkill>) {
|
|
|
109
210
|
.and_then(|n| n.to_str())
|
|
110
211
|
.unwrap_or("skill")
|
|
111
212
|
.to_string();
|
|
213
|
+
let description = parse_skill_description(&content);
|
|
112
214
|
list.push(LearnedSkill {
|
|
113
215
|
id: 0,
|
|
114
216
|
name,
|
|
115
217
|
source_path: skill_file.to_string_lossy().to_string(),
|
|
116
218
|
content,
|
|
117
219
|
created_at: String::new(),
|
|
220
|
+
description,
|
|
118
221
|
});
|
|
119
222
|
}
|
|
120
223
|
break;
|
|
121
224
|
}
|
|
122
225
|
}
|
|
123
|
-
} else if path.is_file()
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
226
|
+
} else if path.is_file()
|
|
227
|
+
&& let Some(ext) = path.extension().and_then(|e| e.to_str())
|
|
228
|
+
&& matches!(ext.to_ascii_lowercase().as_str(), "md" | "txt")
|
|
229
|
+
&& let Ok(content) = fs::read_to_string(&path)
|
|
230
|
+
{
|
|
231
|
+
let name = path
|
|
232
|
+
.file_stem()
|
|
233
|
+
.and_then(|n| n.to_str())
|
|
234
|
+
.unwrap_or("skill")
|
|
235
|
+
.to_string();
|
|
236
|
+
let description = parse_skill_description(&content);
|
|
237
|
+
list.push(LearnedSkill {
|
|
238
|
+
id: 0,
|
|
239
|
+
name,
|
|
240
|
+
source_path: path.to_string_lossy().to_string(),
|
|
241
|
+
content,
|
|
242
|
+
created_at: String::new(),
|
|
243
|
+
description,
|
|
244
|
+
});
|
|
142
245
|
}
|
|
143
246
|
}
|
|
144
247
|
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
use base64::{Engine as _, engine::general_purpose::STANDARD};
|
|
2
|
+
use serde::{Deserialize, Serialize};
|
|
3
|
+
use serde_json::{Value, json};
|
|
4
|
+
use thiserror::Error;
|
|
5
|
+
|
|
6
|
+
use crate::{MintConfig, PictureEntry};
|
|
7
|
+
|
|
8
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
9
|
+
#[serde(rename_all = "camelCase")]
|
|
10
|
+
pub struct VideoGenRequest {
|
|
11
|
+
pub prompt: String,
|
|
12
|
+
pub negative_prompt: Option<String>,
|
|
13
|
+
pub aspect_ratio: String,
|
|
14
|
+
pub duration: u32,
|
|
15
|
+
pub model: Option<String>,
|
|
16
|
+
pub provider: String,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#[derive(Debug, Clone, Serialize)]
|
|
20
|
+
#[serde(rename_all = "camelCase")]
|
|
21
|
+
pub struct VideoGenResponse {
|
|
22
|
+
pub videos: Vec<PictureEntry>,
|
|
23
|
+
pub model: String,
|
|
24
|
+
pub provider: String,
|
|
25
|
+
pub prompt: String,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#[derive(Debug, Error)]
|
|
29
|
+
pub enum VideoGenError {
|
|
30
|
+
#[error("missing Gemini API key — set GEMINI_API_KEY or configure api_key in config")]
|
|
31
|
+
MissingApiKey,
|
|
32
|
+
#[error("API request failed: {0}")]
|
|
33
|
+
Request(#[from] reqwest::Error),
|
|
34
|
+
#[error("unexpected response structure: {0}")]
|
|
35
|
+
UnexpectedResponse(String),
|
|
36
|
+
#[error("video generation timed out")]
|
|
37
|
+
Timeout,
|
|
38
|
+
#[error("failed to save generated video: {0}")]
|
|
39
|
+
SaveError(String),
|
|
40
|
+
#[error(
|
|
41
|
+
"Google Veo model endpoint returned 404 Not Found. Google Veo video generation requires an API Key or Google Cloud project with Veo Model access enabled: {0}"
|
|
42
|
+
)]
|
|
43
|
+
ModelNotFound(String),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
pub async fn generate_video(
|
|
47
|
+
config: &MintConfig,
|
|
48
|
+
request: &VideoGenRequest,
|
|
49
|
+
) -> Result<VideoGenResponse, VideoGenError> {
|
|
50
|
+
let api_key = if config.api_key.trim().is_empty() {
|
|
51
|
+
std::env::var("GEMINI_API_KEY").unwrap_or_default()
|
|
52
|
+
} else {
|
|
53
|
+
config.api_key.clone()
|
|
54
|
+
};
|
|
55
|
+
if api_key.trim().is_empty() {
|
|
56
|
+
return Err(VideoGenError::MissingApiKey);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let mut model_owned = request
|
|
60
|
+
.model
|
|
61
|
+
.as_deref()
|
|
62
|
+
.map(str::trim)
|
|
63
|
+
.filter(|m| !m.is_empty())
|
|
64
|
+
.map(str::to_owned)
|
|
65
|
+
.unwrap_or_else(|| {
|
|
66
|
+
config
|
|
67
|
+
.extra
|
|
68
|
+
.get("veoModel")
|
|
69
|
+
.and_then(|v| v.as_str())
|
|
70
|
+
.unwrap_or("veo-2.0-generate-001")
|
|
71
|
+
.to_string()
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if model_owned == "veo-2.0-flash-exp" {
|
|
75
|
+
model_owned = "veo-2.0-generate-001".to_string();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let client = crate::HTTP_CLIENT.clone();
|
|
79
|
+
let mut url = format!(
|
|
80
|
+
"https://generativelanguage.googleapis.com/v1beta/models/{model_owned}:predictLongRunning"
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
let payload = json!({
|
|
84
|
+
"instances": [
|
|
85
|
+
{
|
|
86
|
+
"prompt": request.prompt
|
|
87
|
+
}
|
|
88
|
+
],
|
|
89
|
+
"parameters": {
|
|
90
|
+
"aspectRatio": request.aspect_ratio,
|
|
91
|
+
"durationSeconds": request.duration,
|
|
92
|
+
"sampleCount": 1
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
let mut response_res = client
|
|
97
|
+
.post(&url)
|
|
98
|
+
.header("x-goog-api-key", &api_key)
|
|
99
|
+
.json(&payload)
|
|
100
|
+
.send()
|
|
101
|
+
.await?;
|
|
102
|
+
|
|
103
|
+
if response_res.status() == reqwest::StatusCode::NOT_FOUND && model_owned != "veo-2.0-flash-001"
|
|
104
|
+
{
|
|
105
|
+
model_owned = "veo-2.0-flash-001".to_string();
|
|
106
|
+
url = format!(
|
|
107
|
+
"https://generativelanguage.googleapis.com/v1beta/models/{model_owned}:predictLongRunning"
|
|
108
|
+
);
|
|
109
|
+
response_res = client
|
|
110
|
+
.post(&url)
|
|
111
|
+
.header("x-goog-api-key", &api_key)
|
|
112
|
+
.json(&payload)
|
|
113
|
+
.send()
|
|
114
|
+
.await?;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if !response_res.status().is_success() {
|
|
118
|
+
let status = response_res.status();
|
|
119
|
+
let err_text = response_res.text().await.unwrap_or_default();
|
|
120
|
+
if status == reqwest::StatusCode::NOT_FOUND {
|
|
121
|
+
return Err(VideoGenError::ModelNotFound(format!(
|
|
122
|
+
"404 for model {model_owned} on generativelanguage.googleapis.com API. ({err_text})"
|
|
123
|
+
)));
|
|
124
|
+
}
|
|
125
|
+
return Err(VideoGenError::UnexpectedResponse(format!(
|
|
126
|
+
"HTTP {status}: {err_text}"
|
|
127
|
+
)));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let res: Value = response_res.json().await?;
|
|
131
|
+
|
|
132
|
+
let operation_name = res
|
|
133
|
+
.get("name")
|
|
134
|
+
.and_then(|v| v.as_str())
|
|
135
|
+
.ok_or_else(|| VideoGenError::UnexpectedResponse("Missing operation name".to_string()))?;
|
|
136
|
+
|
|
137
|
+
let poll_url = format!("https://generativelanguage.googleapis.com/v1beta/{operation_name}");
|
|
138
|
+
let mut done = false;
|
|
139
|
+
let mut final_response = None;
|
|
140
|
+
|
|
141
|
+
// Poll every 5 seconds, up to 60 times (5 minutes max)
|
|
142
|
+
for _ in 0..60 {
|
|
143
|
+
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
|
|
144
|
+
let poll_res: Value = client
|
|
145
|
+
.get(&poll_url)
|
|
146
|
+
.header("x-goog-api-key", &api_key)
|
|
147
|
+
.send()
|
|
148
|
+
.await?
|
|
149
|
+
.error_for_status()?
|
|
150
|
+
.json()
|
|
151
|
+
.await?;
|
|
152
|
+
|
|
153
|
+
if let Some(d) = poll_res.get("done").and_then(|v| v.as_bool()) {
|
|
154
|
+
if d {
|
|
155
|
+
done = true;
|
|
156
|
+
final_response = Some(poll_res);
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if !done {
|
|
163
|
+
return Err(VideoGenError::Timeout);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let op_res = final_response.ok_or(VideoGenError::Timeout)?;
|
|
167
|
+
|
|
168
|
+
// Check if error occurred in operation
|
|
169
|
+
if let Some(err) = op_res.get("error") {
|
|
170
|
+
let msg = err["message"].as_str().unwrap_or("unknown operation error");
|
|
171
|
+
return Err(VideoGenError::UnexpectedResponse(msg.to_string()));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let response_obj = op_res
|
|
175
|
+
.get("response")
|
|
176
|
+
.ok_or_else(|| VideoGenError::UnexpectedResponse("Missing response object".to_string()))?;
|
|
177
|
+
|
|
178
|
+
// Try to find generated samples or outputs
|
|
179
|
+
let generated_samples = response_obj
|
|
180
|
+
.get("generateVideoResponse")
|
|
181
|
+
.and_then(|gvr| gvr.get("generatedSamples"))
|
|
182
|
+
.or_else(|| {
|
|
183
|
+
response_obj
|
|
184
|
+
.get("outputs")
|
|
185
|
+
.and_then(|o| o.as_array()?.first()?.get("generatedVideos"))
|
|
186
|
+
})
|
|
187
|
+
.ok_or_else(|| {
|
|
188
|
+
VideoGenError::UnexpectedResponse(
|
|
189
|
+
"No generated samples or videos in response".to_string(),
|
|
190
|
+
)
|
|
191
|
+
})?;
|
|
192
|
+
|
|
193
|
+
let sample = generated_samples
|
|
194
|
+
.as_array()
|
|
195
|
+
.and_then(|arr| arr.first())
|
|
196
|
+
.ok_or_else(|| {
|
|
197
|
+
VideoGenError::UnexpectedResponse("Generated samples array is empty".to_string())
|
|
198
|
+
})?;
|
|
199
|
+
|
|
200
|
+
let video_uri = sample
|
|
201
|
+
.get("video")
|
|
202
|
+
.and_then(|v| v.get("uri"))
|
|
203
|
+
.and_then(|u| u.as_str())
|
|
204
|
+
.ok_or_else(|| VideoGenError::UnexpectedResponse("Missing video URI".to_string()))?;
|
|
205
|
+
|
|
206
|
+
// Download the video file bytes
|
|
207
|
+
let video_bytes = client
|
|
208
|
+
.get(video_uri)
|
|
209
|
+
.header("x-goog-api-key", &api_key)
|
|
210
|
+
.send()
|
|
211
|
+
.await?
|
|
212
|
+
.error_for_status()?
|
|
213
|
+
.bytes()
|
|
214
|
+
.await?
|
|
215
|
+
.to_vec();
|
|
216
|
+
|
|
217
|
+
// Convert to data URI for saving
|
|
218
|
+
let data_b64 = STANDARD.encode(&video_bytes);
|
|
219
|
+
let data_uri = format!("data:video/mp4;base64,{data_b64}");
|
|
220
|
+
|
|
221
|
+
// Save to the pictures directory and record index entry
|
|
222
|
+
let saved = crate::pictures::save_chat_images(
|
|
223
|
+
vec![data_uri],
|
|
224
|
+
Some("veo".to_string()),
|
|
225
|
+
Some(request.prompt.clone()),
|
|
226
|
+
)
|
|
227
|
+
.map_err(|e| VideoGenError::SaveError(e.to_string()))?;
|
|
228
|
+
|
|
229
|
+
Ok(VideoGenResponse {
|
|
230
|
+
videos: saved,
|
|
231
|
+
model: model_owned,
|
|
232
|
+
provider: "veo".to_string(),
|
|
233
|
+
prompt: request.prompt.clone(),
|
|
234
|
+
})
|
|
235
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pheem49/mint",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "A native Tauri desktop AI assistant with a Rust backend and React UI.",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev:web": "vite --config vite.config.web.ts",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"mint": "src/bin/index.js"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
+
"@pouyanafisi/gmail-mcp": "^2.0.0",
|
|
59
60
|
"@tauri-apps/api": "^2.0.0",
|
|
60
61
|
"@vitejs/plugin-react": "^6.0.1",
|
|
61
62
|
"@xyflow/react": "^12.11.1",
|
|
@@ -71,6 +72,11 @@
|
|
|
71
72
|
"@tauri-apps/cli": "^2.0.0"
|
|
72
73
|
},
|
|
73
74
|
"overrides": {
|
|
74
|
-
"gh-pages": "^5.0.0"
|
|
75
|
+
"gh-pages": "^5.0.0",
|
|
76
|
+
"@modelcontextprotocol/sdk": "^1.24.0",
|
|
77
|
+
"nodemailer": "^9.0.0",
|
|
78
|
+
"uuid": "^11.1.1",
|
|
79
|
+
"brace-expansion": "^5.0.8",
|
|
80
|
+
"@hono/node-server": "^2.0.5"
|
|
75
81
|
}
|
|
76
82
|
}
|
|
@@ -22,9 +22,15 @@
|
|
|
22
22
|
],
|
|
23
23
|
"sources": [
|
|
24
24
|
{
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
"type": "archive",
|
|
26
|
+
"url": "https://github.com/FluidSynth/fluidsynth/archive/v2.5.6.tar.gz",
|
|
27
|
+
"sha256": "0825f024c9cf7a18073739b83612d46542ecbfb349ae9147a1e9f08e2d524407"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/kthohr/gcem.git",
|
|
32
|
+
"commit": "012ae73c6d0a2cb09ffe86475f5c6fba3926e200",
|
|
33
|
+
"dest": "gcem"
|
|
28
34
|
}
|
|
29
35
|
]
|
|
30
36
|
}
|
|
@@ -12,8 +12,14 @@
|
|
|
12
12
|
"sources": [
|
|
13
13
|
{
|
|
14
14
|
"type": "archive",
|
|
15
|
-
"url": "https://github.com/FluidSynth/fluidsynth/archive/
|
|
16
|
-
"sha256": "
|
|
15
|
+
"url": "https://github.com/FluidSynth/fluidsynth/archive/v2.5.6.tar.gz",
|
|
16
|
+
"sha256": "0825f024c9cf7a18073739b83612d46542ecbfb349ae9147a1e9f08e2d524407"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/kthohr/gcem.git",
|
|
21
|
+
"commit": "012ae73c6d0a2cb09ffe86475f5c6fba3926e200",
|
|
22
|
+
"dest": "gcem"
|
|
17
23
|
}
|
|
18
24
|
]
|
|
19
25
|
}
|