@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
|
@@ -10,8 +10,8 @@ use crate::MintConfig;
|
|
|
10
10
|
// Public request / response types
|
|
11
11
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
12
12
|
|
|
13
|
-
/// A request to generate images from any supported provider.
|
|
14
|
-
#[derive(Debug, Clone, Deserialize)]
|
|
13
|
+
/// A request to generate or edit images from any supported provider.
|
|
14
|
+
#[derive(Debug, Clone, Deserialize, Default)]
|
|
15
15
|
#[serde(rename_all = "camelCase")]
|
|
16
16
|
pub struct ImageGenRequest {
|
|
17
17
|
pub prompt: String,
|
|
@@ -30,6 +30,15 @@ pub struct ImageGenRequest {
|
|
|
30
30
|
/// Falls back to `config.image_gen_provider` when omitted.
|
|
31
31
|
#[serde(default)]
|
|
32
32
|
pub provider: Option<String>,
|
|
33
|
+
/// Optional input image data URI (`data:image/png;base64,...`) for Image-to-Image / Inpainting
|
|
34
|
+
#[serde(default)]
|
|
35
|
+
pub image_data_uri: Option<String>,
|
|
36
|
+
/// Optional mask image data URI for Inpainting
|
|
37
|
+
#[serde(default)]
|
|
38
|
+
pub mask_data_uri: Option<String>,
|
|
39
|
+
/// Optional mode: "generate" | "edit" | "inpaint". Inferred if omitted.
|
|
40
|
+
#[serde(default)]
|
|
41
|
+
pub mode: Option<String>,
|
|
33
42
|
}
|
|
34
43
|
|
|
35
44
|
/// One generated image returned from the model.
|
|
@@ -76,6 +85,10 @@ pub enum ImageGenError {
|
|
|
76
85
|
// ── Generic ──────────────────────────────────────────────────────────────
|
|
77
86
|
#[error("unsupported image generation provider: {0}")]
|
|
78
87
|
UnsupportedProvider(String),
|
|
88
|
+
#[error(
|
|
89
|
+
"image-to-image editing is not supported by {0} — please use Replicate, Stability AI, or DALL·E"
|
|
90
|
+
)]
|
|
91
|
+
UnsupportedMode(String),
|
|
79
92
|
#[error("API request failed: {0}")]
|
|
80
93
|
Request(#[from] reqwest::Error),
|
|
81
94
|
#[error("the model returned no images for this prompt")]
|
|
@@ -129,6 +142,15 @@ async fn call_nanobanana(
|
|
|
129
142
|
config: &MintConfig,
|
|
130
143
|
request: &ImageGenRequest,
|
|
131
144
|
) -> Result<ImageGenResponse, ImageGenError> {
|
|
145
|
+
if request.image_data_uri.is_some()
|
|
146
|
+
|| request.mode.as_deref() == Some("edit")
|
|
147
|
+
|| request.mode.as_deref() == Some("inpaint")
|
|
148
|
+
{
|
|
149
|
+
return Err(ImageGenError::UnsupportedMode(
|
|
150
|
+
"NanoBanana (Gemini Text-to-Image)".to_owned(),
|
|
151
|
+
));
|
|
152
|
+
}
|
|
153
|
+
|
|
132
154
|
let api_key = if config.api_key.trim().is_empty() {
|
|
133
155
|
std::env::var("GEMINI_API_KEY").unwrap_or_default()
|
|
134
156
|
} else {
|
|
@@ -270,6 +292,105 @@ async fn call_dalle(
|
|
|
270
292
|
return Err(ImageGenError::MissingOpenAiKey);
|
|
271
293
|
}
|
|
272
294
|
|
|
295
|
+
if let Some(ref img_data_uri) = request.image_data_uri {
|
|
296
|
+
let (mime_type, img_bytes) = parse_data_uri(img_data_uri)
|
|
297
|
+
.ok_or_else(|| ImageGenError::ModelError("invalid base64 image data URI".to_owned()))?;
|
|
298
|
+
|
|
299
|
+
let size = aspect_ratio_to_dalle_size(request.aspect_ratio.as_deref());
|
|
300
|
+
let boundary = format!("MintBoundary{}", uuid_v4_hex());
|
|
301
|
+
|
|
302
|
+
let mut text_fields = vec![
|
|
303
|
+
("prompt".to_owned(), request.prompt.clone()),
|
|
304
|
+
("n".to_owned(), "1".to_owned()),
|
|
305
|
+
("size".to_owned(), size.to_owned()),
|
|
306
|
+
("response_format".to_owned(), "b64_json".to_owned()),
|
|
307
|
+
("model".to_owned(), "dall-e-2".to_owned()),
|
|
308
|
+
];
|
|
309
|
+
if let Some(ref neg) = request.negative_prompt {
|
|
310
|
+
if !neg.trim().is_empty() {
|
|
311
|
+
text_fields.push((
|
|
312
|
+
"prompt".to_owned(),
|
|
313
|
+
format!("{} (avoid: {})", request.prompt, neg),
|
|
314
|
+
));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
let mut file_fields = vec![(
|
|
319
|
+
"image",
|
|
320
|
+
"image.png",
|
|
321
|
+
mime_type.as_str(),
|
|
322
|
+
img_bytes.as_slice(),
|
|
323
|
+
)];
|
|
324
|
+
|
|
325
|
+
let mask_bytes_storage;
|
|
326
|
+
if let Some(ref mask_data_uri) = request.mask_data_uri {
|
|
327
|
+
if let Some((mask_mime, mask_bytes)) = parse_data_uri(mask_data_uri) {
|
|
328
|
+
mask_bytes_storage = (mask_mime, mask_bytes);
|
|
329
|
+
file_fields.push((
|
|
330
|
+
"mask",
|
|
331
|
+
"mask.png",
|
|
332
|
+
mask_bytes_storage.0.as_str(),
|
|
333
|
+
mask_bytes_storage.1.as_slice(),
|
|
334
|
+
));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
let body = build_multipart_binary(&boundary, &text_fields, &file_fields);
|
|
339
|
+
|
|
340
|
+
let response: Value = client
|
|
341
|
+
.post("https://api.openai.com/v1/images/edits")
|
|
342
|
+
.bearer_auth(&api_key)
|
|
343
|
+
.header(
|
|
344
|
+
"content-type",
|
|
345
|
+
format!("multipart/form-data; boundary={boundary}"),
|
|
346
|
+
)
|
|
347
|
+
.body(body)
|
|
348
|
+
.send()
|
|
349
|
+
.await?
|
|
350
|
+
.error_for_status()
|
|
351
|
+
.map_err(ImageGenError::Request)?
|
|
352
|
+
.json()
|
|
353
|
+
.await?;
|
|
354
|
+
|
|
355
|
+
if let Some(err) = response.get("error") {
|
|
356
|
+
let msg = err["message"]
|
|
357
|
+
.as_str()
|
|
358
|
+
.unwrap_or("unknown OpenAI API error");
|
|
359
|
+
return Err(ImageGenError::ModelError(msg.to_owned()));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
let data = response["data"]
|
|
363
|
+
.as_array()
|
|
364
|
+
.ok_or(ImageGenError::UnexpectedResponse)?;
|
|
365
|
+
|
|
366
|
+
let images: Vec<GeneratedImage> = data
|
|
367
|
+
.iter()
|
|
368
|
+
.filter_map(|item| {
|
|
369
|
+
let b64 = item["b64_json"].as_str()?;
|
|
370
|
+
if b64.is_empty() || STANDARD.decode(b64).is_err() {
|
|
371
|
+
return None;
|
|
372
|
+
}
|
|
373
|
+
let data_uri = format!("data:image/png;base64,{b64}");
|
|
374
|
+
Some(GeneratedImage {
|
|
375
|
+
data_uri,
|
|
376
|
+
mime_type: "image/png".to_owned(),
|
|
377
|
+
})
|
|
378
|
+
})
|
|
379
|
+
.collect();
|
|
380
|
+
|
|
381
|
+
if images.is_empty() {
|
|
382
|
+
return Err(ImageGenError::NoImagesReturned);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return Ok(ImageGenResponse {
|
|
386
|
+
images,
|
|
387
|
+
model: "dall-e-2".to_owned(),
|
|
388
|
+
provider: "dalle".to_owned(),
|
|
389
|
+
prompt: request.prompt.clone(),
|
|
390
|
+
description: None,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
273
394
|
let model = request
|
|
274
395
|
.model
|
|
275
396
|
.as_deref()
|
|
@@ -393,42 +514,98 @@ async fn call_stability(
|
|
|
393
514
|
}
|
|
394
515
|
});
|
|
395
516
|
|
|
396
|
-
|
|
397
|
-
|
|
517
|
+
let boundary = format!("MintBoundary{}", uuid_v4_hex());
|
|
518
|
+
|
|
519
|
+
let (endpoint, raw_body) = if let Some(ref img_uri) = request.image_data_uri {
|
|
520
|
+
let (mime_type, img_bytes) = parse_data_uri(img_uri)
|
|
521
|
+
.ok_or_else(|| ImageGenError::ModelError("invalid base64 image data URI".to_owned()))?;
|
|
522
|
+
|
|
523
|
+
let is_inpaint =
|
|
524
|
+
request.mask_data_uri.is_some() || request.mode.as_deref() == Some("inpaint");
|
|
525
|
+
let endpoint = if is_inpaint {
|
|
526
|
+
"https://api.stability.ai/v2beta/stable-image/edit/inpaint".to_owned()
|
|
527
|
+
} else {
|
|
528
|
+
"https://api.stability.ai/v2beta/stable-image/generate/sd3".to_owned()
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
let mut body_parts: Vec<(String, String)> = vec![
|
|
532
|
+
("prompt".into(), request.prompt.clone()),
|
|
533
|
+
("output_format".into(), "png".to_owned()),
|
|
534
|
+
];
|
|
535
|
+
if !is_inpaint && model != "core" {
|
|
536
|
+
body_parts.push(("model".into(), model.clone()));
|
|
537
|
+
body_parts.push(("mode".into(), "image-to-image".to_owned()));
|
|
538
|
+
body_parts.push(("strength".into(), "0.7".to_owned()));
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if let Some(neg) = &request.negative_prompt {
|
|
542
|
+
let neg = neg.trim();
|
|
543
|
+
if !neg.is_empty() {
|
|
544
|
+
body_parts.push(("negative_prompt".into(), neg.to_owned()));
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
let mut file_fields = vec![(
|
|
549
|
+
"image",
|
|
550
|
+
"input.png",
|
|
551
|
+
mime_type.as_str(),
|
|
552
|
+
img_bytes.as_slice(),
|
|
553
|
+
)];
|
|
554
|
+
let mask_storage;
|
|
555
|
+
if let Some(ref mask_uri) = request.mask_data_uri {
|
|
556
|
+
if let Some((mask_mime, mask_bytes)) = parse_data_uri(mask_uri) {
|
|
557
|
+
mask_storage = (mask_mime, mask_bytes);
|
|
558
|
+
file_fields.push((
|
|
559
|
+
"mask",
|
|
560
|
+
"mask.png",
|
|
561
|
+
mask_storage.0.as_str(),
|
|
562
|
+
mask_storage.1.as_slice(),
|
|
563
|
+
));
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
398
567
|
(
|
|
399
|
-
|
|
400
|
-
|
|
568
|
+
endpoint,
|
|
569
|
+
build_multipart_binary(&boundary, &body_parts, &file_fields),
|
|
401
570
|
)
|
|
402
571
|
} else {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
572
|
+
// Route: Stable Image Core uses /core; SD3.x models use /sd3
|
|
573
|
+
let (endpoint, model_param) = if model == "core" {
|
|
574
|
+
(
|
|
575
|
+
"https://api.stability.ai/v2beta/stable-image/generate/core".to_owned(),
|
|
576
|
+
None,
|
|
577
|
+
)
|
|
578
|
+
} else {
|
|
579
|
+
(
|
|
580
|
+
"https://api.stability.ai/v2beta/stable-image/generate/sd3".to_owned(),
|
|
581
|
+
Some(model.clone()),
|
|
582
|
+
)
|
|
583
|
+
};
|
|
410
584
|
|
|
411
|
-
|
|
412
|
-
let boundary = format!("MintBoundary{}", uuid_v4_hex());
|
|
585
|
+
let aspect_ratio = request.aspect_ratio.as_deref().unwrap_or("1:1");
|
|
413
586
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
587
|
+
let mut body_parts: Vec<(String, String)> = vec![
|
|
588
|
+
("prompt".into(), request.prompt.clone()),
|
|
589
|
+
("aspect_ratio".into(), aspect_ratio.to_owned()),
|
|
590
|
+
("output_format".into(), "png".to_owned()),
|
|
591
|
+
];
|
|
419
592
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
593
|
+
if let Some(neg) = &request.negative_prompt {
|
|
594
|
+
let neg = neg.trim();
|
|
595
|
+
if !neg.is_empty() {
|
|
596
|
+
body_parts.push(("negative_prompt".into(), neg.to_owned()));
|
|
597
|
+
}
|
|
424
598
|
}
|
|
425
|
-
}
|
|
426
599
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
600
|
+
if let Some(m) = model_param {
|
|
601
|
+
body_parts.push(("model".into(), m));
|
|
602
|
+
}
|
|
430
603
|
|
|
431
|
-
|
|
604
|
+
(
|
|
605
|
+
endpoint,
|
|
606
|
+
build_multipart_text(&boundary, &body_parts).into_bytes(),
|
|
607
|
+
)
|
|
608
|
+
};
|
|
432
609
|
|
|
433
610
|
let response = client
|
|
434
611
|
.post(&endpoint)
|
|
@@ -604,17 +781,23 @@ async fn call_replicate(
|
|
|
604
781
|
.map(str::to_owned)
|
|
605
782
|
.unwrap_or_else(|| {
|
|
606
783
|
let m = config.replicate_model.trim();
|
|
607
|
-
if m.is_empty() {
|
|
608
|
-
"black-forest-labs/flux-1.1-pro".to_owned()
|
|
609
|
-
} else {
|
|
784
|
+
if !m.is_empty() {
|
|
610
785
|
m.to_owned()
|
|
786
|
+
} else if request.image_data_uri.is_some() {
|
|
787
|
+
if request.mask_data_uri.is_some() {
|
|
788
|
+
"black-forest-labs/flux-fill-dev".to_owned()
|
|
789
|
+
} else {
|
|
790
|
+
"timbrooks/instruct-pix2pix".to_owned()
|
|
791
|
+
}
|
|
792
|
+
} else {
|
|
793
|
+
"black-forest-labs/flux-1.1-pro".to_owned()
|
|
611
794
|
}
|
|
612
795
|
});
|
|
613
796
|
|
|
614
797
|
let num_images = request.num_images.unwrap_or(1).clamp(1, 4);
|
|
615
798
|
let aspect_ratio = request.aspect_ratio.as_deref().unwrap_or("1:1");
|
|
616
799
|
|
|
617
|
-
// Build input — common fields used by FLUX and SDXL families
|
|
800
|
+
// Build input — common fields used by FLUX, InstructPix2Pix, and SDXL families
|
|
618
801
|
let mut input = json!({
|
|
619
802
|
"prompt": request.prompt,
|
|
620
803
|
"num_outputs": num_images,
|
|
@@ -623,6 +806,13 @@ async fn call_replicate(
|
|
|
623
806
|
"output_quality": 90
|
|
624
807
|
});
|
|
625
808
|
|
|
809
|
+
if let Some(ref img_uri) = request.image_data_uri {
|
|
810
|
+
input["image"] = json!(img_uri);
|
|
811
|
+
if let Some(ref mask_uri) = request.mask_data_uri {
|
|
812
|
+
input["mask"] = json!(mask_uri);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
626
816
|
if let Some(neg) = &request.negative_prompt {
|
|
627
817
|
let neg = neg.trim();
|
|
628
818
|
if !neg.is_empty() {
|
|
@@ -659,11 +849,11 @@ async fn call_replicate(
|
|
|
659
849
|
poll_replicate(client, &api_key, &prediction_url).await?
|
|
660
850
|
};
|
|
661
851
|
|
|
662
|
-
if let Some(err) = prediction.get("error")
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
852
|
+
if let Some(err) = prediction.get("error")
|
|
853
|
+
&& !err.is_null()
|
|
854
|
+
{
|
|
855
|
+
let msg = err.as_str().unwrap_or("unknown Replicate error");
|
|
856
|
+
return Err(ImageGenError::ModelError(msg.to_owned()));
|
|
667
857
|
}
|
|
668
858
|
|
|
669
859
|
// Output is an array of image URLs
|
|
@@ -790,6 +980,58 @@ fn build_multipart_text(boundary: &str, fields: &[(String, String)]) -> String {
|
|
|
790
980
|
body
|
|
791
981
|
}
|
|
792
982
|
|
|
983
|
+
/// Parse base64 data URI (e.g. data:image/png;base64,...) into (mime_type, bytes).
|
|
984
|
+
fn parse_data_uri(data_uri: &str) -> Option<(String, Vec<u8>)> {
|
|
985
|
+
if !data_uri.starts_with("data:") {
|
|
986
|
+
return None;
|
|
987
|
+
}
|
|
988
|
+
let parts: Vec<&str> = data_uri.splitn(2, ',').collect();
|
|
989
|
+
if parts.len() != 2 {
|
|
990
|
+
return None;
|
|
991
|
+
}
|
|
992
|
+
let header = parts[0];
|
|
993
|
+
let b64 = parts[1];
|
|
994
|
+
|
|
995
|
+
let mime_type = header
|
|
996
|
+
.trim_start_matches("data:")
|
|
997
|
+
.split(';')
|
|
998
|
+
.next()
|
|
999
|
+
.unwrap_or("image/png")
|
|
1000
|
+
.trim()
|
|
1001
|
+
.to_string();
|
|
1002
|
+
|
|
1003
|
+
let bytes = STANDARD.decode(b64.trim()).ok()?;
|
|
1004
|
+
Some((mime_type, bytes))
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/// Build multipart body containing both text fields and binary file fields.
|
|
1008
|
+
fn build_multipart_binary(
|
|
1009
|
+
boundary: &str,
|
|
1010
|
+
text_fields: &[(String, String)],
|
|
1011
|
+
file_fields: &[(&str, &str, &str, &[u8])],
|
|
1012
|
+
) -> Vec<u8> {
|
|
1013
|
+
let mut body = Vec::new();
|
|
1014
|
+
for (name, value) in text_fields {
|
|
1015
|
+
body.extend_from_slice(format!("--{boundary}\r\n").as_bytes());
|
|
1016
|
+
body.extend_from_slice(
|
|
1017
|
+
format!("Content-Disposition: form-data; name=\"{name}\"\r\n\r\n").as_bytes(),
|
|
1018
|
+
);
|
|
1019
|
+
body.extend_from_slice(value.as_bytes());
|
|
1020
|
+
body.extend_from_slice(b"\r\n");
|
|
1021
|
+
}
|
|
1022
|
+
for (param_name, file_name, mime_type, bytes) in file_fields {
|
|
1023
|
+
body.extend_from_slice(format!("--{boundary}\r\n").as_bytes());
|
|
1024
|
+
body.extend_from_slice(
|
|
1025
|
+
format!("Content-Disposition: form-data; name=\"{param_name}\"; filename=\"{file_name}\"\r\n").as_bytes(),
|
|
1026
|
+
);
|
|
1027
|
+
body.extend_from_slice(format!("Content-Type: {mime_type}\r\n\r\n").as_bytes());
|
|
1028
|
+
body.extend_from_slice(bytes);
|
|
1029
|
+
body.extend_from_slice(b"\r\n");
|
|
1030
|
+
}
|
|
1031
|
+
body.extend_from_slice(format!("--{boundary}--\r\n").as_bytes());
|
|
1032
|
+
body
|
|
1033
|
+
}
|
|
1034
|
+
|
|
793
1035
|
/// Build the final prompt string, optionally embedding aspect ratio and negative
|
|
794
1036
|
/// prompt guidance. Used by providers that accept a plain text prompt.
|
|
795
1037
|
fn build_prompt(request: &ImageGenRequest) -> String {
|
|
@@ -832,6 +1074,9 @@ mod tests {
|
|
|
832
1074
|
num_images: None,
|
|
833
1075
|
model: None,
|
|
834
1076
|
provider: None,
|
|
1077
|
+
image_data_uri: None,
|
|
1078
|
+
mask_data_uri: None,
|
|
1079
|
+
mode: None,
|
|
835
1080
|
}
|
|
836
1081
|
}
|
|
837
1082
|
|
|
@@ -850,6 +1095,9 @@ mod tests {
|
|
|
850
1095
|
num_images: Some(2),
|
|
851
1096
|
model: None,
|
|
852
1097
|
provider: None,
|
|
1098
|
+
image_data_uri: None,
|
|
1099
|
+
mask_data_uri: None,
|
|
1100
|
+
mode: None,
|
|
853
1101
|
};
|
|
854
1102
|
let result = build_prompt(&req);
|
|
855
1103
|
assert!(result.contains("a cat"));
|
|
@@ -866,6 +1114,9 @@ mod tests {
|
|
|
866
1114
|
num_images: None,
|
|
867
1115
|
model: None,
|
|
868
1116
|
provider: None,
|
|
1117
|
+
image_data_uri: None,
|
|
1118
|
+
mask_data_uri: None,
|
|
1119
|
+
mode: None,
|
|
869
1120
|
};
|
|
870
1121
|
assert!(!build_prompt(&req).contains("aspect ratio"));
|
|
871
1122
|
}
|
|
@@ -892,4 +1143,14 @@ mod tests {
|
|
|
892
1143
|
assert_eq!(aspect_ratio_to_ideogram(Some("1:1")), "ASPECT_1_1");
|
|
893
1144
|
assert_eq!(aspect_ratio_to_ideogram(None), "ASPECT_1_1");
|
|
894
1145
|
}
|
|
1146
|
+
|
|
1147
|
+
#[test]
|
|
1148
|
+
fn parse_data_uri_valid() {
|
|
1149
|
+
let uri = "data:image/png;base64,aGVsbG8=";
|
|
1150
|
+
let parsed = parse_data_uri(uri);
|
|
1151
|
+
assert!(parsed.is_some());
|
|
1152
|
+
let (mime, bytes) = parsed.unwrap();
|
|
1153
|
+
assert_eq!(mime, "image/png");
|
|
1154
|
+
assert_eq!(bytes, b"hello");
|
|
1155
|
+
}
|
|
895
1156
|
}
|
|
@@ -20,6 +20,7 @@ pub mod skills;
|
|
|
20
20
|
pub mod symbols;
|
|
21
21
|
pub mod tasks;
|
|
22
22
|
pub mod tts;
|
|
23
|
+
pub mod video_gen;
|
|
23
24
|
pub mod weather;
|
|
24
25
|
pub mod web_search;
|
|
25
26
|
pub mod workflows;
|
|
@@ -31,8 +32,9 @@ pub use chat::{
|
|
|
31
32
|
};
|
|
32
33
|
|
|
33
34
|
pub use browser::{
|
|
34
|
-
BrowserTab, click,
|
|
35
|
-
|
|
35
|
+
BrowserTab, click, get_element_coordinates, is_browser_running, key_press, list_tabs,
|
|
36
|
+
mouse_click, mouse_move, navigate, read_page_text, screenshot, spawn_automation_browser,
|
|
37
|
+
type_text, type_text_native,
|
|
36
38
|
};
|
|
37
39
|
pub use code_tools::{
|
|
38
40
|
AppliedCodeEdit, CodeEdit, CodeEditPreview, CodeEditProposal, CodeFile, CodeInspectionError,
|
|
@@ -41,8 +43,8 @@ pub use code_tools::{
|
|
|
41
43
|
propose_code_edits, read_code_file, repository_summary, search_code,
|
|
42
44
|
};
|
|
43
45
|
pub use config::{
|
|
44
|
-
|
|
45
|
-
set_config_value,
|
|
46
|
+
AgentConfig, ConfigError, CustomProvider, CustomProviderHeader, CustomProviderModel,
|
|
47
|
+
MintConfig, config_path, initialize_config, load_config, save_config, set_config_value,
|
|
46
48
|
};
|
|
47
49
|
pub use files::{FileOperationError, PathKind, PathMatch, create_folder, find_paths};
|
|
48
50
|
pub use image_gen::{
|
|
@@ -82,6 +84,7 @@ pub use skills::{SkillError, learn_skill, learned_skills_context};
|
|
|
82
84
|
pub use symbols::{CodeSymbol, SymbolError, SymbolIndex, build_symbol_index};
|
|
83
85
|
pub use tasks::{Task, TaskError, TaskStore, tasks_path};
|
|
84
86
|
pub use tts::{TtsUrl, google_tts_urls};
|
|
87
|
+
pub use video_gen::{VideoGenError, VideoGenRequest, VideoGenResponse, generate_video};
|
|
85
88
|
pub use weather::{WeatherError, WeatherReport, weather};
|
|
86
89
|
pub use workflows::{WorkflowError, load_workflows, save_workflows, workflows_path};
|
|
87
90
|
pub mod api_server;
|
|
@@ -92,8 +95,9 @@ pub use channels::start_channels;
|
|
|
92
95
|
pub static HTTP_CLIENT: std::sync::LazyLock<reqwest::Client> =
|
|
93
96
|
std::sync::LazyLock::new(reqwest::Client::new);
|
|
94
97
|
|
|
95
|
-
pub static ACTIVE_AGENTS: std::sync::LazyLock<
|
|
96
|
-
std::sync::
|
|
98
|
+
pub static ACTIVE_AGENTS: std::sync::LazyLock<
|
|
99
|
+
std::sync::Mutex<std::collections::HashMap<String, tokio::task::AbortHandle>>,
|
|
100
|
+
> = std::sync::LazyLock::new(|| std::sync::Mutex::new(std::collections::HashMap::new()));
|
|
97
101
|
|
|
98
102
|
pub fn cancel_agent(chat_id: &str) -> bool {
|
|
99
103
|
let mut agents = ACTIVE_AGENTS.lock().unwrap();
|
|
@@ -104,4 +108,3 @@ pub fn cancel_agent(chat_id: &str) -> bool {
|
|
|
104
108
|
false
|
|
105
109
|
}
|
|
106
110
|
}
|
|
107
|
-
|