@pheem49/mint 1.9.0 → 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 +285 -140
- 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 +287 -43
- package/crates/mint-cli/src/gmail.rs +22 -2
- package/crates/mint-cli/src/interactive.rs +2888 -0
- package/crates/mint-cli/src/main.rs +336 -2854
- package/crates/mint-cli/src/markdown.rs +294 -0
- package/crates/mint-cli/src/onboard.rs +44 -1
- 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 +376 -13
- package/crates/mint-core/src/browser.rs +629 -101
- package/crates/mint-core/src/channels.rs +105 -6
- package/crates/mint-core/src/chat.rs +52 -4
- package/crates/mint-core/src/code_tools.rs +29 -11
- package/crates/mint-core/src/config.rs +118 -0
- package/crates/mint-core/src/image_gen.rs +294 -33
- package/crates/mint-core/src/lib.rs +5 -2
- package/crates/mint-core/src/mcp.rs +119 -6
- package/crates/mint-core/src/orchestration.rs +133 -11
- package/crates/mint-core/src/pictures.rs +64 -1
- package/crates/mint-core/src/skills.rs +82 -6
- package/crates/mint-core/src/video_gen.rs +235 -0
- package/package.json +8 -2
- 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/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/markdown.tsx +217 -88
- package/src/renderer/shared/utils/slashCommandProcessor.ts +239 -0
- package/src/renderer/src/components/ChatPanel.tsx +461 -64
- 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 +150 -5
- package/src/renderer/src/components/PicturesLibrary.tsx +156 -15
- package/src/renderer/src/components/Settings/AutomationTab.tsx +316 -18
- package/src/renderer/src/components/Settings/GeneralTab.tsx +762 -634
- 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 +38 -2
- package/src/renderer/src/components/VeoStudioPanel.tsx +549 -0
- 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 +352 -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 +360 -88
- 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 +137 -4
- package/src/renderer/src-web/components/PicturesLibrary.tsx +155 -15
- package/src/renderer/src-web/components/Settings/AutomationTab.tsx +316 -18
- package/src/renderer/src-web/components/Settings/GeneralTab.tsx +762 -629
- 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 +35 -2
- 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 +353 -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/lib.rs +114 -8
- 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
|
@@ -4,6 +4,7 @@ use std::{
|
|
|
4
4
|
process::{Command, Stdio},
|
|
5
5
|
};
|
|
6
6
|
|
|
7
|
+
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
|
7
8
|
use serde::Deserialize;
|
|
8
9
|
use serde_json::{Value, json};
|
|
9
10
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
@@ -11,23 +12,48 @@ use tokio::net::TcpListener;
|
|
|
11
12
|
|
|
12
13
|
use crate::{
|
|
13
14
|
AgentProgress, ApprovalOutcome, ChatRequest, ChatResponse, DEFAULT_CONVERSATION_ID,
|
|
14
|
-
ImageGenRequest, MemoryStore, MintConfig, config_path, create_folder,
|
|
15
|
-
generate_images, list_saved_pictures, load_config,
|
|
16
|
-
orchestrate_chat_stream_with_fallback, orchestrate_chat_with_fallback,
|
|
17
|
-
save_config, weather,
|
|
15
|
+
ImageGenRequest, MemoryStore, MintConfig, VideoGenRequest, config_path, create_folder,
|
|
16
|
+
find_paths, generate_images, generate_video, list_saved_pictures, load_config,
|
|
17
|
+
orchestrate_agent_loop, orchestrate_chat_stream_with_fallback, orchestrate_chat_with_fallback,
|
|
18
|
+
save_chat_images, save_config, weather,
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
const MAX_API_REQUEST_BYTES: usize = 32 * 1024 * 1024;
|
|
21
22
|
|
|
23
|
+
pub(crate) fn log_api_req(method: &str, route: &str, status: &str, info: Option<&str>) {
|
|
24
|
+
let timestamp = chrono::Local::now().format("%H:%M:%S");
|
|
25
|
+
let status_str = if status.starts_with('2') {
|
|
26
|
+
format!("\x1b[32m{}\x1b[0m", status)
|
|
27
|
+
} else if status.starts_with('4') || status.starts_with('5') {
|
|
28
|
+
format!("\x1b[1;31m{}\x1b[0m", status)
|
|
29
|
+
} else {
|
|
30
|
+
format!("\x1b[33m{}\x1b[0m", status)
|
|
31
|
+
};
|
|
32
|
+
if let Some(detail) = info {
|
|
33
|
+
println!(
|
|
34
|
+
"\x1b[90m[{}]\x1b[0m \x1b[1;36m[API]\x1b[0m \x1b[1m{}\x1b[0m {} -> {} \x1b[90m({})\x1b[0m",
|
|
35
|
+
timestamp, method, route, status_str, detail
|
|
36
|
+
);
|
|
37
|
+
} else {
|
|
38
|
+
println!(
|
|
39
|
+
"\x1b[90m[{}]\x1b[0m \x1b[1;36m[API]\x1b[0m \x1b[1m{}\x1b[0m {} -> {}",
|
|
40
|
+
timestamp, method, route, status_str
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
pub(crate) fn log_api_err(context: &str, error: &dyn std::fmt::Display) {
|
|
46
|
+
let timestamp = chrono::Local::now().format("%H:%M:%S");
|
|
47
|
+
eprintln!(
|
|
48
|
+
"\x1b[90m[{}]\x1b[0m \x1b[1;31m[ERROR]\x1b[0m \x1b[1;33m{}\x1b[0m: {}",
|
|
49
|
+
timestamp, context, error
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
22
53
|
pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
23
54
|
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
|
24
55
|
let listener = TcpListener::bind(addr).await?;
|
|
25
|
-
|
|
26
|
-
println!(
|
|
27
|
-
"\x1b[32m Mint Local API Server running at http://{}\x1b[0m",
|
|
28
|
-
addr
|
|
29
|
-
);
|
|
30
|
-
println!("\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m");
|
|
56
|
+
// API server banner removed to prevent duplicate output
|
|
31
57
|
|
|
32
58
|
// Start background messaging bridges (Telegram, Discord, Slack)
|
|
33
59
|
crate::start_channels();
|
|
@@ -125,6 +151,19 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
125
151
|
|
|
126
152
|
let (route, query) = path.split_once('?').unwrap_or((path, ""));
|
|
127
153
|
|
|
154
|
+
if route.starts_with("/api/")
|
|
155
|
+
&& !route.starts_with("/api/pictures/")
|
|
156
|
+
&& !route.starts_with("/api/thumbnails/")
|
|
157
|
+
&& route != "/api/chat"
|
|
158
|
+
&& route != "/api/chat-stream"
|
|
159
|
+
&& route != "/api/image-generate"
|
|
160
|
+
&& route != "/api/video-generate"
|
|
161
|
+
&& route != "/api/action"
|
|
162
|
+
&& route != "/api/config"
|
|
163
|
+
{
|
|
164
|
+
log_api_req(method, route, "200 OK", None);
|
|
165
|
+
}
|
|
166
|
+
|
|
128
167
|
match (method, route) {
|
|
129
168
|
("GET", "/api/status") => {
|
|
130
169
|
let config = load_config().unwrap_or_default();
|
|
@@ -230,6 +269,48 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
230
269
|
)
|
|
231
270
|
.await;
|
|
232
271
|
}
|
|
272
|
+
("GET", "/api/learned-skills") => {
|
|
273
|
+
let mut skills = match MemoryStore::open_default() {
|
|
274
|
+
Ok(m) => m.learned_skills(100).unwrap_or_default(),
|
|
275
|
+
Err(_) => Vec::new(),
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
if let Some(home) = dirs::home_dir() {
|
|
279
|
+
let global_agents_path =
|
|
280
|
+
home.join(".gemini").join("config").join("AGENTS.md");
|
|
281
|
+
crate::skills::load_agent_rules_file(&global_agents_path, &mut skills);
|
|
282
|
+
|
|
283
|
+
let global_skills_path =
|
|
284
|
+
home.join(".config").join("mint").join("mint-skills");
|
|
285
|
+
crate::skills::load_skills_from_dir(&global_skills_path, &mut skills);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if let Ok(current_dir) = std::env::current_dir() {
|
|
289
|
+
let workspace_agents_path1 = current_dir.join(".agents").join("AGENTS.md");
|
|
290
|
+
crate::skills::load_agent_rules_file(&workspace_agents_path1, &mut skills);
|
|
291
|
+
let workspace_agents_path2 = current_dir.join("AGENTS.md");
|
|
292
|
+
crate::skills::load_agent_rules_file(&workspace_agents_path2, &mut skills);
|
|
293
|
+
|
|
294
|
+
let workspace_skills_path1 = current_dir.join(".agents").join("skills");
|
|
295
|
+
crate::skills::load_skills_from_dir(&workspace_skills_path1, &mut skills);
|
|
296
|
+
let workspace_skills_path2 = current_dir.join("skills");
|
|
297
|
+
crate::skills::load_skills_from_dir(&workspace_skills_path2, &mut skills);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
let mut unique_skills = std::collections::BTreeMap::new();
|
|
301
|
+
for s in skills {
|
|
302
|
+
unique_skills.insert(s.name.clone(), s);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
let list: Vec<_> = unique_skills.into_values().collect();
|
|
306
|
+
send_json_response(
|
|
307
|
+
socket,
|
|
308
|
+
"200 OK",
|
|
309
|
+
&serde_json::to_string(&list).unwrap_or_default(),
|
|
310
|
+
)
|
|
311
|
+
.await;
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
233
314
|
("GET", "/api/profile") => {
|
|
234
315
|
let key = query_param(query, "key").unwrap_or_default();
|
|
235
316
|
if let Ok(memory) = MemoryStore::open_default() {
|
|
@@ -265,6 +346,35 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
265
346
|
)
|
|
266
347
|
.await;
|
|
267
348
|
}
|
|
349
|
+
("POST", "/api/active-model") => {
|
|
350
|
+
#[derive(Deserialize)]
|
|
351
|
+
struct ActiveModelReq {
|
|
352
|
+
provider: String,
|
|
353
|
+
model: Option<String>,
|
|
354
|
+
}
|
|
355
|
+
if let Ok(req) = serde_json::from_str::<ActiveModelReq>(body)
|
|
356
|
+
&& let Ok(mut config) = load_config()
|
|
357
|
+
{
|
|
358
|
+
if let Ok(display_name) =
|
|
359
|
+
config.set_active_model(&req.provider, req.model.as_deref())
|
|
360
|
+
{
|
|
361
|
+
send_json_response(
|
|
362
|
+
socket,
|
|
363
|
+
"200 OK",
|
|
364
|
+
&serde_json::json!({ "status": "ok", "displayName": display_name })
|
|
365
|
+
.to_string(),
|
|
366
|
+
)
|
|
367
|
+
.await;
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
send_json_response(
|
|
372
|
+
socket,
|
|
373
|
+
"500 Internal Server Error",
|
|
374
|
+
"{\"status\":\"error\"}",
|
|
375
|
+
)
|
|
376
|
+
.await;
|
|
377
|
+
}
|
|
268
378
|
("POST", "/api/interactions/clear") => {
|
|
269
379
|
if let Ok(memory) = MemoryStore::open_default() {
|
|
270
380
|
let chat_id = query_param(query, "chatId")
|
|
@@ -286,6 +396,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
286
396
|
struct ApiSaveInteraction {
|
|
287
397
|
chat_id: String,
|
|
288
398
|
user_text: String,
|
|
399
|
+
ai_text: Option<String>,
|
|
289
400
|
provider: String,
|
|
290
401
|
model: String,
|
|
291
402
|
}
|
|
@@ -295,7 +406,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
295
406
|
match memory.add_interaction_for_chat(
|
|
296
407
|
&req.chat_id,
|
|
297
408
|
&req.user_text,
|
|
298
|
-
"",
|
|
409
|
+
req.ai_text.as_deref().unwrap_or(""),
|
|
299
410
|
&req.provider,
|
|
300
411
|
&req.model,
|
|
301
412
|
) {
|
|
@@ -389,6 +500,22 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
389
500
|
}
|
|
390
501
|
}
|
|
391
502
|
}
|
|
503
|
+
("GET", route) if route.starts_with("/api/thumbnails/") => {
|
|
504
|
+
let filename = percent_decode(route.trim_start_matches("/api/thumbnails/"));
|
|
505
|
+
match thumbnail_bytes(&filename) {
|
|
506
|
+
Ok((mime_type, bytes)) => {
|
|
507
|
+
send_binary_response(socket, "200 OK", &mime_type, &bytes).await
|
|
508
|
+
}
|
|
509
|
+
Err(_) => {
|
|
510
|
+
send_json_response(
|
|
511
|
+
socket,
|
|
512
|
+
"404 Not Found",
|
|
513
|
+
"{\"error\":\"thumbnail not found\"}",
|
|
514
|
+
)
|
|
515
|
+
.await
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
392
519
|
("GET", "/api/config") => {
|
|
393
520
|
let config = load_config().unwrap_or_default();
|
|
394
521
|
if let Ok(json_str) = serde_json::to_string(&config) {
|
|
@@ -458,6 +585,71 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
458
585
|
.await;
|
|
459
586
|
}
|
|
460
587
|
}
|
|
588
|
+
("POST", "/api/uploads") => {
|
|
589
|
+
// Accept raw body bytes and optional filename query param.
|
|
590
|
+
// Body is the raw file bytes (client should POST the file as the request body).
|
|
591
|
+
let filename_param = query_param(query, "filename");
|
|
592
|
+
let header_end_idx = header_end as usize;
|
|
593
|
+
// extract raw bytes for body
|
|
594
|
+
let body_bytes = &request_bytes[header_end_idx + 4..];
|
|
595
|
+
|
|
596
|
+
if body_bytes.is_empty() {
|
|
597
|
+
send_json_response(socket, "400 Bad Request", "{\"error\":\"empty body\"}")
|
|
598
|
+
.await;
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Determine mime type from filename or fallback to video/mp4
|
|
603
|
+
let (mime_type, _extension) = filename_param
|
|
604
|
+
.as_deref()
|
|
605
|
+
.and_then(|f| f.rsplit_once('.'))
|
|
606
|
+
.map(|(_, ext)| {
|
|
607
|
+
let e = ext.to_ascii_lowercase();
|
|
608
|
+
match e.as_str() {
|
|
609
|
+
"mp4" => ("video/mp4", "mp4"),
|
|
610
|
+
"webm" => ("video/webm", "webm"),
|
|
611
|
+
"mov" => ("video/quicktime", "mov"),
|
|
612
|
+
"mkv" => ("video/x-matroska", "mkv"),
|
|
613
|
+
"avi" => ("video/x-msvideo", "avi"),
|
|
614
|
+
_ => ("application/octet-stream", "bin"),
|
|
615
|
+
}
|
|
616
|
+
})
|
|
617
|
+
.unwrap_or(("video/mp4", "mp4"));
|
|
618
|
+
|
|
619
|
+
// Build data URI and reuse save_chat_images helper to persist and index file
|
|
620
|
+
let encoded = BASE64.encode(body_bytes);
|
|
621
|
+
let data_uri = format!("data:{};base64,{}", mime_type, encoded);
|
|
622
|
+
match save_chat_images(
|
|
623
|
+
vec![data_uri],
|
|
624
|
+
Some("upload".into()),
|
|
625
|
+
Some("uploaded".into()),
|
|
626
|
+
) {
|
|
627
|
+
Ok(saved) => {
|
|
628
|
+
if let Some(entry) = saved.into_iter().next() {
|
|
629
|
+
let res_json = json!({ "url": format!("/api/pictures/{}", entry.filename), "filename": entry.filename });
|
|
630
|
+
send_json_response(socket, "200 OK", &res_json.to_string()).await;
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
Err(err) => {
|
|
635
|
+
let err_json = json!({ "error": err.to_string() });
|
|
636
|
+
send_json_response(
|
|
637
|
+
socket,
|
|
638
|
+
"500 Internal Server Error",
|
|
639
|
+
&err_json.to_string(),
|
|
640
|
+
)
|
|
641
|
+
.await;
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
send_json_response(
|
|
646
|
+
socket,
|
|
647
|
+
"500 Internal Server Error",
|
|
648
|
+
"{\"error\":\"failed to save upload\"}",
|
|
649
|
+
)
|
|
650
|
+
.await;
|
|
651
|
+
}
|
|
652
|
+
|
|
461
653
|
("POST", "/api/chat") => {
|
|
462
654
|
#[derive(Deserialize)]
|
|
463
655
|
#[serde(rename_all = "camelCase")]
|
|
@@ -467,6 +659,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
467
659
|
chat_id: Option<String>,
|
|
468
660
|
image_data_uri: Option<String>,
|
|
469
661
|
audio_data_uri: Option<String>,
|
|
662
|
+
video_data_uri: Option<String>,
|
|
470
663
|
document_attachment: Option<crate::chat::DocumentAttachment>,
|
|
471
664
|
agent_id: Option<String>,
|
|
472
665
|
}
|
|
@@ -479,6 +672,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
479
672
|
chat_id: req.chat_id,
|
|
480
673
|
image_data_uri: req.image_data_uri,
|
|
481
674
|
audio_data_uri: req.audio_data_uri,
|
|
675
|
+
video_data_uri: req.video_data_uri,
|
|
482
676
|
document_attachment: req.document_attachment,
|
|
483
677
|
workspace_path: None,
|
|
484
678
|
agent_id: req.agent_id,
|
|
@@ -486,6 +680,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
486
680
|
let mut chat_req =
|
|
487
681
|
chat_req.with_document_context(&config).unwrap_or(chat_req);
|
|
488
682
|
let sent_image = chat_req.image_data_uri.clone();
|
|
683
|
+
let sent_video = chat_req.video_data_uri.clone();
|
|
489
684
|
let sent_message = chat_req.message.clone();
|
|
490
685
|
|
|
491
686
|
let response = if let Some(clean_message) =
|
|
@@ -505,6 +700,12 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
505
700
|
|
|
506
701
|
match response {
|
|
507
702
|
Ok(resp) => {
|
|
703
|
+
log_api_req(
|
|
704
|
+
"POST",
|
|
705
|
+
"/api/chat",
|
|
706
|
+
"200 OK",
|
|
707
|
+
Some(&format!("Model: {}", config.ai_provider)),
|
|
708
|
+
);
|
|
508
709
|
if let Some(image) = sent_image {
|
|
509
710
|
let _ = save_chat_images(
|
|
510
711
|
image
|
|
@@ -512,6 +713,16 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
512
713
|
.map(str::to_owned)
|
|
513
714
|
.collect::<Vec<_>>(),
|
|
514
715
|
Some("web".into()),
|
|
716
|
+
Some(sent_message.clone()),
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
if let Some(video) = sent_video {
|
|
720
|
+
let _ = save_chat_images(
|
|
721
|
+
video
|
|
722
|
+
.split_whitespace()
|
|
723
|
+
.map(str::to_owned)
|
|
724
|
+
.collect::<Vec<_>>(),
|
|
725
|
+
Some("web".into()),
|
|
515
726
|
Some(sent_message),
|
|
516
727
|
);
|
|
517
728
|
}
|
|
@@ -521,7 +732,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
521
732
|
}
|
|
522
733
|
}
|
|
523
734
|
Err(e) => {
|
|
524
|
-
|
|
735
|
+
log_api_err("API /api/chat error", &e);
|
|
525
736
|
let err_json = serde_json::json!({
|
|
526
737
|
"provider": "error",
|
|
527
738
|
"model": "error",
|
|
@@ -571,6 +782,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
571
782
|
chat_id: Option<String>,
|
|
572
783
|
image_data_uri: Option<String>,
|
|
573
784
|
audio_data_uri: Option<String>,
|
|
785
|
+
video_data_uri: Option<String>,
|
|
574
786
|
document_attachment: Option<crate::chat::DocumentAttachment>,
|
|
575
787
|
agent_id: Option<String>,
|
|
576
788
|
}
|
|
@@ -583,6 +795,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
583
795
|
chat_id: req.chat_id,
|
|
584
796
|
image_data_uri: req.image_data_uri,
|
|
585
797
|
audio_data_uri: req.audio_data_uri,
|
|
798
|
+
video_data_uri: req.video_data_uri,
|
|
586
799
|
document_attachment: req.document_attachment,
|
|
587
800
|
workspace_path: None,
|
|
588
801
|
agent_id: req.agent_id,
|
|
@@ -590,6 +803,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
590
803
|
let mut chat_req =
|
|
591
804
|
chat_req.with_document_context(&config).unwrap_or(chat_req);
|
|
592
805
|
let sent_image = chat_req.image_data_uri.clone();
|
|
806
|
+
let sent_video = chat_req.video_data_uri.clone();
|
|
593
807
|
let sent_message = chat_req.message.clone();
|
|
594
808
|
|
|
595
809
|
let is_chat = if let Some(clean_message) =
|
|
@@ -666,6 +880,15 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
666
880
|
|
|
667
881
|
match result {
|
|
668
882
|
Ok((response, _)) => {
|
|
883
|
+
log_api_req(
|
|
884
|
+
"POST",
|
|
885
|
+
"/api/chat-stream",
|
|
886
|
+
"200 OK",
|
|
887
|
+
Some(&format!(
|
|
888
|
+
"Provider: {}",
|
|
889
|
+
config_clone.ai_provider
|
|
890
|
+
)),
|
|
891
|
+
);
|
|
669
892
|
if let Ok(json_val) =
|
|
670
893
|
serde_json::to_string(&serde_json::json!({
|
|
671
894
|
"type": "done",
|
|
@@ -676,7 +899,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
676
899
|
}
|
|
677
900
|
}
|
|
678
901
|
Err(e) => {
|
|
679
|
-
|
|
902
|
+
log_api_err("API /api/chat-stream error", &e);
|
|
680
903
|
let err_json = serde_json::json!({
|
|
681
904
|
"type": "done",
|
|
682
905
|
"response": {
|
|
@@ -722,6 +945,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
722
945
|
let chat_id_str = chat_id.clone().unwrap_or_default();
|
|
723
946
|
let message = chat_req.message.clone();
|
|
724
947
|
let image_data_uri = chat_req.image_data_uri.clone();
|
|
948
|
+
let video_data_uri = chat_req.video_data_uri.clone();
|
|
725
949
|
let agent_id = chat_req.agent_id.clone();
|
|
726
950
|
|
|
727
951
|
let join_handle = tokio::spawn(async move {
|
|
@@ -730,6 +954,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
730
954
|
&message,
|
|
731
955
|
&root,
|
|
732
956
|
image_data_uri,
|
|
957
|
+
video_data_uri,
|
|
733
958
|
chat_id.as_deref(),
|
|
734
959
|
agent_id.as_deref(),
|
|
735
960
|
fast_mode,
|
|
@@ -807,6 +1032,16 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
807
1032
|
.map(str::to_owned)
|
|
808
1033
|
.collect::<Vec<_>>(),
|
|
809
1034
|
Some("web".into()),
|
|
1035
|
+
Some(sent_message.clone()),
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
if let Some(video) = sent_video {
|
|
1039
|
+
let _ = save_chat_images(
|
|
1040
|
+
video
|
|
1041
|
+
.split_whitespace()
|
|
1042
|
+
.map(str::to_owned)
|
|
1043
|
+
.collect::<Vec<_>>(),
|
|
1044
|
+
Some("web".into()),
|
|
810
1045
|
Some(sent_message),
|
|
811
1046
|
);
|
|
812
1047
|
}
|
|
@@ -836,6 +1071,12 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
836
1071
|
/// Which image provider to use (overrides config.image_gen_provider).
|
|
837
1072
|
#[serde(default)]
|
|
838
1073
|
provider: Option<String>,
|
|
1074
|
+
#[serde(default)]
|
|
1075
|
+
image_data_uri: Option<String>,
|
|
1076
|
+
#[serde(default)]
|
|
1077
|
+
mask_data_uri: Option<String>,
|
|
1078
|
+
#[serde(default)]
|
|
1079
|
+
mode: Option<String>,
|
|
839
1080
|
}
|
|
840
1081
|
|
|
841
1082
|
if let Ok(req) = serde_json::from_str::<ImageGenApiRequest>(body) {
|
|
@@ -847,9 +1088,18 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
847
1088
|
num_images: req.num_images,
|
|
848
1089
|
model: req.model,
|
|
849
1090
|
provider: req.provider,
|
|
1091
|
+
image_data_uri: req.image_data_uri,
|
|
1092
|
+
mask_data_uri: req.mask_data_uri,
|
|
1093
|
+
mode: req.mode,
|
|
850
1094
|
};
|
|
851
1095
|
match generate_images(&config, &gen_request).await {
|
|
852
1096
|
Ok(result) => {
|
|
1097
|
+
log_api_req(
|
|
1098
|
+
"POST",
|
|
1099
|
+
"/api/image-generate",
|
|
1100
|
+
"200 OK",
|
|
1101
|
+
Some(&format!("Provider: {}", result.provider)),
|
|
1102
|
+
);
|
|
853
1103
|
let data_uris: Vec<String> = result
|
|
854
1104
|
.images
|
|
855
1105
|
.iter()
|
|
@@ -877,6 +1127,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
877
1127
|
send_json_response(socket, "200 OK", &response.to_string()).await;
|
|
878
1128
|
}
|
|
879
1129
|
Err(e) => {
|
|
1130
|
+
log_api_err("API /api/image-generate error", &e);
|
|
880
1131
|
let err = json!({ "error": e.to_string() });
|
|
881
1132
|
send_json_response(
|
|
882
1133
|
socket,
|
|
@@ -895,6 +1146,98 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
895
1146
|
.await;
|
|
896
1147
|
}
|
|
897
1148
|
}
|
|
1149
|
+
("POST", "/api/video-generate") => {
|
|
1150
|
+
#[derive(serde::Deserialize)]
|
|
1151
|
+
#[serde(rename_all = "camelCase")]
|
|
1152
|
+
struct VideoGenApiRequest {
|
|
1153
|
+
prompt: String,
|
|
1154
|
+
#[serde(default)]
|
|
1155
|
+
negative_prompt: Option<String>,
|
|
1156
|
+
#[serde(default)]
|
|
1157
|
+
aspect_ratio: Option<String>,
|
|
1158
|
+
#[serde(default)]
|
|
1159
|
+
duration: Option<u32>,
|
|
1160
|
+
#[serde(default)]
|
|
1161
|
+
model: Option<String>,
|
|
1162
|
+
#[serde(default)]
|
|
1163
|
+
provider: Option<String>,
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
if let Ok(req) = serde_json::from_str::<VideoGenApiRequest>(body) {
|
|
1167
|
+
let config = load_config().unwrap_or_default();
|
|
1168
|
+
let gen_request = VideoGenRequest {
|
|
1169
|
+
prompt: req.prompt.clone(),
|
|
1170
|
+
negative_prompt: req.negative_prompt,
|
|
1171
|
+
aspect_ratio: req.aspect_ratio.unwrap_or_else(|| "16:9".to_string()),
|
|
1172
|
+
duration: req.duration.unwrap_or(5),
|
|
1173
|
+
model: req.model,
|
|
1174
|
+
provider: req.provider.unwrap_or_else(|| "veo".to_string()),
|
|
1175
|
+
};
|
|
1176
|
+
match generate_video(&config, &gen_request).await {
|
|
1177
|
+
Ok(result) => {
|
|
1178
|
+
let mut response =
|
|
1179
|
+
serde_json::to_value(&result).unwrap_or(json!({}));
|
|
1180
|
+
if let Some(videos) =
|
|
1181
|
+
response.get_mut("videos").and_then(|v| v.as_array_mut())
|
|
1182
|
+
{
|
|
1183
|
+
for picture in videos {
|
|
1184
|
+
let filename = picture
|
|
1185
|
+
.get("filename")
|
|
1186
|
+
.and_then(|f| f.as_str())
|
|
1187
|
+
.map(|s| s.to_string());
|
|
1188
|
+
if let Some(filename) = filename {
|
|
1189
|
+
picture.as_object_mut().unwrap().insert(
|
|
1190
|
+
"url".to_string(),
|
|
1191
|
+
json!(format!("/api/pictures/{}", filename)),
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
let id = picture
|
|
1195
|
+
.get("id")
|
|
1196
|
+
.and_then(|i| i.as_str())
|
|
1197
|
+
.map(|s| s.to_string());
|
|
1198
|
+
if let Some(id) = id {
|
|
1199
|
+
let has_thumb = picture.get("thumbnailPath").is_some()
|
|
1200
|
+
|| picture.get("thumbnailUrl").is_some();
|
|
1201
|
+
if has_thumb {
|
|
1202
|
+
picture.as_object_mut().unwrap().insert(
|
|
1203
|
+
"thumbnailUrl".to_string(),
|
|
1204
|
+
json!(format!(
|
|
1205
|
+
"/api/thumbnails/{}.thumb.png",
|
|
1206
|
+
id
|
|
1207
|
+
)),
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
log_api_req(
|
|
1214
|
+
"POST",
|
|
1215
|
+
"/api/video-generate",
|
|
1216
|
+
"200 OK",
|
|
1217
|
+
Some(&format!("Provider: {}", result.provider)),
|
|
1218
|
+
);
|
|
1219
|
+
send_json_response(socket, "200 OK", &response.to_string()).await;
|
|
1220
|
+
}
|
|
1221
|
+
Err(e) => {
|
|
1222
|
+
log_api_err("API /api/video-generate error", &e);
|
|
1223
|
+
let err = json!({ "error": e.to_string() });
|
|
1224
|
+
send_json_response(
|
|
1225
|
+
socket,
|
|
1226
|
+
"500 Internal Server Error",
|
|
1227
|
+
&err.to_string(),
|
|
1228
|
+
)
|
|
1229
|
+
.await;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
} else {
|
|
1233
|
+
send_json_response(
|
|
1234
|
+
socket,
|
|
1235
|
+
"400 Bad Request",
|
|
1236
|
+
"{\"error\":\"invalid video generation request body\"}",
|
|
1237
|
+
)
|
|
1238
|
+
.await;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
898
1241
|
("GET", "/api/image-gen/providers") => {
|
|
899
1242
|
let config = load_config().unwrap_or_default();
|
|
900
1243
|
let mut available: Vec<String> = Vec::new();
|
|
@@ -947,6 +1290,7 @@ async fn run_web_agent_loop(
|
|
|
947
1290
|
&request.message,
|
|
948
1291
|
&root,
|
|
949
1292
|
request.image_data_uri.clone(),
|
|
1293
|
+
request.video_data_uri.clone(),
|
|
950
1294
|
request.chat_id.as_deref(),
|
|
951
1295
|
request.agent_id.as_deref(),
|
|
952
1296
|
fast_mode,
|
|
@@ -1125,6 +1469,25 @@ fn picture_bytes(filename: &str) -> Result<(String, Vec<u8>), String> {
|
|
|
1125
1469
|
Ok((picture.mime_type, bytes))
|
|
1126
1470
|
}
|
|
1127
1471
|
|
|
1472
|
+
fn thumbnail_bytes(filename: &str) -> Result<(String, Vec<u8>), String> {
|
|
1473
|
+
if filename.contains('/') || filename.contains('\\') || filename.contains("..") {
|
|
1474
|
+
return Err("invalid thumbnail path".into());
|
|
1475
|
+
}
|
|
1476
|
+
let id = filename
|
|
1477
|
+
.strip_suffix(".thumb.png")
|
|
1478
|
+
.ok_or_else(|| "invalid thumbnail name".to_string())?;
|
|
1479
|
+
let picture = list_saved_pictures()
|
|
1480
|
+
.map_err(|error| error.to_string())?
|
|
1481
|
+
.into_iter()
|
|
1482
|
+
.find(|entry| entry.id == id)
|
|
1483
|
+
.ok_or_else(|| "picture not found".to_string())?;
|
|
1484
|
+
let thumb_path = picture
|
|
1485
|
+
.thumbnail_path
|
|
1486
|
+
.ok_or_else(|| "thumbnail not found".to_string())?;
|
|
1487
|
+
let bytes = std::fs::read(&thumb_path).map_err(|error| error.to_string())?;
|
|
1488
|
+
Ok(("image/png".to_string(), bytes))
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1128
1491
|
fn query_param(query: &str, key: &str) -> Option<String> {
|
|
1129
1492
|
query.split('&').find_map(|pair| {
|
|
1130
1493
|
let (name, value) = pair.split_once('=')?;
|