mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-31 07:45:55 -04:00
Compare commits
49
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65145f38e3 | ||
|
|
3e386a9127 | ||
|
|
036aa3545f | ||
|
|
7f5e766358 | ||
|
|
d6d5d871e3 | ||
|
|
cdd76c0106 | ||
|
|
01854c394f | ||
|
|
f9f8bba457 | ||
|
|
3e9f9caf52 | ||
|
|
bd0acd05de | ||
|
|
a28325534d | ||
|
|
9b69a92a87 | ||
|
|
1673fef867 | ||
|
|
01dcf3b985 | ||
|
|
59ff4dc9b8 | ||
|
|
ba5eca0195 | ||
|
|
a77289c971 | ||
|
|
52b89565ca | ||
|
|
742e00a052 | ||
|
|
c6515298a4 | ||
|
|
282a0e6a01 | ||
|
|
236474f6b4 | ||
|
|
255eeca554 | ||
|
|
92630dde77 | ||
|
|
0bfe0f2a2f | ||
|
|
a3bb3d44ce | ||
|
|
5f0793d41e | ||
|
|
94846b580b | ||
|
|
cf091fbc4d | ||
|
|
76156ecb8f | ||
|
|
954784d9a9 | ||
|
|
8b9d34ce2d | ||
|
|
a75052ac44 | ||
|
|
9d38b45bc2 | ||
|
|
4b5e171261 | ||
|
|
ccd7b38760 | ||
|
|
d937a9db5f | ||
|
|
13896d9306 | ||
|
|
874705f691 | ||
|
|
f192e2c077 | ||
|
|
5beac6c368 | ||
|
|
f2d327481b | ||
|
|
23084a765a | ||
|
|
9060ff8099 | ||
|
|
837a8d417a | ||
|
|
62cb63b8dc | ||
|
|
b73d0b1af6 | ||
|
|
afc9db10fe | ||
|
|
9945a825ed |
Generated
+5
@@ -11469,13 +11469,18 @@ name = "support-tool"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"artcraft_api_defs",
|
||||
"artcraft_client",
|
||||
"clap 4.5.60",
|
||||
"easyenv",
|
||||
"enums",
|
||||
"errors",
|
||||
"log",
|
||||
"seedance2pro_client",
|
||||
"serde_json",
|
||||
"tokens",
|
||||
"tokio",
|
||||
"uuid 1.22.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use cloudflare_errors::cloudflare_error::CloudflareError;
|
||||
use errors::AnyhowError;
|
||||
use reqwest::StatusCode;
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::io;
|
||||
@@ -30,6 +31,12 @@ pub enum ApiError {
|
||||
backend_hostname: Option<String>,
|
||||
},
|
||||
|
||||
/// An uncategorized bad HTTP response with status code and body.
|
||||
UncategorizedBadResponseWithStatusAndBody {
|
||||
status_code: StatusCode,
|
||||
body: String,
|
||||
},
|
||||
|
||||
/// Cloudflare errors.
|
||||
CloudflareError(CloudflareError),
|
||||
|
||||
@@ -66,6 +73,8 @@ impl Display for ApiError {
|
||||
ApiError::TooManyRequests(msg) => write!(f, "Too many requests to Storyteller backend: {}", msg),
|
||||
ApiError::InternalServerError {body, backend_hostname} =>
|
||||
write!(f, "Internal Server Error; backend hostname: {:?} ; body: {}; ", backend_hostname, body),
|
||||
ApiError::UncategorizedBadResponseWithStatusAndBody { status_code, body } =>
|
||||
write!(f, "Uncategorized bad response with status code {} and body: {}", status_code, body),
|
||||
// Server response handling errors
|
||||
ApiError::DeserializationError(error) => write!(f, "Deserialization error: {}", error),
|
||||
// Network errors
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use log::{info, warn};
|
||||
use reqwest::Client;
|
||||
use url::Url;
|
||||
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::endpoints::media_files::get_media_file::{get_media_file, GetMediaFileSuccessResponse};
|
||||
use crate::error::api_error::ApiError;
|
||||
use crate::error::client_error::ClientError;
|
||||
use crate::error::storyteller_error::StorytellerError;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
|
||||
pub struct DownloadMediaFileArgs<'a, P: AsRef<Path>> {
|
||||
pub media_token: &'a MediaFileToken,
|
||||
pub api_host: &'a ApiHost,
|
||||
pub download_path: DownloadPath<P>,
|
||||
}
|
||||
|
||||
pub enum DownloadPath<P: AsRef<Path>> {
|
||||
/// Write the file to this exact path.
|
||||
ExactFilename(P),
|
||||
/// Write the file into this directory, generating a filename from the CDN URL extension.
|
||||
Directory(P),
|
||||
}
|
||||
|
||||
pub struct DownloadMediaFileResult {
|
||||
/// The path the file was written to.
|
||||
pub downloaded_file_path: PathBuf,
|
||||
|
||||
/// The full media file response from the API.
|
||||
pub media_file_response: GetMediaFileSuccessResponse,
|
||||
|
||||
/// Size of downloaded file in bytes.
|
||||
pub filesize_bytes: usize,
|
||||
}
|
||||
|
||||
pub async fn download_media_file<P: AsRef<Path>>(
|
||||
args: DownloadMediaFileArgs<'_, P>,
|
||||
) -> Result<DownloadMediaFileResult, StorytellerError> {
|
||||
let DownloadMediaFileArgs {
|
||||
media_token,
|
||||
api_host,
|
||||
download_path
|
||||
} = args;
|
||||
|
||||
// 1. Fetch media file info from the API.
|
||||
let response = get_media_file(api_host, media_token).await?;
|
||||
let media_class = &response.media_file.media_class;
|
||||
let cdn_url = &response.media_file.media_links.cdn_url;
|
||||
|
||||
info!("Downloading media file {} of class {} from CDN: {}",
|
||||
media_token.as_str(), media_class.to_str(), cdn_url);
|
||||
|
||||
// 2. Determine the output file path.
|
||||
let output_path = match &download_path {
|
||||
DownloadPath::ExactFilename(path) => path.as_ref().to_path_buf(),
|
||||
DownloadPath::Directory(dir) => {
|
||||
let filename = derive_filename_from_url(cdn_url, &media_token);
|
||||
dir.as_ref().join(filename)
|
||||
}
|
||||
};
|
||||
|
||||
// 3. Download the bytes from the CDN.
|
||||
let bytes = download_bytes(cdn_url).await?;
|
||||
|
||||
// 4. Write to disk.
|
||||
let mut file = fs::File::create(&output_path)
|
||||
.map_err(|err| StorytellerError::Client(ClientError::IoError(err)))?;
|
||||
|
||||
file.write_all(&bytes)
|
||||
.map_err(|err| StorytellerError::Client(ClientError::IoError(err)))?;
|
||||
|
||||
file.flush()
|
||||
.map_err(|err| StorytellerError::Client(ClientError::IoError(err)))?;
|
||||
|
||||
info!("Downloaded {} bytes to {:?}", bytes.len(), output_path);
|
||||
|
||||
Ok(DownloadMediaFileResult {
|
||||
downloaded_file_path: output_path,
|
||||
media_file_response: response,
|
||||
filesize_bytes: bytes.len(),
|
||||
})
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
async fn download_bytes(url: &Url) -> Result<Vec<u8>, StorytellerError> {
|
||||
let client = Client::builder()
|
||||
.gzip(true)
|
||||
.build()
|
||||
.map_err(|err| StorytellerError::Client(ClientError::ReqwestError(err)))?;
|
||||
|
||||
let response = client.get(url.as_str())
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::OtherReqwestError(err)))?;
|
||||
|
||||
let status_code = response.status();
|
||||
|
||||
if !status_code.is_success() {
|
||||
let body = response.text().await.unwrap_or_else(|err| {
|
||||
warn!("Failed to retrieve response body: {}", err);
|
||||
"".to_string()
|
||||
});
|
||||
return Err(StorytellerError::Api(ApiError::UncategorizedBadResponseWithStatusAndBody {
|
||||
status_code,
|
||||
body,
|
||||
}));
|
||||
}
|
||||
|
||||
let bytes = response.bytes()
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Client(ClientError::ReqwestError(err)))?;
|
||||
|
||||
Ok(bytes.to_vec())
|
||||
}
|
||||
|
||||
/// Derive a filename from the CDN URL's path extension, falling back to the media token.
|
||||
fn derive_filename_from_url(url: &Url, media_token: &MediaFileToken) -> String {
|
||||
let path = url.path();
|
||||
let extension = Path::new(path)
|
||||
.extension() // NB: without dot '.'
|
||||
.and_then(|ext| ext.to_str())
|
||||
.unwrap_or("bin");
|
||||
|
||||
format!("{}.{}", media_token.as_str(), extension)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn derive_filename_from_url_extracts_extension() {
|
||||
let url = Url::parse("https://cdn.example.com/files/abc123.png").unwrap();
|
||||
let token = MediaFileToken::new_from_str("mf_test123");
|
||||
assert_eq!(derive_filename_from_url(&url, &token), "mf_test123.png");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_filename_from_url_handles_no_extension() {
|
||||
let url = Url::parse("https://cdn.example.com/files/abc123").unwrap();
|
||||
let token = MediaFileToken::new_from_str("mf_test123");
|
||||
assert_eq!(derive_filename_from_url(&url, &token), "mf_test123.bin");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_filename_from_url_handles_query_params() {
|
||||
let url = Url::parse("https://cdn.example.com/files/abc123.mp4?token=xyz").unwrap();
|
||||
let token = MediaFileToken::new_from_str("mf_test123");
|
||||
assert_eq!(derive_filename_from_url(&url, &token), "mf_test123.mp4");
|
||||
}
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
pub mod download_media_file;
|
||||
pub mod upload_media_file_from_file;
|
||||
@@ -0,0 +1,58 @@
|
||||
# support-tool
|
||||
|
||||
Production support CLI for debugging and manual operations against external services.
|
||||
|
||||
## Building & Running
|
||||
|
||||
```bash
|
||||
cargo build -p support-tool
|
||||
cargo run -p support-tool -- seedance2pro generate_video --prompt "A corgi at the lake"
|
||||
```
|
||||
|
||||
## Environment
|
||||
|
||||
Requires a `.env-support-tool-secrets` file (or env vars) with:
|
||||
|
||||
- `SEEDANCE2PRO_COOKIES` — session cookies for Kinovi/Seedance2Pro API auth
|
||||
- `ARTCRAFT_COOKIES` — session cookies for ArtCraft API auth (format: `session=...; visitor=...`)
|
||||
|
||||
## Architecture
|
||||
|
||||
- Entry point: `src/main.rs` → normalizes args (underscores optional) → dispatches
|
||||
- Top-level commands in `src/commands/run.rs` (`TopLevelCommand` enum)
|
||||
- Each top-level command has its own module with `dispatch.rs`, `state.rs`, `subcommands/`
|
||||
- Subcommands use `#[derive(Args)]` for their argument structs
|
||||
- All subcommand `run()` functions are `async fn` returning `anyhow::Result<()>`
|
||||
|
||||
## Adding a New Subcommand (to seedance2pro)
|
||||
|
||||
1. Create `src/commands/seedance2pro/subcommands/my_command.rs`
|
||||
2. Add `pub mod my_command;` to `subcommands/mod.rs`
|
||||
3. Add the name to `SUBCOMMAND_NAMES` in `dispatch.rs`
|
||||
4. Add a variant to `Seedance2proCommand` enum in `dispatch.rs`
|
||||
5. Add a match arm in `dispatch.rs::run()`
|
||||
|
||||
## Utilities
|
||||
|
||||
- `src/utils/parse_video_model.rs` — shared parser for loose video model names → `CommonVideoModel`
|
||||
- `src/utils/normalize_subcommands.rs` — underscore-insensitive arg normalizer
|
||||
|
||||
## Conventions
|
||||
|
||||
- Use `log::info!()` for status output, never `println!` (except for final results)
|
||||
- Use `anyhow::anyhow!()` for ad-hoc errors
|
||||
- Get `Seedance2ProSession` from `state.cookies` via `from_cookies_string()`
|
||||
- Get `StorytellerCredentialSet` from `state.creds` (parsed from ARTCRAFT_COOKIES)
|
||||
- External crates used: `artcraft_client` (media file download, omni API), `seedance2pro_client` (Kinovi direct), `artcraft_api_defs` (request types)
|
||||
|
||||
## Subcommands
|
||||
|
||||
### seedance2pro (direct Kinovi API)
|
||||
|
||||
- `seedance2pro find_job --token <order_id>` — search for a job across all pages
|
||||
- `seedance2pro failed_job_histogram` — histogram of failure reasons
|
||||
- `seedance2pro generate_video --prompt <text_or_file> [--model seedance2p0] [--start_frame_media_token mf_xxx] [--localhost] [--download_path /tmp/media_files]`
|
||||
|
||||
### artcraft (omni API)
|
||||
|
||||
- `artcraft generate_video --prompt <text_or_file> [--model seedance2p0] [--start_frame_media_token mf_xxx] [--duration 5] [--localhost]`
|
||||
@@ -11,8 +11,12 @@ path = "src/main.rs"
|
||||
easyenv.workspace = true
|
||||
|
||||
# Internal
|
||||
artcraft_api_defs.workspace = true
|
||||
artcraft_client.workspace = true
|
||||
enums.workspace = true
|
||||
errors.workspace = true
|
||||
seedance2pro_client.workspace = true
|
||||
tokens.workspace = true
|
||||
|
||||
# External
|
||||
anyhow.workspace = true
|
||||
@@ -20,3 +24,4 @@ clap.workspace = true
|
||||
log.workspace = true
|
||||
serde_json.workspace = true
|
||||
tokio.workspace = true
|
||||
uuid = { version = "1.8.0", features = ["v4"] }
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
use anyhow::anyhow;
|
||||
use clap::Subcommand;
|
||||
|
||||
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
|
||||
use super::state::ArtcraftState;
|
||||
use super::subcommands;
|
||||
|
||||
/// All canonical subcommand names for this module.
|
||||
/// Used by the underscore-insensitive arg normalizer.
|
||||
pub const SUBCOMMAND_NAMES: &[&str] = &["generate_video"];
|
||||
|
||||
#[derive(Subcommand)]
|
||||
#[command(rename_all = "snake_case")]
|
||||
pub enum ArtcraftCommand {
|
||||
/// Generate a video via the ArtCraft omni endpoint
|
||||
GenerateVideo(subcommands::generate_video::GenerateVideoArgs),
|
||||
}
|
||||
|
||||
pub async fn run(command: ArtcraftCommand) -> anyhow::Result<()> {
|
||||
let cookies_str = easyenv::get_env_string_required("ARTCRAFT_COOKIES")
|
||||
.map_err(|err| anyhow!("Missing ARTCRAFT_COOKIES env var: {:?}", err))?;
|
||||
|
||||
let creds = StorytellerCredentialSet::parse_multi_cookie_header(&cookies_str)
|
||||
.map_err(|err| anyhow!("Failed to parse ARTCRAFT_COOKIES: {:?}", err))?
|
||||
.ok_or_else(|| anyhow!("ARTCRAFT_COOKIES contained no recognized cookies (expected session=... and/or visitor=...)"))?;
|
||||
|
||||
let state = ArtcraftState { creds };
|
||||
|
||||
match command {
|
||||
ArtcraftCommand::GenerateVideo(args) => subcommands::generate_video::run(&state, args).await,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
pub mod dispatch;
|
||||
mod state;
|
||||
mod subcommands;
|
||||
|
||||
pub use dispatch::ArtcraftCommand;
|
||||
pub use dispatch::run;
|
||||
@@ -0,0 +1,5 @@
|
||||
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
|
||||
pub struct ArtcraftState {
|
||||
pub creds: StorytellerCredentialSet,
|
||||
}
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use clap::Args;
|
||||
use log::info;
|
||||
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
use artcraft_client::endpoints::omni_gen::generate::video::omni_gen_video::omni_gen_video_generate;
|
||||
use artcraft_client::utils::api_host::ApiHost;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::utils::parse_video_model::parse_video_model;
|
||||
use super::super::state::ArtcraftState;
|
||||
|
||||
// ── Args ──
|
||||
|
||||
#[derive(Args)]
|
||||
#[command(
|
||||
override_usage = "support-tool artcraft generate_video [OPTIONS] --prompt <PROMPT>",
|
||||
after_help = "\
|
||||
EXAMPLES:
|
||||
support-tool artcraft generate_video --prompt \"A corgi at the lake\" --model seedance2
|
||||
support-tool artcraft generate_video --prompt prompt.txt --model veo3
|
||||
support-tool artcraft generate_video --prompt \"Dancing\" --start_frame_media_token mf_abc123
|
||||
support-tool artcraft generate_video --prompt \"Cat\" --image_reference_tokens \"mf_abc,mf_def\" --model happyhorse
|
||||
",
|
||||
)]
|
||||
pub struct GenerateVideoArgs {
|
||||
/// A prompt string or path to a file containing the prompt (.txt or .md).
|
||||
#[arg(long)]
|
||||
pub prompt: String,
|
||||
|
||||
/// A single media token for the start frame image.
|
||||
#[arg(long)]
|
||||
pub start_frame_media_token: Option<String>,
|
||||
|
||||
/// A single media token for the end frame image.
|
||||
#[arg(long)]
|
||||
pub end_frame_media_token: Option<String>,
|
||||
|
||||
/// Comma or space separated media tokens for image references.
|
||||
#[arg(long)]
|
||||
pub image_reference_tokens: Option<String>,
|
||||
|
||||
/// Comma or space separated media tokens for video references.
|
||||
#[arg(long)]
|
||||
pub video_reference_tokens: Option<String>,
|
||||
|
||||
/// Comma or space separated media tokens for audio references.
|
||||
#[arg(long)]
|
||||
pub audio_reference_tokens: Option<String>,
|
||||
|
||||
/// Use localhost:12345 instead of production API.
|
||||
#[arg(long)]
|
||||
pub localhost: bool,
|
||||
|
||||
/// Video model to use. Accepts canonical names (eg. "seedance_2p0") or aliases
|
||||
/// (eg. "seedance2", "happyhorse", "veo3fast"). [default: seedance2p0]
|
||||
#[arg(long)]
|
||||
pub model: Option<String>,
|
||||
|
||||
/// Duration in seconds. [default: 5]
|
||||
#[arg(long)]
|
||||
pub duration: Option<u16>,
|
||||
|
||||
/// Idempotency token (auto-generated if not specified).
|
||||
#[arg(long)]
|
||||
pub idempotency_token: Option<String>,
|
||||
}
|
||||
|
||||
// ── Entry point ──
|
||||
|
||||
pub async fn run(state: &ArtcraftState, args: GenerateVideoArgs) -> anyhow::Result<()> {
|
||||
let model_str = args.model.as_deref().unwrap_or("seedance2p0");
|
||||
let model = parse_video_model(model_str)
|
||||
.ok_or_else(|| anyhow!("Unknown model '{}'. Try --help for supported names.", model_str))?;
|
||||
|
||||
let prompt = resolve_prompt(&args.prompt)?;
|
||||
let api_host = if args.localhost { ApiHost::Localhost { port: 12345 } } else { ApiHost::Storyteller };
|
||||
let duration = args.duration.unwrap_or(5);
|
||||
|
||||
info!("Model: {:?}", model);
|
||||
info!("Prompt: {:?}", prompt);
|
||||
info!("Duration: {}s", duration);
|
||||
|
||||
// Parse media tokens.
|
||||
let start_frame_token = args.start_frame_media_token.as_deref()
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| MediaFileToken::new_from_str(s));
|
||||
|
||||
let end_frame_token = args.end_frame_media_token.as_deref()
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| MediaFileToken::new_from_str(s));
|
||||
|
||||
let image_ref_tokens = parse_media_tokens(args.image_reference_tokens.as_deref());
|
||||
let video_ref_tokens = parse_media_tokens(args.video_reference_tokens.as_deref());
|
||||
let audio_ref_tokens = parse_media_tokens(args.audio_reference_tokens.as_deref());
|
||||
|
||||
if let Some(t) = &start_frame_token { info!("Start frame: {}", t.as_str()); }
|
||||
if let Some(t) = &end_frame_token { info!("End frame: {}", t.as_str()); }
|
||||
if !image_ref_tokens.is_empty() { info!("Image refs: {:?}", image_ref_tokens.iter().map(|t| t.as_str()).collect::<Vec<_>>()); }
|
||||
if !video_ref_tokens.is_empty() { info!("Video refs: {:?}", video_ref_tokens.iter().map(|t| t.as_str()).collect::<Vec<_>>()); }
|
||||
if !audio_ref_tokens.is_empty() { info!("Audio refs: {:?}", audio_ref_tokens.iter().map(|t| t.as_str()).collect::<Vec<_>>()); }
|
||||
|
||||
// Build the omni request. Media tokens are already uploaded — no download/re-upload needed.
|
||||
let idempotency_token = args.idempotency_token
|
||||
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
|
||||
|
||||
let request = OmniGenVideoCostAndGenerateRequest {
|
||||
idempotency_token: Some(idempotency_token.clone()),
|
||||
model: Some(model),
|
||||
prompt: Some(prompt),
|
||||
negative_prompt: None,
|
||||
start_frame_image_media_token: start_frame_token,
|
||||
end_frame_image_media_token: end_frame_token,
|
||||
reference_image_media_tokens: if image_ref_tokens.is_empty() { None } else { Some(image_ref_tokens) },
|
||||
reference_video_media_tokens: if video_ref_tokens.is_empty() { None } else { Some(video_ref_tokens) },
|
||||
reference_audio_media_tokens: if audio_ref_tokens.is_empty() { None } else { Some(audio_ref_tokens) },
|
||||
reference_character_tokens: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
duration_seconds: Some(duration),
|
||||
video_batch_count: None,
|
||||
generate_audio: None,
|
||||
};
|
||||
|
||||
info!("Sending request to omni endpoint (idempotency_token={})...", idempotency_token);
|
||||
|
||||
let response = omni_gen_video_generate(
|
||||
&api_host,
|
||||
Some(&state.creds),
|
||||
request,
|
||||
).await.map_err(|err| anyhow!("Omni video generation failed: {}", err))?;
|
||||
|
||||
info!("Success!");
|
||||
info!("Primary job token: {}", response.inference_job_token.as_str());
|
||||
info!("All job tokens: {:?}", response.all_job_tokens.iter().map(|t| t.as_str()).collect::<Vec<_>>());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn resolve_prompt(input: &str) -> anyhow::Result<String> {
|
||||
let looks_like_file = input.ends_with(".txt") || input.ends_with(".md");
|
||||
|
||||
if looks_like_file {
|
||||
let path = Path::new(input);
|
||||
if !path.exists() {
|
||||
return Err(anyhow!("Prompt file does not exist: {:?}", path));
|
||||
}
|
||||
let content = fs::read_to_string(path)
|
||||
.map_err(|err| anyhow!("Failed to read prompt file {:?}: {}", path, err))?;
|
||||
let trimmed = content.trim().to_string();
|
||||
info!("Read prompt from file {:?} ({} chars)", path, trimmed.len());
|
||||
Ok(trimmed)
|
||||
} else {
|
||||
info!("Using prompt string directly ({} chars)", input.len());
|
||||
Ok(input.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_media_tokens(input: Option<&str>) -> Vec<MediaFileToken> {
|
||||
let input = match input {
|
||||
Some(s) if !s.trim().is_empty() => s,
|
||||
_ => return Vec::new(),
|
||||
};
|
||||
|
||||
input
|
||||
.split(|c: char| c == ',' || c.is_whitespace())
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| MediaFileToken::new_from_str(s))
|
||||
.collect()
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod generate_video;
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod artcraft;
|
||||
pub mod run;
|
||||
pub mod seedance2pro;
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
use super::artcraft;
|
||||
use super::seedance2pro;
|
||||
|
||||
/// All canonical subcommand names across all modules.
|
||||
/// Used by the underscore-insensitive arg normalizer.
|
||||
pub fn all_canonical_names() -> Vec<&'static str> {
|
||||
let mut names: Vec<&str> = vec!["seedance2pro"];
|
||||
let mut names: Vec<&str> = vec!["seedance2pro", "artcraft"];
|
||||
names.extend_from_slice(seedance2pro::dispatch::SUBCOMMAND_NAMES);
|
||||
names.extend_from_slice(artcraft::dispatch::SUBCOMMAND_NAMES);
|
||||
names
|
||||
}
|
||||
|
||||
@@ -20,11 +22,17 @@ pub struct Cli {
|
||||
#[derive(Subcommand)]
|
||||
#[command(rename_all = "snake_case")]
|
||||
pub enum TopLevelCommand {
|
||||
/// Seedance2 Pro support commands
|
||||
/// Seedance2 Pro support commands (direct Kinovi API)
|
||||
Seedance2pro {
|
||||
#[command(subcommand)]
|
||||
command: seedance2pro::Seedance2proCommand,
|
||||
},
|
||||
|
||||
/// ArtCraft support commands (omni API)
|
||||
Artcraft {
|
||||
#[command(subcommand)]
|
||||
command: artcraft::ArtcraftCommand,
|
||||
},
|
||||
}
|
||||
|
||||
pub async fn run(cli: Cli) -> anyhow::Result<()> {
|
||||
@@ -32,5 +40,8 @@ pub async fn run(cli: Cli) -> anyhow::Result<()> {
|
||||
TopLevelCommand::Seedance2pro { command } => {
|
||||
seedance2pro::run(command).await
|
||||
}
|
||||
TopLevelCommand::Artcraft { command } => {
|
||||
artcraft::run(command).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,11 @@ use super::subcommands;
|
||||
|
||||
/// All canonical subcommand names for this module.
|
||||
/// Used by the underscore-insensitive arg normalizer.
|
||||
pub const SUBCOMMAND_NAMES: &[&str] = &["find_job", "failed_job_histogram"];
|
||||
pub const SUBCOMMAND_NAMES: &[&str] = &[
|
||||
"failed_job_histogram",
|
||||
"find_job",
|
||||
"generate_video",
|
||||
];
|
||||
|
||||
#[derive(Subcommand)]
|
||||
#[command(rename_all = "snake_case")]
|
||||
@@ -16,6 +20,9 @@ pub enum Seedance2proCommand {
|
||||
|
||||
/// Scan all jobs and print a histogram of failure reasons
|
||||
FailedJobHistogram,
|
||||
|
||||
/// Generate a video via Seedance2Pro/Kinovi directly
|
||||
GenerateVideo(subcommands::generate_video::GenerateVideoArgs),
|
||||
}
|
||||
|
||||
pub async fn run(command: Seedance2proCommand) -> anyhow::Result<()> {
|
||||
@@ -27,5 +34,6 @@ pub async fn run(command: Seedance2proCommand) -> anyhow::Result<()> {
|
||||
match command {
|
||||
Seedance2proCommand::FindJob(args) => subcommands::find_job::run(&state, args).await,
|
||||
Seedance2proCommand::FailedJobHistogram => subcommands::failed_job_histogram::run(&state).await,
|
||||
Seedance2proCommand::GenerateVideo(args) => subcommands::generate_video::run(&state, args).await,
|
||||
}
|
||||
}
|
||||
|
||||
+475
@@ -0,0 +1,475 @@
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use clap::Args;
|
||||
use log::info;
|
||||
|
||||
use artcraft_client::recipes::download_media_file::{
|
||||
download_media_file, DownloadMediaFileArgs, DownloadPath,
|
||||
};
|
||||
use artcraft_client::utils::api_host::ApiHost;
|
||||
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use seedance2pro_client::generate::video::generate_happy_horse_1p0::{
|
||||
generate_happy_horse_1p0, GenerateHappyHorse1p0Args, GenerateHappyHorse1p0Request,
|
||||
};
|
||||
use seedance2pro_client::generate::video::generate_seedance_2p0::{
|
||||
generate_seedance_2p0, GenerateSeedance2p0Args, GenerateSeedance2p0Request,
|
||||
};
|
||||
use seedance2pro_client::generate::video::generate_seedance_2p0_fast::{
|
||||
generate_seedance_2p0_fast, GenerateSeedance2p0FastArgs, GenerateSeedance2p0FastRequest,
|
||||
};
|
||||
use seedance2pro_client::requests::prepare_file_upload::prepare_file_upload::{
|
||||
prepare_file_upload, PrepareFileUploadArgs,
|
||||
};
|
||||
use seedance2pro_client::requests::upload_file::upload_file::{upload_file, UploadFileArgs};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use super::super::state::Seedance2ProState;
|
||||
|
||||
const DEFAULT_DOWNLOAD_PATH: &str = "/tmp/media_files";
|
||||
|
||||
// ── Args ──
|
||||
|
||||
#[derive(Args)]
|
||||
#[command(
|
||||
override_usage = "support-tool seedance2pro generate_video [OPTIONS] --prompt <PROMPT>",
|
||||
after_help = "\
|
||||
EXAMPLES:
|
||||
support-tool seedance2pro generate_video --prompt \"A corgi at the lake\"
|
||||
support-tool seedance2pro generate_video --prompt prompt.txt --model happyhorse
|
||||
support-tool seedance2pro generate_video --prompt \"Dancing\" --start_frame_media_token mf_abc123
|
||||
support-tool seedance2pro generate_video --prompt \"Cat\" --image_reference_tokens \"mf_abc,mf_def\"
|
||||
",
|
||||
)]
|
||||
pub struct GenerateVideoArgs {
|
||||
/// A prompt string or path to a file containing the prompt (.txt or .md).
|
||||
#[arg(long)]
|
||||
pub prompt: String,
|
||||
|
||||
/// A single media token for the start frame image.
|
||||
#[arg(long)]
|
||||
pub start_frame_media_token: Option<String>,
|
||||
|
||||
/// A single media token for the end frame image.
|
||||
#[arg(long)]
|
||||
pub end_frame_media_token: Option<String>,
|
||||
|
||||
/// Comma or space separated media tokens for image references.
|
||||
#[arg(long)]
|
||||
pub image_reference_tokens: Option<String>,
|
||||
|
||||
/// Comma or space separated media tokens for video references.
|
||||
#[arg(long)]
|
||||
pub video_reference_tokens: Option<String>,
|
||||
|
||||
/// Comma or space separated media tokens for audio references.
|
||||
#[arg(long)]
|
||||
pub audio_reference_tokens: Option<String>,
|
||||
|
||||
/// Use localhost:12345 instead of production API for artcraft_client lookups.
|
||||
#[arg(long)]
|
||||
pub localhost: bool,
|
||||
|
||||
/// Directory to cache downloaded media files. [default: /tmp/media_files]
|
||||
#[arg(long)]
|
||||
pub download_path: Option<String>,
|
||||
|
||||
/// Model: seedance2p0, seedance2p0fast, happyhorse (and underscore variants).
|
||||
/// [default: seedance2p0]
|
||||
#[arg(long)]
|
||||
pub model: Option<String>,
|
||||
}
|
||||
|
||||
// ── Model enum ──
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum VideoModel {
|
||||
Seedance2p0,
|
||||
Seedance2p0Fast,
|
||||
HappyHorse,
|
||||
}
|
||||
|
||||
fn parse_model(value: &str) -> anyhow::Result<VideoModel> {
|
||||
match value.to_lowercase().as_str() {
|
||||
"seedance2" | "seedance2p0" | "seedance_2p0" => Ok(VideoModel::Seedance2p0),
|
||||
"seedance2fast" | "seedance2p0fast" | "seedance_2p0_fast" => Ok(VideoModel::Seedance2p0Fast),
|
||||
"happyhorse" | "happy_horse" => Ok(VideoModel::HappyHorse),
|
||||
other => Err(anyhow!(
|
||||
"Unknown model '{}'. Valid: seedance2, seedance2p0, seedance_2p0, \
|
||||
seedance2fast, seedance2p0fast, seedance_2p0_fast, happyhorse, happy_horse",
|
||||
other
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Resolved media inputs ──
|
||||
|
||||
struct ResolvedMediaInputs {
|
||||
start_frame_url: Option<String>,
|
||||
end_frame_url: Option<String>,
|
||||
reference_image_urls: Option<Vec<String>>,
|
||||
reference_video_urls: Option<Vec<String>>,
|
||||
reference_audio_urls: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
// ── Entry point ──
|
||||
|
||||
pub async fn run(state: &Seedance2ProState, args: GenerateVideoArgs) -> anyhow::Result<()> {
|
||||
let session = Seedance2ProSession::from_cookies_string(state.cookies.clone());
|
||||
let model = parse_model(args.model.as_deref().unwrap_or("seedance2p0"))?;
|
||||
let prompt = resolve_prompt(&args.prompt)?;
|
||||
let api_host = if args.localhost { ApiHost::Localhost { port: 12345 } } else { ApiHost::Storyteller };
|
||||
|
||||
info!("Model: {:?}", model);
|
||||
info!("Prompt: {:?}", prompt);
|
||||
|
||||
// Resolve download path.
|
||||
let download_dir = resolve_download_dir(args.download_path.as_deref())?;
|
||||
info!("Download cache directory: {:?}", download_dir);
|
||||
|
||||
// Parse media tokens.
|
||||
let start_frame_token = args.start_frame_media_token.as_deref()
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| MediaFileToken::new_from_str(s));
|
||||
|
||||
let end_frame_token = args.end_frame_media_token.as_deref()
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| MediaFileToken::new_from_str(s));
|
||||
|
||||
let image_ref_tokens = parse_media_tokens(args.image_reference_tokens.as_deref());
|
||||
let video_ref_tokens = parse_media_tokens(args.video_reference_tokens.as_deref());
|
||||
let audio_ref_tokens = parse_media_tokens(args.audio_reference_tokens.as_deref());
|
||||
|
||||
// Collect all tokens that need downloading.
|
||||
let mut all_tokens: Vec<MediaFileToken> = Vec::new();
|
||||
if let Some(t) = &start_frame_token { all_tokens.push(t.clone()); }
|
||||
if let Some(t) = &end_frame_token { all_tokens.push(t.clone()); }
|
||||
all_tokens.extend(image_ref_tokens.iter().cloned());
|
||||
all_tokens.extend(video_ref_tokens.iter().cloned());
|
||||
all_tokens.extend(audio_ref_tokens.iter().cloned());
|
||||
|
||||
info!("Total media tokens to process: {}", all_tokens.len());
|
||||
|
||||
// Download media files (with caching).
|
||||
let downloaded_files = download_media_files(&all_tokens, &api_host, &download_dir).await?;
|
||||
|
||||
// Upload media files to Kinovi/Seedance2Pro.
|
||||
let uploaded_urls = upload_media_files_to_kinovi(&session, &downloaded_files).await?;
|
||||
|
||||
// Map uploaded URLs back to their roles.
|
||||
let mut url_iter = uploaded_urls.into_iter();
|
||||
|
||||
let start_frame_url = if start_frame_token.is_some() { url_iter.next() } else { None };
|
||||
let end_frame_url = if end_frame_token.is_some() { url_iter.next() } else { None };
|
||||
|
||||
let reference_image_urls: Vec<String> = url_iter.by_ref().take(image_ref_tokens.len()).collect();
|
||||
let reference_video_urls: Vec<String> = url_iter.by_ref().take(video_ref_tokens.len()).collect();
|
||||
let reference_audio_urls: Vec<String> = url_iter.by_ref().take(audio_ref_tokens.len()).collect();
|
||||
|
||||
let media_inputs = ResolvedMediaInputs {
|
||||
start_frame_url,
|
||||
end_frame_url,
|
||||
reference_image_urls: if reference_image_urls.is_empty() { None } else { Some(reference_image_urls) },
|
||||
reference_video_urls: if reference_video_urls.is_empty() { None } else { Some(reference_video_urls) },
|
||||
reference_audio_urls: if reference_audio_urls.is_empty() { None } else { Some(reference_audio_urls) },
|
||||
};
|
||||
|
||||
// Generate video.
|
||||
generate(model, &session, &prompt, media_inputs).await
|
||||
}
|
||||
|
||||
// ── Prompt resolution ──
|
||||
|
||||
fn resolve_prompt(input: &str) -> anyhow::Result<String> {
|
||||
let looks_like_file = input.ends_with(".txt") || input.ends_with(".md");
|
||||
|
||||
if looks_like_file {
|
||||
let path = Path::new(input);
|
||||
if !path.exists() {
|
||||
return Err(anyhow!("Prompt file does not exist: {:?}", path));
|
||||
}
|
||||
let content = fs::read_to_string(path)
|
||||
.map_err(|err| anyhow!("Failed to read prompt file {:?}: {}", path, err))?;
|
||||
let trimmed = content.trim().to_string();
|
||||
info!("Read prompt from file {:?} ({} chars)", path, trimmed.len());
|
||||
Ok(trimmed)
|
||||
} else {
|
||||
info!("Using prompt string directly ({} chars)", input.len());
|
||||
Ok(input.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
// ── Download path resolution ──
|
||||
|
||||
fn resolve_download_dir(maybe_path: Option<&str>) -> anyhow::Result<PathBuf> {
|
||||
let dir = match maybe_path {
|
||||
Some(path) => {
|
||||
let p = PathBuf::from(path);
|
||||
if p.exists() && !p.is_dir() {
|
||||
return Err(anyhow!("--download_path {:?} exists but is not a directory", p));
|
||||
}
|
||||
p
|
||||
}
|
||||
None => PathBuf::from(DEFAULT_DOWNLOAD_PATH),
|
||||
};
|
||||
|
||||
if !dir.exists() {
|
||||
info!("Creating download directory: {:?}", dir);
|
||||
fs::create_dir_all(&dir)
|
||||
.map_err(|err| anyhow!("Failed to create download directory {:?}: {}", dir, err))?;
|
||||
}
|
||||
|
||||
Ok(dir)
|
||||
}
|
||||
|
||||
// ── Media token parsing ──
|
||||
|
||||
fn parse_media_tokens(input: Option<&str>) -> Vec<MediaFileToken> {
|
||||
let input = match input {
|
||||
Some(s) if !s.trim().is_empty() => s,
|
||||
_ => return Vec::new(),
|
||||
};
|
||||
|
||||
input
|
||||
.split(|c: char| c == ',' || c.is_whitespace())
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| MediaFileToken::new_from_str(s))
|
||||
.collect()
|
||||
}
|
||||
|
||||
// ── Download media files with caching ──
|
||||
|
||||
struct DownloadedFile {
|
||||
path: PathBuf,
|
||||
extension: String,
|
||||
}
|
||||
|
||||
async fn download_media_files(
|
||||
tokens: &[MediaFileToken],
|
||||
api_host: &ApiHost,
|
||||
download_dir: &Path,
|
||||
) -> anyhow::Result<Vec<DownloadedFile>> {
|
||||
let mut results = Vec::new();
|
||||
|
||||
for token in tokens {
|
||||
// Check if file already exists in cache.
|
||||
let maybe_cached = find_cached_file(download_dir, token);
|
||||
|
||||
if let Some(cached) = maybe_cached {
|
||||
info!(
|
||||
"Cache hit: {} already exists at {:?} ({} bytes)",
|
||||
token.as_str(),
|
||||
cached.path,
|
||||
fs::metadata(&cached.path).map(|m| m.len()).unwrap_or(0),
|
||||
);
|
||||
results.push(cached);
|
||||
continue;
|
||||
}
|
||||
|
||||
info!("Downloading {} from API...", token.as_str());
|
||||
|
||||
let result = download_media_file(DownloadMediaFileArgs {
|
||||
media_token: token,
|
||||
api_host,
|
||||
download_path: DownloadPath::Directory(download_dir),
|
||||
}).await.map_err(|err| anyhow!("Failed to download {}: {}", token.as_str(), err))?;
|
||||
|
||||
let extension = result.downloaded_file_path
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.unwrap_or("bin")
|
||||
.to_string();
|
||||
|
||||
info!(
|
||||
"Downloaded: {} -> {:?} ({} bytes, class: {})",
|
||||
token.as_str(),
|
||||
result.downloaded_file_path,
|
||||
result.filesize_bytes,
|
||||
result.media_file_response.media_file.media_class.to_str(),
|
||||
);
|
||||
|
||||
results.push(DownloadedFile {
|
||||
path: result.downloaded_file_path,
|
||||
extension,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Look for an existing cached file matching the token (any extension).
|
||||
fn find_cached_file(download_dir: &Path, token: &MediaFileToken) -> Option<DownloadedFile> {
|
||||
let prefix = format!("{}.", token.as_str());
|
||||
|
||||
let entries = fs::read_dir(download_dir).ok()?;
|
||||
for entry in entries.flatten() {
|
||||
let filename = entry.file_name();
|
||||
let filename_str = filename.to_string_lossy();
|
||||
if filename_str.starts_with(&prefix) {
|
||||
let path = entry.path();
|
||||
// Skip zero-byte files — treat as incomplete downloads.
|
||||
let metadata = fs::metadata(&path).ok()?;
|
||||
if metadata.len() == 0 {
|
||||
continue;
|
||||
}
|
||||
let extension = path.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.unwrap_or("bin")
|
||||
.to_string();
|
||||
return Some(DownloadedFile { path, extension });
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
// ── Upload to Kinovi/Seedance2Pro ──
|
||||
|
||||
async fn upload_media_files_to_kinovi(
|
||||
session: &Seedance2ProSession,
|
||||
files: &[DownloadedFile],
|
||||
) -> anyhow::Result<Vec<String>> {
|
||||
let mut urls = Vec::new();
|
||||
|
||||
for file in files {
|
||||
info!("Uploading {:?} to Kinovi (extension: {})...", file.path, file.extension);
|
||||
|
||||
// 1. Prepare upload (get signed URL).
|
||||
let prepare_result = prepare_file_upload(PrepareFileUploadArgs {
|
||||
session,
|
||||
extension: file.extension.clone(),
|
||||
host_override: None,
|
||||
}).await.map_err(|err| anyhow!("Failed to prepare upload for {:?}: {:?}", file.path, err))?;
|
||||
|
||||
// 2. Read file bytes.
|
||||
let file_bytes = fs::read(&file.path)
|
||||
.map_err(|err| anyhow!("Failed to read {:?}: {}", file.path, err))?;
|
||||
|
||||
info!("Uploading {} bytes to signed URL...", file_bytes.len());
|
||||
|
||||
// 3. Upload to Cloudflare R2.
|
||||
let upload_result = upload_file(UploadFileArgs {
|
||||
upload_url: prepare_result.upload_url,
|
||||
file_bytes,
|
||||
host_override: None,
|
||||
}).await.map_err(|err| anyhow!("Failed to upload {:?}: {:?}", file.path, err))?;
|
||||
|
||||
info!("Uploaded: {:?} -> {}", file.path, upload_result.public_url);
|
||||
urls.push(upload_result.public_url);
|
||||
}
|
||||
|
||||
Ok(urls)
|
||||
}
|
||||
|
||||
// ── Generate video ──
|
||||
|
||||
async fn generate(
|
||||
model: VideoModel,
|
||||
session: &Seedance2ProSession,
|
||||
prompt: &str,
|
||||
media: ResolvedMediaInputs,
|
||||
) -> anyhow::Result<()> {
|
||||
match model {
|
||||
VideoModel::HappyHorse => {
|
||||
// Happy Horse only supports start_frame_url.
|
||||
if media.end_frame_url.is_some() {
|
||||
return Err(anyhow!("Happy Horse does not support --end_frame_media_token"));
|
||||
}
|
||||
if media.reference_image_urls.is_some() {
|
||||
return Err(anyhow!("Happy Horse does not support --image_reference_tokens"));
|
||||
}
|
||||
if media.reference_video_urls.is_some() {
|
||||
return Err(anyhow!("Happy Horse does not support --video_reference_tokens"));
|
||||
}
|
||||
if media.reference_audio_urls.is_some() {
|
||||
return Err(anyhow!("Happy Horse does not support --audio_reference_tokens"));
|
||||
}
|
||||
|
||||
info!("Generating video with Happy Horse 1.0...");
|
||||
let result = generate_happy_horse_1p0(GenerateHappyHorse1p0Args {
|
||||
request: GenerateHappyHorse1p0Request {
|
||||
prompt: prompt.to_string(),
|
||||
aspect_ratio: None,
|
||||
output_resolution: None,
|
||||
batch_count: None,
|
||||
duration_seconds: 5,
|
||||
start_frame_url: media.start_frame_url,
|
||||
},
|
||||
session,
|
||||
host_override: None,
|
||||
}).await.map_err(|err| anyhow!("Happy Horse generation failed: {:?}", err))?;
|
||||
|
||||
info!("Success! task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
if let Some(task_ids) = &result.task_ids {
|
||||
info!("All task_ids: {:?}", task_ids);
|
||||
}
|
||||
if let Some(order_ids) = &result.order_ids {
|
||||
info!("All order_ids: {:?}", order_ids);
|
||||
}
|
||||
}
|
||||
VideoModel::Seedance2p0 => {
|
||||
info!("Generating video with Seedance 2.0 Pro...");
|
||||
let result = generate_seedance_2p0(GenerateSeedance2p0Args {
|
||||
request: GenerateSeedance2p0Request {
|
||||
prompt: prompt.to_string(),
|
||||
aspect_ratio: None,
|
||||
output_resolution: None,
|
||||
duration_seconds: 5,
|
||||
batch_count: None,
|
||||
start_frame_url: media.start_frame_url,
|
||||
end_frame_url: media.end_frame_url,
|
||||
reference_image_urls: media.reference_image_urls,
|
||||
reference_video_urls: media.reference_video_urls,
|
||||
reference_audio_urls: media.reference_audio_urls,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
},
|
||||
session,
|
||||
host_override: None,
|
||||
}).await.map_err(|err| anyhow!("Seedance 2.0 generation failed: {:?}", err))?;
|
||||
|
||||
info!("Success! task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
if let Some(task_ids) = &result.task_ids {
|
||||
info!("All task_ids: {:?}", task_ids);
|
||||
}
|
||||
if let Some(order_ids) = &result.order_ids {
|
||||
info!("All order_ids: {:?}", order_ids);
|
||||
}
|
||||
}
|
||||
VideoModel::Seedance2p0Fast => {
|
||||
info!("Generating video with Seedance 2.0 Fast...");
|
||||
let result = generate_seedance_2p0_fast(GenerateSeedance2p0FastArgs {
|
||||
request: GenerateSeedance2p0FastRequest {
|
||||
prompt: prompt.to_string(),
|
||||
aspect_ratio: None,
|
||||
output_resolution: None,
|
||||
duration_seconds: 5,
|
||||
batch_count: None,
|
||||
start_frame_url: media.start_frame_url,
|
||||
end_frame_url: media.end_frame_url,
|
||||
reference_image_urls: media.reference_image_urls,
|
||||
reference_video_urls: media.reference_video_urls,
|
||||
reference_audio_urls: media.reference_audio_urls,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
},
|
||||
session,
|
||||
host_override: None,
|
||||
}).await.map_err(|err| anyhow!("Seedance 2.0 Fast generation failed: {:?}", err))?;
|
||||
|
||||
info!("Success! task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
if let Some(task_ids) = &result.task_ids {
|
||||
info!("All task_ids: {:?}", task_ids);
|
||||
}
|
||||
if let Some(order_ids) = &result.order_ids {
|
||||
info!("All order_ids: {:?}", order_ids);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod failed_job_histogram;
|
||||
pub mod find_job;
|
||||
pub mod generate_video;
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pub mod normalize_subcommands;
|
||||
pub mod parse_video_model;
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
|
||||
/// Parse a loose model name into a CommonVideoModel.
|
||||
///
|
||||
/// Accepts the canonical serde names (eg. "seedance_2p0") as well as
|
||||
/// human-friendly aliases (eg. "seedance2", "happyhorse", "veo3fast").
|
||||
pub fn parse_video_model(value: &str) -> Option<CommonVideoModel> {
|
||||
let value = value.trim();
|
||||
|
||||
// Try canonical serde deserialization first (eg. "seedance_2p0", "happy_horse_1p0").
|
||||
if let Ok(model) = serde_json::from_str::<CommonVideoModel>(&format!("\"{}\"", value)) {
|
||||
return Some(model);
|
||||
}
|
||||
|
||||
// Fallback: human-friendly aliases.
|
||||
match value.to_lowercase().replace('-', "_").as_str() {
|
||||
// Seedance family
|
||||
"seedance2" | "seedance2p0" | "seedance_2p0" => Some(CommonVideoModel::Seedance2p0),
|
||||
"seedance2fast" | "seedance2p0fast" | "seedance_2p0_fast" => Some(CommonVideoModel::Seedance2p0Fast),
|
||||
"seedance1" | "seedance10lite" | "seedance_1p0_lite" => Some(CommonVideoModel::Seedance10Lite),
|
||||
"seedance15" | "seedance1p5" | "seedance_1p5_pro" => Some(CommonVideoModel::Seedance1p5Pro),
|
||||
|
||||
// Happy Horse
|
||||
"happyhorse" | "happy_horse" | "happyhorse1" | "happy_horse_1p0" => Some(CommonVideoModel::HappyHorse1p0),
|
||||
|
||||
// Kling family
|
||||
"kling16" | "kling_1p6_pro" => Some(CommonVideoModel::Kling16Pro),
|
||||
"kling21" | "kling_2p1_pro" => Some(CommonVideoModel::Kling21Pro),
|
||||
"kling21master" | "kling_2p1_master" => Some(CommonVideoModel::Kling21Master),
|
||||
"kling25turbo" | "kling_2p5_turbo_pro" => Some(CommonVideoModel::Kling2p5TurboPro),
|
||||
"kling26" | "kling_2p6_pro" => Some(CommonVideoModel::Kling2p6Pro),
|
||||
"kling3" | "kling30" | "kling_3p0_standard" => Some(CommonVideoModel::Kling3p0Standard),
|
||||
"kling3pro" | "kling30pro" | "kling_3p0_pro" => Some(CommonVideoModel::Kling3p0Pro),
|
||||
|
||||
// Veo family
|
||||
"veo2" | "veo_2" => Some(CommonVideoModel::Veo2),
|
||||
"veo3" | "veo_3" => Some(CommonVideoModel::Veo3),
|
||||
"veo3fast" | "veo_3_fast" => Some(CommonVideoModel::Veo3Fast),
|
||||
"veo31" | "veo3p1" | "veo_3p1" => Some(CommonVideoModel::Veo3p1),
|
||||
"veo31fast" | "veo3p1fast" | "veo_3p1_fast" => Some(CommonVideoModel::Veo3p1Fast),
|
||||
|
||||
// Sora family
|
||||
"sora2" | "sora_2" => Some(CommonVideoModel::Sora2),
|
||||
"sora2pro" | "sora_2_pro" => Some(CommonVideoModel::Sora2Pro),
|
||||
|
||||
// Grok
|
||||
"grok" | "grok_video" | "grokvideo" => Some(CommonVideoModel::GrokVideo),
|
||||
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn canonical_names() {
|
||||
assert_eq!(parse_video_model("seedance_2p0"), Some(CommonVideoModel::Seedance2p0));
|
||||
assert_eq!(parse_video_model("happy_horse_1p0"), Some(CommonVideoModel::HappyHorse1p0));
|
||||
assert_eq!(parse_video_model("veo_3"), Some(CommonVideoModel::Veo3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aliases() {
|
||||
assert_eq!(parse_video_model("seedance2"), Some(CommonVideoModel::Seedance2p0));
|
||||
assert_eq!(parse_video_model("seedance2fast"), Some(CommonVideoModel::Seedance2p0Fast));
|
||||
assert_eq!(parse_video_model("happyhorse"), Some(CommonVideoModel::HappyHorse1p0));
|
||||
assert_eq!(parse_video_model("veo3fast"), Some(CommonVideoModel::Veo3Fast));
|
||||
assert_eq!(parse_video_model("kling3pro"), Some(CommonVideoModel::Kling3p0Pro));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown() {
|
||||
assert_eq!(parse_video_model("nonexistent"), None);
|
||||
assert_eq!(parse_video_model(""), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trims_whitespace() {
|
||||
assert_eq!(parse_video_model(" seedance2 "), Some(CommonVideoModel::Seedance2p0));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod aspect_ratio_to_artcraft_flux_1_dev;
|
||||
pub mod router_aspect_ratio_to_enums_aspect_ratio;
|
||||
pub mod aspect_ratio_to_artcraft_flux_1_schnell;
|
||||
pub mod aspect_ratio_to_artcraft_flux_pro_1p1;
|
||||
pub mod aspect_ratio_to_artcraft_flux_pro_1p1_ultra;
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
use artcraft_router::api::common_aspect_ratio::CommonAspectRatio as RouterCommonAspectRatio;
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio as EnumsCommonAspectRatio;
|
||||
|
||||
/// Map from the router's CommonAspectRatio to the enums crate's CommonAspectRatio.
|
||||
pub fn router_aspect_ratio_to_enums_aspect_ratio(ratio: RouterCommonAspectRatio) -> EnumsCommonAspectRatio {
|
||||
match ratio {
|
||||
RouterCommonAspectRatio::Auto => EnumsCommonAspectRatio::Auto,
|
||||
RouterCommonAspectRatio::Square => EnumsCommonAspectRatio::Square,
|
||||
RouterCommonAspectRatio::WideThreeByTwo => EnumsCommonAspectRatio::WideThreeByTwo,
|
||||
RouterCommonAspectRatio::WideFourByThree => EnumsCommonAspectRatio::WideFourByThree,
|
||||
RouterCommonAspectRatio::WideFiveByFour => EnumsCommonAspectRatio::WideFiveByFour,
|
||||
RouterCommonAspectRatio::WideSixteenByNine => EnumsCommonAspectRatio::WideSixteenByNine,
|
||||
RouterCommonAspectRatio::WideTwentyOneByNine => EnumsCommonAspectRatio::WideTwentyOneByNine,
|
||||
RouterCommonAspectRatio::TallTwoByThree => EnumsCommonAspectRatio::TallTwoByThree,
|
||||
RouterCommonAspectRatio::TallThreeByFour => EnumsCommonAspectRatio::TallThreeByFour,
|
||||
RouterCommonAspectRatio::TallFourByFive => EnumsCommonAspectRatio::TallFourByFive,
|
||||
RouterCommonAspectRatio::TallNineBySixteen => EnumsCommonAspectRatio::TallNineBySixteen,
|
||||
RouterCommonAspectRatio::TallNineByTwentyOne => EnumsCommonAspectRatio::TallNineByTwentyOne,
|
||||
RouterCommonAspectRatio::Wide => EnumsCommonAspectRatio::Wide,
|
||||
RouterCommonAspectRatio::Tall => EnumsCommonAspectRatio::Tall,
|
||||
RouterCommonAspectRatio::Auto2k => EnumsCommonAspectRatio::Auto2k,
|
||||
RouterCommonAspectRatio::Auto3k => EnumsCommonAspectRatio::Auto3k,
|
||||
RouterCommonAspectRatio::Auto4k => EnumsCommonAspectRatio::Auto4k,
|
||||
RouterCommonAspectRatio::SquareHd => EnumsCommonAspectRatio::SquareHd,
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod aspect_ratio;
|
||||
pub mod resolution;
|
||||
pub mod quality;
|
||||
pub mod models;
|
||||
pub mod quality;
|
||||
pub mod resolution;
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod tauri_image_model_to_enums_model;
|
||||
pub mod tauri_image_model_to_generation_model;
|
||||
pub mod tauri_image_model_to_router_model;
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
use enums::common::generation::common_image_model::CommonImageModel;
|
||||
|
||||
use crate::core::commands::generate::generate_image::tauri_image_model::TauriImageModel;
|
||||
|
||||
/// Map TauriImageModel to the omni endpoint's CommonImageModel (enums crate).
|
||||
/// Returns None for models not yet supported by the omni endpoint.
|
||||
pub fn tauri_image_model_to_enums_model(model: TauriImageModel) -> Option<CommonImageModel> {
|
||||
match model {
|
||||
TauriImageModel::Flux1Dev => Some(CommonImageModel::Flux1Dev),
|
||||
TauriImageModel::Flux1Schnell => Some(CommonImageModel::Flux1Schnell),
|
||||
TauriImageModel::FluxPro1 => Some(CommonImageModel::FluxPro11), // TODO: Might be a slight mismatch
|
||||
TauriImageModel::FluxPro11 => Some(CommonImageModel::FluxPro11),
|
||||
TauriImageModel::FluxPro11Ultra => Some(CommonImageModel::FluxPro11Ultra),
|
||||
TauriImageModel::GptImage1 => Some(CommonImageModel::GptImage1),
|
||||
TauriImageModel::GptImage1p5 => Some(CommonImageModel::GptImage1p5),
|
||||
TauriImageModel::GptImage2 => Some(CommonImageModel::GptImage2),
|
||||
TauriImageModel::NanoBanana => Some(CommonImageModel::NanoBanana),
|
||||
TauriImageModel::NanoBanana2 => Some(CommonImageModel::NanoBanana2),
|
||||
TauriImageModel::NanoBananaPro => Some(CommonImageModel::NanoBananaPro),
|
||||
TauriImageModel::Gemini25Flash => Some(CommonImageModel::NanoBanana),
|
||||
TauriImageModel::Seedream4 => Some(CommonImageModel::Seedream4),
|
||||
TauriImageModel::Seedream4p5 => Some(CommonImageModel::Seedream4p5),
|
||||
TauriImageModel::Seedream5Lite => Some(CommonImageModel::Seedream5Lite),
|
||||
// Not accounted for yet
|
||||
TauriImageModel::GrokImage => None,
|
||||
TauriImageModel::Recraft3 => None,
|
||||
TauriImageModel::Midjourney => None,
|
||||
TauriImageModel::FluxProKontextMax => None,
|
||||
TauriImageModel::QwenEdit2511Angles => None,
|
||||
TauriImageModel::Flux2LoraAngles => None,
|
||||
TauriImageModel::FluxDevJuggernaut => None,
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
use crate::core::commands::generate::generate_image::tauri_image_model::TauriImageModel;
|
||||
use crate::core::events::generation_events::common::GenerationModel;
|
||||
|
||||
/// Map TauriImageModel to GenerationModel for frontend events.
|
||||
pub fn tauri_image_model_to_generation_model(model: TauriImageModel) -> GenerationModel {
|
||||
match model {
|
||||
TauriImageModel::Flux1Dev => GenerationModel::Flux1Dev,
|
||||
TauriImageModel::Flux1Schnell => GenerationModel::Flux1Schnell,
|
||||
TauriImageModel::FluxPro11 => GenerationModel::FluxPro11,
|
||||
TauriImageModel::FluxPro11Ultra => GenerationModel::FluxPro11Ultra,
|
||||
TauriImageModel::GrokImage => GenerationModel::GrokImage,
|
||||
TauriImageModel::Recraft3 => GenerationModel::Flux1Dev, // Fallback
|
||||
TauriImageModel::GptImage1 => GenerationModel::GptImage1,
|
||||
TauriImageModel::GptImage1p5 => GenerationModel::GptImage1p5,
|
||||
TauriImageModel::GptImage2 => GenerationModel::GptImage2,
|
||||
TauriImageModel::Gemini25Flash => GenerationModel::NanoBanana,
|
||||
TauriImageModel::NanoBanana => GenerationModel::NanoBanana,
|
||||
TauriImageModel::NanoBanana2 => GenerationModel::NanoBanana2,
|
||||
TauriImageModel::NanoBananaPro => GenerationModel::NanoBananaPro,
|
||||
TauriImageModel::Seedream4 => GenerationModel::Seedream4,
|
||||
TauriImageModel::Seedream4p5 => GenerationModel::Seedream4p5,
|
||||
TauriImageModel::Seedream5Lite => GenerationModel::Seedream5Lite,
|
||||
TauriImageModel::Midjourney => GenerationModel::Flux1Dev, // Fallback
|
||||
TauriImageModel::FluxProKontextMax => GenerationModel::FluxProKontextMax,
|
||||
TauriImageModel::QwenEdit2511Angles => GenerationModel::QwenEdit2511Angles,
|
||||
TauriImageModel::Flux2LoraAngles => GenerationModel::Flux2LoraAngles,
|
||||
TauriImageModel::FluxDevJuggernaut => GenerationModel::FluxDevJuggernaut,
|
||||
TauriImageModel::FluxPro1 => GenerationModel::FluxPro1,
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
use artcraft_router::api::common_image_model::CommonImageModel;
|
||||
|
||||
use crate::core::commands::generate::generate_image::tauri_image_model::TauriImageModel;
|
||||
|
||||
/// Map TauriImageModel to the artcraft_router's CommonImageModel.
|
||||
/// Returns None for models not supported by the router (Grok, Midjourney, etc.).
|
||||
pub fn tauri_image_model_to_router_model(model: TauriImageModel) -> Option<CommonImageModel> {
|
||||
match model {
|
||||
TauriImageModel::Flux1Dev => Some(CommonImageModel::Flux1Dev), // Text-to-Image
|
||||
TauriImageModel::Flux1Schnell => Some(CommonImageModel::Flux1Schnell), // Text-to-Image
|
||||
TauriImageModel::FluxPro1 => Some(CommonImageModel::FluxPro11), // TODO: Might be a slight mismatch
|
||||
TauriImageModel::FluxPro11 => Some(CommonImageModel::FluxPro11), // Text-to-Image
|
||||
TauriImageModel::FluxPro11Ultra => Some(CommonImageModel::FluxPro11Ultra), // Text-to-Image
|
||||
TauriImageModel::GptImage1 => Some(CommonImageModel::GptImage1), // Text-to-Image
|
||||
TauriImageModel::GptImage1p5 => Some(CommonImageModel::GptImage1p5), // Text-to-Image
|
||||
TauriImageModel::GptImage2 => Some(CommonImageModel::GptImage2), // Text-to-Image
|
||||
TauriImageModel::NanoBanana => Some(CommonImageModel::NanoBanana), // Text-to-Image
|
||||
TauriImageModel::NanoBanana2 => Some(CommonImageModel::NanoBanana2), // Text-to-Image
|
||||
TauriImageModel::NanoBananaPro => Some(CommonImageModel::NanoBananaPro), // Text-to-Image
|
||||
TauriImageModel::Gemini25Flash => Some(CommonImageModel::NanoBanana), // Text-to-Image
|
||||
TauriImageModel::Seedream4 => Some(CommonImageModel::Seedream4), // Text-to-Image
|
||||
TauriImageModel::Seedream4p5 => Some(CommonImageModel::Seedream4p5), // Text-to-Image
|
||||
TauriImageModel::Seedream5Lite => Some(CommonImageModel::Seedream5Lite), // Text-to-Image
|
||||
TauriImageModel::QwenEdit2511Angles => Some(CommonImageModel::QwenEdit2511Angles),
|
||||
TauriImageModel::Flux2LoraAngles => Some(CommonImageModel::Flux2LoraAngles),
|
||||
// Not accounted for yet
|
||||
TauriImageModel::GrokImage => None,
|
||||
TauriImageModel::Recraft3 => None,
|
||||
TauriImageModel::Midjourney => None,
|
||||
TauriImageModel::FluxProKontextMax => None,
|
||||
TauriImageModel::FluxDevJuggernaut => None,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod image;
|
||||
@@ -1 +1,2 @@
|
||||
pub mod resolution_to_artcraft_nano_banana_pro;
|
||||
pub mod resolution_to_artcraft_nano_banana_pro;
|
||||
pub mod router_resolution_to_enums_resolution;
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
use artcraft_router::api::common_resolution::CommonResolution as RouterCommonResolution;
|
||||
use enums::common::generation::common_resolution::CommonResolution as EnumsCommonResolution;
|
||||
|
||||
/// Map from the router's CommonResolution to the enums crate's CommonResolution.
|
||||
pub fn router_resolution_to_enums_resolution(res: RouterCommonResolution) -> EnumsCommonResolution {
|
||||
match res {
|
||||
RouterCommonResolution::OneK => EnumsCommonResolution::OneK,
|
||||
RouterCommonResolution::TwoK => EnumsCommonResolution::TwoK,
|
||||
RouterCommonResolution::ThreeK => EnumsCommonResolution::ThreeK,
|
||||
RouterCommonResolution::FourK => EnumsCommonResolution::FourK,
|
||||
RouterCommonResolution::HalfK => EnumsCommonResolution::HalfK,
|
||||
RouterCommonResolution::FourEightyP => EnumsCommonResolution::FourEightyP,
|
||||
RouterCommonResolution::SevenTwentyP => EnumsCommonResolution::SevenTwentyP,
|
||||
RouterCommonResolution::TenEightyP => EnumsCommonResolution::TenEightyP,
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::EnqueueEditImageSuccessResponse;
|
||||
use crate::core::commands::deprecated::image_edit::enqueue_edit_image_command::EnqueueEditImageSuccessResponse;
|
||||
use crate::core::commands::response::shorthand::ResponseOrErrorMessage;
|
||||
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
|
||||
use crate::core::state::app_preferences::app_preferences_manager::AppPreferencesManager;
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
|
||||
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
|
||||
use crate::core::commands::deprecated::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
|
||||
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
|
||||
use crate::core::commands::deprecated::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
use crate::core::commands::enqueue::generate_error::GenerateError;
|
||||
use crate::core::commands::enqueue::image_edit::artcraft::handle_artcraft_flux_kontext_edit::handle_artcraft_flux_kontext_edit;
|
||||
use crate::core::commands::enqueue::image_edit::artcraft::handle_artcraft_gpt_image_1_edit::handle_artcraft_gpt_image_1_edit;
|
||||
use crate::core::commands::enqueue::image_edit::artcraft::handle_image_edit_artcraft_via_router::handle_image_edit_artcraft_via_router;
|
||||
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EnqueueEditImageCommand, ImageEditModel};
|
||||
use crate::core::commands::deprecated::image_edit::artcraft::handle_artcraft_flux_kontext_edit::handle_artcraft_flux_kontext_edit;
|
||||
use crate::core::commands::deprecated::image_edit::artcraft::handle_artcraft_gpt_image_1_edit::handle_artcraft_gpt_image_1_edit;
|
||||
use crate::core::commands::deprecated::image_edit::artcraft::handle_image_edit_artcraft_via_router::handle_image_edit_artcraft_via_router;
|
||||
use crate::core::commands::deprecated::image_edit::enqueue_edit_image_command::{EnqueueEditImageCommand, ImageEditModel};
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::events::generation_events::common::GenerationModel;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
use crate::core::api_adapters::aspect_ratio::common_aspect_ratio::CommonAspectRatio as DesktopCommonAspectRatio;
|
||||
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason};
|
||||
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{
|
||||
use crate::core::commands::deprecated::image_edit::enqueue_edit_image_command::{
|
||||
EditImageResolution, EditImageSize, EnqueueEditImageCommand,
|
||||
};
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
+3
-3
@@ -2,9 +2,9 @@ use crate::core::api_adapters::aspect_ratio::common_aspect_ratio::CommonAspectRa
|
||||
use crate::core::api_adapters::quality::common_quality::CommonQuality;
|
||||
use crate::core::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors;
|
||||
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
|
||||
use crate::core::commands::enqueue::image_edit::artcraft::handle_image_edit_artcraft::handle_image_edit_artcraft;
|
||||
use crate::core::commands::enqueue::image_edit::image_edit_models::image_edit_model_to_model_type;
|
||||
use crate::core::commands::enqueue::image_edit::sora::handle_image_edit_sora::handle_image_edit_sora;
|
||||
use crate::core::commands::deprecated::image_edit::artcraft::handle_image_edit_artcraft::handle_image_edit_artcraft;
|
||||
use crate::core::commands::deprecated::image_edit::image_edit_models::image_edit_model_to_model_type;
|
||||
use crate::core::commands::deprecated::image_edit::sora::handle_image_edit_sora::handle_image_edit_sora;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
|
||||
use crate::core::commands::response::shorthand::{Response, ResponseOrErrorType};
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
use enums::common::generation::common_model_type::CommonModelType;
|
||||
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::ImageEditModel;
|
||||
use crate::core::commands::deprecated::image_edit::enqueue_edit_image_command::ImageEditModel;
|
||||
|
||||
pub fn image_edit_model_to_model_type(model: ImageEditModel) -> CommonModelType {
|
||||
match model {
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
use crate::core::commands::enqueue::generate_error::GenerateError;
|
||||
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EnqueueEditImageCommand, ImageEditModel};
|
||||
use crate::core::commands::enqueue::image_edit::image_edit_models::image_edit_model_to_model_type;
|
||||
use crate::core::commands::enqueue::image_edit::sora::handle_sora_gpt_image_1_edit::handle_sora_gpt_image_1_edit;
|
||||
use crate::core::commands::deprecated::image_edit::enqueue_edit_image_command::{EnqueueEditImageCommand, ImageEditModel};
|
||||
use crate::core::commands::deprecated::image_edit::image_edit_models::image_edit_model_to_model_type;
|
||||
use crate::core::commands::deprecated::image_edit::sora::handle_sora_gpt_image_1_edit::handle_sora_gpt_image_1_edit;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
|
||||
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageSize, EnqueueEditImageCommand, EnqueueEditImageErrorType};
|
||||
use crate::core::commands::deprecated::image_edit::enqueue_edit_image_command::{EditImageSize, EnqueueEditImageCommand, EnqueueEditImageErrorType};
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
|
||||
use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalCommand;
|
||||
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
|
||||
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
|
||||
use crate::core::commands::deprecated::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
|
||||
use crate::core::commands::deprecated::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
|
||||
use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalCommand;
|
||||
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
|
||||
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
|
||||
use crate::core::commands::deprecated::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
|
||||
use crate::core::commands::deprecated::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
|
||||
use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalCommand;
|
||||
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
|
||||
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
|
||||
use crate::core::commands::deprecated::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
|
||||
use crate::core::commands::deprecated::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
|
||||
use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalCommand;
|
||||
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
|
||||
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
|
||||
use crate::core::commands::deprecated::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
|
||||
use crate::core::commands::deprecated::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
|
||||
+5
-5
@@ -1,9 +1,9 @@
|
||||
use crate::core::commands::enqueue::generate_error::GenerateError;
|
||||
use crate::core::commands::enqueue::image_inpaint::artcraft::handle_artcraft_flux_dev_juggernaut_inpaint::handle_artcraft_flux_dev_juggernaut_inpaint;
|
||||
use crate::core::commands::enqueue::image_inpaint::artcraft::handle_artcraft_flux_pro_1_inpaint::handle_artcraft_flux_pro_1_inpaint;
|
||||
use crate::core::commands::enqueue::image_inpaint::artcraft::handle_artcraft_flux_pro_kontext_inpaint::handle_artcraft_flux_pro_kontext_inpaint;
|
||||
use crate::core::commands::enqueue::image_inpaint::artcraft::handle_artcraft_nano_banana_inpaint::handle_artcraft_nano_banana_inpaint;
|
||||
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::{EnqueueInpaintImageCommand, ImageInpaintModel};
|
||||
use crate::core::commands::deprecated::image_inpaint::artcraft::handle_artcraft_flux_dev_juggernaut_inpaint::handle_artcraft_flux_dev_juggernaut_inpaint;
|
||||
use crate::core::commands::deprecated::image_inpaint::artcraft::handle_artcraft_flux_pro_1_inpaint::handle_artcraft_flux_pro_1_inpaint;
|
||||
use crate::core::commands::deprecated::image_inpaint::artcraft::handle_artcraft_flux_pro_kontext_inpaint::handle_artcraft_flux_pro_kontext_inpaint;
|
||||
use crate::core::commands::deprecated::image_inpaint::artcraft::handle_artcraft_nano_banana_inpaint::handle_artcraft_nano_banana_inpaint;
|
||||
use crate::core::commands::deprecated::image_inpaint::enqueue_image_inpaint_command::{EnqueueInpaintImageCommand, ImageInpaintModel};
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
use crate::core::api_adapters::quality::common_quality::CommonQuality;
|
||||
use crate::core::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors;
|
||||
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
|
||||
use crate::core::commands::enqueue::image_edit::image_edit_models::image_edit_model_to_model_type;
|
||||
use crate::core::commands::enqueue::image_inpaint::artcraft::handle_inpaint_artcraft::handle_inpaint_artcraft;
|
||||
use crate::core::commands::enqueue::image_inpaint::inpaint_models::image_inpaint_model_to_model_type;
|
||||
use crate::core::commands::deprecated::image_edit::image_edit_models::image_edit_model_to_model_type;
|
||||
use crate::core::commands::deprecated::image_inpaint::artcraft::handle_inpaint_artcraft::handle_inpaint_artcraft;
|
||||
use crate::core::commands::deprecated::image_inpaint::inpaint_models::image_inpaint_model_to_model_type;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
|
||||
use crate::core::commands::response::shorthand::{Response, ResponseOrErrorType};
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::ImageInpaintModel;
|
||||
use crate::core::commands::deprecated::image_inpaint::enqueue_image_inpaint_command::ImageInpaintModel;
|
||||
use enums::common::generation::common_model_type::CommonModelType;
|
||||
|
||||
pub fn image_inpaint_model_to_model_type(model: ImageInpaintModel) -> CommonModelType {
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod image_edit;
|
||||
pub mod image_inpaint;
|
||||
pub mod text_to_image;
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
use crate::core::commands::enqueue::generate_error::GenerateError;
|
||||
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize};
|
||||
use crate::core::commands::deprecated::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize};
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
|
||||
use crate::core::commands::deprecated::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
|
||||
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
use crate::core::commands::enqueue::generate_error::GenerateError;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::enqueue::text_to_image::artcraft::handle_artcraft_gpt_image_1_text_to_image::handle_artcraft_gpt_image_1_text_to_image;
|
||||
use crate::core::commands::enqueue::text_to_image::artcraft::handle_text_to_image_artcraft_via_router::handle_text_to_image_artcraft_via_router;
|
||||
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageModel};
|
||||
use crate::core::commands::enqueue::text_to_image::text_to_image_models::text_to_image_model_to_model_type;
|
||||
use crate::core::commands::deprecated::text_to_image::artcraft::handle_artcraft_gpt_image_1_text_to_image::handle_artcraft_gpt_image_1_text_to_image;
|
||||
use crate::core::commands::deprecated::text_to_image::artcraft::handle_text_to_image_artcraft_via_router::handle_text_to_image_artcraft_via_router;
|
||||
use crate::core::commands::deprecated::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageModel};
|
||||
use crate::core::commands::deprecated::text_to_image::text_to_image_models::text_to_image_model_to_model_type;
|
||||
use crate::core::events::generation_events::common::GenerationModel;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
+1
-1
@@ -2,7 +2,7 @@ use crate::core::api_adapters::aspect_ratio::common_aspect_ratio::CommonAspectRa
|
||||
use crate::core::api_adapters::resolution::common_resolution::CommonResolution as CommonResolution2;
|
||||
use crate::core::commands::enqueue::generate_error::{GenerateError, MissingCredentialsReason};
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{
|
||||
use crate::core::commands::deprecated::text_to_image::enqueue_text_to_image_command::{
|
||||
EnqueueTextToImageRequest, TextToImageResolution, TextToImageSize,
|
||||
};
|
||||
use crate::core::events::generation_events::common::GenerationModel;
|
||||
+6
-6
@@ -3,13 +3,13 @@ use crate::core::api_adapters::quality::common_quality::CommonQuality;
|
||||
use crate::core::api_adapters::resolution::common_resolution::CommonResolution;
|
||||
use crate::core::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors;
|
||||
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason};
|
||||
use crate::core::commands::enqueue::image_edit::image_edit_models::image_edit_model_to_model_type;
|
||||
use crate::core::commands::deprecated::image_edit::image_edit_models::image_edit_model_to_model_type;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::enqueue::text_to_image::artcraft::handle_text_to_image_artcraft::handle_text_to_image_artcraft;
|
||||
use crate::core::commands::enqueue::text_to_image::grok::handle_grok::handle_grok;
|
||||
use crate::core::commands::enqueue::text_to_image::midjourney::handle_midjourney::handle_midjourney;
|
||||
use crate::core::commands::enqueue::text_to_image::sora::handle_text_to_image_sora::handle_text_to_image_sora;
|
||||
use crate::core::commands::enqueue::text_to_image::text_to_image_models::text_to_image_model_to_model_type;
|
||||
use crate::core::commands::deprecated::text_to_image::artcraft::handle_text_to_image_artcraft::handle_text_to_image_artcraft;
|
||||
use crate::core::commands::deprecated::text_to_image::grok::handle_grok::handle_grok;
|
||||
use crate::core::commands::deprecated::text_to_image::midjourney::handle_midjourney::handle_midjourney;
|
||||
use crate::core::commands::deprecated::text_to_image::sora::handle_text_to_image_sora::handle_text_to_image_sora;
|
||||
use crate::core::commands::deprecated::text_to_image::text_to_image_models::text_to_image_model_to_model_type;
|
||||
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
|
||||
use crate::core::commands::response::shorthand::Response;
|
||||
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
use crate::core::api_adapters::aspect_ratio::convert::aspect_ratio_to_grok_image::aspect_ratio_to_grok_image;
|
||||
use crate::core::commands::enqueue::generate_error::GenerateError;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
|
||||
use crate::core::commands::deprecated::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
|
||||
use crate::core::events::generation_events::common::GenerationModel;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::services::grok::state::grok_credential_manager::GrokCredentialManager;
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
use crate::core::commands::enqueue::generate_error::{GenerateError, MissingCredentialsReason, ProviderFailureReason};
|
||||
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize};
|
||||
use crate::core::commands::deprecated::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize};
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
|
||||
use crate::core::commands::deprecated::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::functional_events::canvas_background_removal_complete_event::CanvasBackgroundRemovalCompleteEvent;
|
||||
use crate::core::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
|
||||
+1
-1
@@ -2,7 +2,7 @@ use crate::core::api_adapters::aspect_ratio::convert::aspect_ratio_to_grok_image
|
||||
use crate::core::api_adapters::aspect_ratio::convert::aspect_ratio_to_sora_native_gpt_image_1::aspect_ratio_to_sora_native_gpt_image_1;
|
||||
use crate::core::commands::enqueue::generate_error::GenerateError;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
|
||||
use crate::core::commands::deprecated::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
use crate::core::commands::enqueue::generate_error::GenerateError;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::deprecated::text_to_image::enqueue_text_to_image_command::EnqueueTextToImageRequest;
|
||||
use crate::core::commands::deprecated::text_to_image::sora::handle_gpt_image_1_sora_text_to_image::handle_gpt_image_1_sora_text_to_image;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use tauri::AppHandle;
|
||||
|
||||
pub async fn handle_text_to_image_sora(
|
||||
request: &EnqueueTextToImageRequest,
|
||||
app: &AppHandle,
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
sora_task_queue: &SoraTaskQueue,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
handle_gpt_image_1_sora_text_to_image(
|
||||
request,
|
||||
app,
|
||||
sora_creds_manager,
|
||||
sora_task_queue,
|
||||
).await
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::TextToImageModel;
|
||||
use crate::core::commands::deprecated::text_to_image::enqueue_text_to_image_command::TextToImageModel;
|
||||
use enums::common::generation::common_model_type::CommonModelType;
|
||||
|
||||
pub fn text_to_image_model_to_model_type(model: TextToImageModel) -> CommonModelType {
|
||||
@@ -1,3 +1,3 @@
|
||||
mod maybe_notify_frontend_of_billing_errors;
|
||||
mod maybe_notify_frontend_of_grok_errors;
|
||||
pub mod maybe_notify_frontend_of_billing_errors;
|
||||
pub mod maybe_notify_frontend_of_grok_errors;
|
||||
pub mod notify_frontend_of_errors;
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason, ProviderFailureReason};
|
||||
use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalCommand;
|
||||
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
|
||||
use crate::core::commands::deprecated::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize, EnqueueEditImageCommand};
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason};
|
||||
use crate::core::commands::enqueue::image_to_gaussian::enqueue_image_to_gaussian_command::EnqueueImageToGaussianRequest;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
|
||||
use crate::core::commands::deprecated::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
|
||||
use crate::core::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
|
||||
use crate::core::events::generation_events::common::GenerationModel;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
|
||||
-81
@@ -1,81 +0,0 @@
|
||||
use crate::core::commands::enqueue::generate_error::GenerateError;
|
||||
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::EnqueueImageToVideoRequest;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::events::generation_events::common::GenerationModel;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use artcraft_router::api::audio_list_ref::AudioListRef;
|
||||
use artcraft_router::api::character_list_ref::CharacterListRef;
|
||||
use artcraft_router::api::common_video_model::CommonVideoModel;
|
||||
use artcraft_router::api::image_list_ref::ImageListRef;
|
||||
use artcraft_router::api::image_ref::ImageRef;
|
||||
use artcraft_router::api::provider::Provider;
|
||||
use artcraft_router::api::video_list_ref::VideoListRef;
|
||||
use artcraft_router::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use artcraft_router::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use artcraft_router::client::router_client::RouterClient;
|
||||
use artcraft_router::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use log::{error, info};
|
||||
|
||||
pub(super) async fn handle_artcraft_seedance_2p0(
|
||||
request: &EnqueueImageToVideoRequest,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
creds: &StorytellerCredentialSet,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
let client = RouterClient::Artcraft(RouterArtcraftClient::new(
|
||||
app_env_configs.storyteller_host.clone(),
|
||||
creds.clone(),
|
||||
));
|
||||
|
||||
let start_frame = request.image_media_token.clone().map(ImageRef::MediaFileToken);
|
||||
let end_frame = request.end_frame_image_media_token.clone().map(ImageRef::MediaFileToken);
|
||||
let reference_images = request.reference_image_media_tokens.clone().map(ImageListRef::MediaFileTokens);
|
||||
|
||||
let router_request = GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::Seedance2p0,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: request.prompt.clone(),
|
||||
negative_prompt: None,
|
||||
start_frame,
|
||||
end_frame,
|
||||
reference_images,
|
||||
reference_videos: request.reference_video_media_tokens.clone().map(VideoListRef::MediaFileTokens),
|
||||
reference_audio: request.reference_audio_media_tokens.clone().map(AudioListRef::MediaFileTokens),
|
||||
reference_character_tokens: request.reference_character_tokens.clone().map(CharacterListRef::CharacterTokens),
|
||||
resolution: None,
|
||||
aspect_ratio: request.aspect_ratio,
|
||||
duration_seconds: request.duration_seconds,
|
||||
video_batch_count: request.video_batch_count,
|
||||
generate_audio: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
idempotency_token: None,
|
||||
};
|
||||
|
||||
let plan = router_request.build()?;
|
||||
|
||||
info!("Video Generation Plan: {:?}", plan);
|
||||
|
||||
let response = match plan.generate_video(&client).await {
|
||||
Ok(resp) => {
|
||||
info!("Successfully enqueued.");
|
||||
resp
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to enqueue: {:?}", err);
|
||||
return Err(GenerateError::from(err));
|
||||
}
|
||||
};
|
||||
|
||||
let job_id = response.get_artcraft_payload()
|
||||
.map(|p| p.inference_job_token.to_string())
|
||||
.ok_or(GenerateError::ResponseHadNoJobTokens)?;
|
||||
|
||||
Ok(TaskEnqueueSuccess {
|
||||
task_type: TaskType::VideoGeneration,
|
||||
model: Some(GenerationModel::Seedance2p0),
|
||||
provider: GenerationProvider::Artcraft,
|
||||
provider_job_id: Some(job_id),
|
||||
})
|
||||
}
|
||||
-66
@@ -1,66 +0,0 @@
|
||||
use crate::core::commands::enqueue::generate_error::GenerateError;
|
||||
use crate::core::commands::enqueue::image_to_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
|
||||
use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_kling_1p6_pro::handle_artcraft_kling_1p6_pro;
|
||||
use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_kling_2p1_master::handle_artcraft_kling_2p1_master;
|
||||
use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_kling_2p1_pro::handle_artcraft_kling_2p1_pro;
|
||||
use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_kling_2p5_turbo_pro::handle_artcraft_kling_2p5_turbo_pro;
|
||||
use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_kling_2p6_pro::handle_artcraft_kling_2p6_pro;
|
||||
use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_video_via_router::handle_artcraft_video_via_router;
|
||||
use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_seedance_1_lite::handle_artcraft_seedance_1_lite;
|
||||
use crate::core::events::generation_events::common::GenerationModel;
|
||||
use artcraft_router::api::common_video_model::CommonVideoModel;
|
||||
use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_seedance_2p0::handle_artcraft_seedance_2p0;
|
||||
use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_sora2::handle_artcraft_sora2;
|
||||
use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_sora2_pro::handle_artcraft_sora2_pro;
|
||||
use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_veo2::handle_artcraft_veo2;
|
||||
use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_veo3::handle_artcraft_veo3;
|
||||
use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_veo3_fast::handle_artcraft_veo3_fast;
|
||||
use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_veo3p1::handle_artcraft_veo3p1;
|
||||
use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_veo3p1_fast::handle_artcraft_veo3p1_fast;
|
||||
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::{EnqueueImageToVideoRequest, VideoModel};
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use anyhow::anyhow;
|
||||
use tauri::AppHandle;
|
||||
|
||||
pub async fn handle_video_artcraft(
|
||||
request: &EnqueueImageToVideoRequest,
|
||||
app: &AppHandle,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
|
||||
let creds = get_storyteller_creds_or_error(app, storyteller_creds_manager)?;
|
||||
|
||||
match request.model {
|
||||
None => {
|
||||
Err(GenerateError::no_model_specified())
|
||||
},
|
||||
// Handled via router
|
||||
Some(VideoModel::Kling3p0Pro) => handle_artcraft_video_via_router(request, app_env_configs, &creds, CommonVideoModel::Kling3p0Pro, GenerationModel::Kling3p0Pro).await,
|
||||
Some(VideoModel::Kling3p0Standard) => handle_artcraft_video_via_router(request, app_env_configs, &creds, CommonVideoModel::Kling3p0Standard, GenerationModel::Kling3p0Standard).await,
|
||||
Some(VideoModel::Seedance1p5Pro) => handle_artcraft_video_via_router(request, app_env_configs, &creds, CommonVideoModel::Seedance1p5Pro, GenerationModel::Seedance1p5Pro).await,
|
||||
Some(VideoModel::Seedance2p0) => handle_artcraft_video_via_router(request, app_env_configs, &creds, CommonVideoModel::Seedance2p0, GenerationModel::Seedance2p0).await,
|
||||
Some(VideoModel::Seedance2p0Fast) => handle_artcraft_video_via_router(request, app_env_configs, &creds, CommonVideoModel::Seedance2p0Fast, GenerationModel::Seedance2p0Fast).await,
|
||||
Some(VideoModel::HappyHorse1p0) => handle_artcraft_video_via_router(request, app_env_configs, &creds, CommonVideoModel::HappyHorse1p0, GenerationModel::HappyHorse1p0).await,
|
||||
// Legacy endpoints
|
||||
Some(VideoModel::Kling16Pro) => handle_artcraft_kling_1p6_pro(request, app_env_configs, &creds).await,
|
||||
Some(VideoModel::Kling21Pro) => handle_artcraft_kling_2p1_pro(request, app_env_configs, &creds).await,
|
||||
Some(VideoModel::Kling21Master) => handle_artcraft_kling_2p1_master(request, app_env_configs, &creds).await,
|
||||
Some(VideoModel::Kling2p5TurboPro) => handle_artcraft_kling_2p5_turbo_pro(request, app_env_configs, &creds).await,
|
||||
Some(VideoModel::Kling2p6Pro) => handle_artcraft_kling_2p6_pro(request, app_env_configs, &creds).await,
|
||||
Some(VideoModel::Seedance10Lite) => handle_artcraft_seedance_1_lite(request, app_env_configs, &creds).await,
|
||||
Some(VideoModel::Sora2) => handle_artcraft_sora2(request, app_env_configs, &creds).await,
|
||||
Some(VideoModel::Sora2Pro) => handle_artcraft_sora2_pro(request, app_env_configs, &creds).await,
|
||||
Some(VideoModel::Veo2) => handle_artcraft_veo2(request, app_env_configs, &creds).await,
|
||||
Some(VideoModel::Veo3) => handle_artcraft_veo3(request, app_env_configs, &creds).await,
|
||||
Some(VideoModel::Veo3Fast) => handle_artcraft_veo3_fast(request, app_env_configs, &creds).await,
|
||||
Some(VideoModel::Veo3p1) => handle_artcraft_veo3p1(request, app_env_configs, &creds).await,
|
||||
Some(VideoModel::Veo3p1Fast) => handle_artcraft_veo3p1_fast(request, app_env_configs, &creds).await,
|
||||
Some(_) => {
|
||||
Err(GenerateError::AnyhowError(
|
||||
anyhow!("wrong logic: another branch should handle this: {:?}", request.model)))
|
||||
},
|
||||
}
|
||||
}
|
||||
-395
@@ -1,395 +0,0 @@
|
||||
use crate::core::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors;
|
||||
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason};
|
||||
use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_video::handle_video_artcraft;
|
||||
use crate::core::commands::enqueue::image_to_video::grok::handle_grok_video::handle_grok_video;
|
||||
use crate::core::commands::enqueue::image_to_video::sora2::handle_sora_sora2::handle_sora_sora2;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
|
||||
use crate::core::commands::response::shorthand::Response;
|
||||
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::functional_events::credits_balance_changed_event::CreditsBalanceChangedEvent;
|
||||
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
|
||||
use crate::core::state::artcraft_usage_tracker::artcraft_usage_type::{ArtcraftUsagePage, ArtcraftUsageType};
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
use crate::core::state::task_database::TaskDatabase;
|
||||
use crate::services::grok::state::grok_credential_manager::GrokCredentialManager;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use artcraft_router::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use artcraft_router::api::common_resolution::CommonResolution;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::ux::tauri_command_caller::TauriCommandCaller;
|
||||
use log::{error, info, warn};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, State};
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
/// This is used in the Tauri command bridge.
|
||||
/// Don't change the serializations without coordinating with the frontend.
|
||||
#[derive(Deserialize, Debug, Copy, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum VideoModel {
|
||||
#[serde(rename = "grok_video")]
|
||||
GrokVideo,
|
||||
|
||||
#[serde(rename = "kling_1.6_pro")]
|
||||
Kling16Pro,
|
||||
|
||||
#[serde(rename = "kling_2.1_pro")]
|
||||
Kling21Pro,
|
||||
|
||||
#[serde(rename = "kling_2.1_master")]
|
||||
Kling21Master,
|
||||
|
||||
#[serde(rename = "kling_2p5_turbo_pro")]
|
||||
Kling2p5TurboPro,
|
||||
|
||||
#[serde(rename = "kling_2p6_pro")]
|
||||
Kling2p6Pro,
|
||||
|
||||
#[serde(rename = "kling_3p0_standard")]
|
||||
Kling3p0Standard,
|
||||
|
||||
#[serde(rename = "kling_3p0_pro")]
|
||||
Kling3p0Pro,
|
||||
|
||||
#[serde(rename = "happy_horse_1p0")]
|
||||
HappyHorse1p0,
|
||||
|
||||
#[serde(rename = "seedance_1.0_lite")]
|
||||
Seedance10Lite,
|
||||
|
||||
#[serde(rename = "seedance_1p5_pro")]
|
||||
Seedance1p5Pro,
|
||||
|
||||
#[serde(rename = "seedance_2p0")]
|
||||
Seedance2p0,
|
||||
|
||||
#[serde(rename = "seedance_2p0_fast")]
|
||||
Seedance2p0Fast,
|
||||
|
||||
#[serde(rename = "sora_2")]
|
||||
Sora2,
|
||||
|
||||
#[serde(rename = "sora_2_pro")]
|
||||
Sora2Pro,
|
||||
|
||||
#[serde(rename = "veo_2")]
|
||||
Veo2,
|
||||
|
||||
#[serde(rename = "veo_3")]
|
||||
Veo3,
|
||||
|
||||
#[serde(rename = "veo_3_fast")]
|
||||
Veo3Fast,
|
||||
|
||||
#[serde(rename = "veo_3p1")]
|
||||
Veo3p1,
|
||||
|
||||
#[serde(rename = "veo_3p1_fast")]
|
||||
Veo3p1Fast,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct EnqueueImageToVideoRequest {
|
||||
/// The provider to use (defaults to Artcraft/Storyteller).
|
||||
/// Not all (provider, model) combinations are valid.
|
||||
pub provider: Option<GenerationProvider>,
|
||||
|
||||
/// REQUIRED.
|
||||
/// The model to use.
|
||||
pub model: Option<VideoModel>,
|
||||
|
||||
/// Currently REQUIRED by some downstream models.
|
||||
/// Image media file; the starting frame of the video.
|
||||
/// TODO: In the future we may support base64 images, URLs, or file paths here.
|
||||
pub image_media_token: Option<MediaFileToken>,
|
||||
|
||||
/// Optional.
|
||||
/// Image media file; the image to remove the background from.
|
||||
/// TODO: In the future we may support base64 images, URLs, or file paths here.
|
||||
pub end_frame_image_media_token: Option<MediaFileToken>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// Reference images for the video generation (e.g. character consistency, props, etc.)
|
||||
pub reference_image_media_tokens: Option<Vec<MediaFileToken>>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// Reference videos for the video generation (e.g. video edits, etc.)
|
||||
pub reference_video_media_tokens: Option<Vec<MediaFileToken>>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// Reference audio for the video generation (e.g. voices, lip sync, music, etc.)
|
||||
pub reference_audio_media_tokens: Option<Vec<MediaFileToken>>,
|
||||
|
||||
/// Optional character tokens to reference in the prompt.
|
||||
/// Characters are referenced in prompts as @CharacterName.
|
||||
pub reference_character_tokens: Option<Vec<CharacterToken>>,
|
||||
|
||||
/// Optional.
|
||||
/// Text prompt used to direct the video.
|
||||
pub prompt: Option<String>,
|
||||
|
||||
/// Optional.
|
||||
/// Generate the video with audio, if there is an option.
|
||||
/// Typically, this costs money when presented as an option, so we default to off.
|
||||
pub generate_audio: Option<bool>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// Aspect ratio for the generated video.
|
||||
pub aspect_ratio: Option<CommonAspectRatio>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// Resolution for the generated video.
|
||||
pub resolution: Option<CommonResolution>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// Duration of the generated video in seconds.
|
||||
pub duration_seconds: Option<u16>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// Number of videos to generate in one batch.
|
||||
pub video_batch_count: Option<u16>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// Only for Sora2 model currently.
|
||||
pub sora_orientation: Option<SoraOrientation>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// Only for Grok model currently
|
||||
pub grok_aspect_ratio: Option<GrokAspectRatio>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// Name of the frontend caller.
|
||||
/// We'll use this to selectively trigger events.
|
||||
pub frontend_caller: Option<TauriCommandCaller>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// A frontend-defined identifier that we'll send back to the frontend
|
||||
/// as a Tauri event on task completion.
|
||||
pub frontend_subscriber_id: Option<String>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// A frontend-defined payload that we'll send back to the frontend
|
||||
/// as a Tauri event on task completion.
|
||||
pub frontend_subscriber_payload: Option<String>,
|
||||
}
|
||||
|
||||
// TODO: Not sure how to handle so many different types of video (model) x (services).
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SoraOrientation {
|
||||
Portrait,
|
||||
Landscape,
|
||||
}
|
||||
|
||||
// TODO: Not sure how to handle so many different types of video (model) x (services).
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GrokAspectRatio {
|
||||
Portrait,
|
||||
Landscape,
|
||||
Square
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct EnqueueImageToVideoSuccessResponse {
|
||||
}
|
||||
|
||||
impl SerializeMarker for EnqueueImageToVideoSuccessResponse {}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum EnqueueImageToVideoErrorType {
|
||||
/// Caller didn't specify a model
|
||||
ModelNotSpecified,
|
||||
/// No model available for video generation
|
||||
NoProviderAvailable,
|
||||
/// Generic server error
|
||||
ServerError,
|
||||
/// No Fal API key available
|
||||
NeedsFalApiKey,
|
||||
/// Fal had an API error
|
||||
FalError,
|
||||
/// Needs to be logged into Artcraft
|
||||
NeedsStorytellerCredentials,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn enqueue_image_to_video_command(
|
||||
request: EnqueueImageToVideoRequest,
|
||||
app: AppHandle,
|
||||
app_env_configs: State<'_, AppEnvConfigs>,
|
||||
app_data_root: State<'_, AppDataRoot>,
|
||||
artcraft_usage_tracker: State<'_, ArtcraftUsageTracker>,
|
||||
provider_priority_store: State<'_, ProviderPriorityStore>,
|
||||
task_database: State<'_, TaskDatabase>,
|
||||
grok_creds_manager: State<'_, GrokCredentialManager>,
|
||||
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
|
||||
sora_task_queue: State<'_, SoraTaskQueue>,
|
||||
sora_creds_manager: State<'_, SoraCredentialManager>,
|
||||
) -> Response<EnqueueImageToVideoSuccessResponse, EnqueueImageToVideoErrorType, ()> {
|
||||
|
||||
info!("enqueue_image_to_video_command called, request: {:?}", request);
|
||||
|
||||
let result = handle_request(
|
||||
request,
|
||||
&app,
|
||||
&app_env_configs,
|
||||
&app_data_root,
|
||||
&artcraft_usage_tracker,
|
||||
&provider_priority_store,
|
||||
&task_database,
|
||||
&grok_creds_manager,
|
||||
&sora_creds_manager,
|
||||
&storyteller_creds_manager,
|
||||
).await;
|
||||
|
||||
match result {
|
||||
Err(err) => {
|
||||
error!("error: {:?}", err);
|
||||
|
||||
notify_frontend_of_errors(&app, &err).await;
|
||||
|
||||
let mut status = CommandErrorStatus::ServerError;
|
||||
let mut error_type = EnqueueImageToVideoErrorType::ServerError;
|
||||
let mut error_message = "A server error occurred. Please try again. If it continues, please tell our staff about the problem.";
|
||||
|
||||
match err {
|
||||
GenerateError::BadInput(BadInputReason::NoModelSpecified) => {
|
||||
status = CommandErrorStatus::BadRequest;
|
||||
error_type = EnqueueImageToVideoErrorType::ModelNotSpecified;
|
||||
error_message = "No model specified for video generation";
|
||||
}
|
||||
GenerateError::NoProviderAvailable => {
|
||||
status = CommandErrorStatus::ServerError;
|
||||
error_type = EnqueueImageToVideoErrorType::NoProviderAvailable;
|
||||
error_message = "No configured provider available for video generation";
|
||||
}
|
||||
GenerateError::MissingCredentials(MissingCredentialsReason::NeedsFalApiKey) => {
|
||||
status = CommandErrorStatus::Unauthorized;
|
||||
error_type = EnqueueImageToVideoErrorType::NeedsFalApiKey;
|
||||
error_message = "You need to set a FAL api key";
|
||||
},
|
||||
GenerateError::MissingCredentials(MissingCredentialsReason::NeedsStorytellerCredentials) => {
|
||||
status = CommandErrorStatus::Unauthorized;
|
||||
error_type = EnqueueImageToVideoErrorType::NeedsStorytellerCredentials;
|
||||
error_message = "You need to be logged into Artcraft.";
|
||||
}
|
||||
_ => {}, // Fall-through
|
||||
}
|
||||
|
||||
Err(CommandErrorResponseWrapper {
|
||||
status,
|
||||
error_message: Some(error_message.to_string()),
|
||||
error_type: Some(error_type),
|
||||
error_details: None,
|
||||
})
|
||||
}
|
||||
Ok(event) => {
|
||||
let event = GenerationEnqueueSuccessEvent {
|
||||
action: event.to_frontend_event_action(),
|
||||
service: event.to_frontend_event_service(),
|
||||
model: event.model,
|
||||
};
|
||||
|
||||
if let Err(err) = event.send(&app) {
|
||||
error!("Failed to emit event: {:?}", err); // Fail open.
|
||||
}
|
||||
|
||||
CreditsBalanceChangedEvent{}.send_infallible(&app);
|
||||
|
||||
Ok(EnqueueImageToVideoSuccessResponse {}.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub async fn handle_request(
|
||||
request: EnqueueImageToVideoRequest,
|
||||
app: &AppHandle,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
app_data_root: &AppDataRoot,
|
||||
artcraft_usage_tracker: &ArtcraftUsageTracker,
|
||||
provider_priority_store: &ProviderPriorityStore,
|
||||
task_database: &TaskDatabase,
|
||||
grok_creds_manager: &GrokCredentialManager,
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
|
||||
let model = match request.model {
|
||||
Some(model) => model,
|
||||
None => {
|
||||
return Err(GenerateError::no_model_specified())
|
||||
}
|
||||
};
|
||||
|
||||
let provider = match (model, request.provider) {
|
||||
(VideoModel::GrokVideo, _) => GenerationProvider::Grok,
|
||||
_ => request.provider.unwrap_or(GenerationProvider::Artcraft),
|
||||
};
|
||||
|
||||
info!("generate video with {:?} via provider {:?}", &model, &provider);
|
||||
|
||||
let result = match provider {
|
||||
GenerationProvider::Grok => {
|
||||
handle_grok_video(
|
||||
&request,
|
||||
app,
|
||||
app_data_root,
|
||||
app_env_configs,
|
||||
grok_creds_manager,
|
||||
).await
|
||||
}
|
||||
GenerationProvider::Sora => {
|
||||
handle_sora_sora2(
|
||||
&request,
|
||||
app,
|
||||
app_data_root,
|
||||
app_env_configs,
|
||||
sora_creds_manager,
|
||||
).await
|
||||
}
|
||||
_ => {
|
||||
handle_video_artcraft(
|
||||
&request,
|
||||
app,
|
||||
app_env_configs,
|
||||
storyteller_creds_manager,
|
||||
).await
|
||||
}
|
||||
};
|
||||
|
||||
let success_event = match result {
|
||||
Err(err) => return Err(err),
|
||||
Ok(event) => event,
|
||||
};
|
||||
|
||||
let result = success_event
|
||||
.insert_into_task_database_with_frontend_payload(
|
||||
task_database,
|
||||
request.frontend_caller,
|
||||
request.frontend_subscriber_id.as_deref(),
|
||||
request.frontend_subscriber_payload.as_deref()
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Err(err) = result {
|
||||
error!("Failed to create task in database: {:?}", err);
|
||||
// NB: Fail open, but find a way to flag this.
|
||||
}
|
||||
|
||||
if let Err(err) = artcraft_usage_tracker.record_video_generation(1, ArtcraftUsageType::ImageToResult, ArtcraftUsagePage::VideoPage) {
|
||||
// NB: Fail open.
|
||||
warn!("Failed to report usage: {:?}", err);
|
||||
}
|
||||
|
||||
Ok(success_event)
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
pub mod enqueue_image_to_video_command;
|
||||
pub mod sora2;
|
||||
mod grok;
|
||||
mod artcraft;
|
||||
@@ -1,10 +1,6 @@
|
||||
pub mod common;
|
||||
pub mod generate_error;
|
||||
pub mod image_bg_removal;
|
||||
pub mod image_edit;
|
||||
pub mod image_inpaint;
|
||||
pub mod image_to_gaussian;
|
||||
pub mod image_to_object;
|
||||
pub mod image_to_video;
|
||||
pub mod task_enqueue_success;
|
||||
pub mod text_to_image;
|
||||
pub mod image_to_gaussian;
|
||||
pub mod task_enqueue_success;
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
use crate::core::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors;
|
||||
use crate::core::commands::enqueue::generate_error::GenerateError;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::generate::generate_image::providers::artcraft::handle_artcraft;
|
||||
use crate::core::commands::generate::generate_image::tauri_generate_image_request::{
|
||||
TauriGenerateImageErrorType, TauriGenerateImageRequest, TauriGenerateImageResponse,
|
||||
};
|
||||
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
|
||||
use crate::core::commands::response::shorthand::Response;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::functional_events::credits_balance_changed_event::CreditsBalanceChangedEvent;
|
||||
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::task_database::TaskDatabase;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use log::{error, info};
|
||||
use tauri::{AppHandle, State};
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn generate_image_command(
|
||||
request: TauriGenerateImageRequest,
|
||||
app: AppHandle,
|
||||
app_env_configs: State<'_, AppEnvConfigs>,
|
||||
task_database: State<'_, TaskDatabase>,
|
||||
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
|
||||
) -> Response<TauriGenerateImageResponse, TauriGenerateImageErrorType, ()> {
|
||||
|
||||
info!("generate_image_command called, request: {:?}", request);
|
||||
|
||||
// Only Artcraft provider is supported via this command.
|
||||
// Other providers (Grok, Sora, Midjourney) should use the legacy commands.
|
||||
|
||||
let is_artcraft_provider = matches!(
|
||||
request.provider,
|
||||
None | Some(GenerationProvider::Artcraft)
|
||||
);
|
||||
|
||||
if !is_artcraft_provider {
|
||||
return Err(CommandErrorResponseWrapper {
|
||||
status: CommandErrorStatus::BadRequest,
|
||||
error_message: Some("Only Artcraft is supported.".to_string()),
|
||||
error_type: Some(TauriGenerateImageErrorType::NoProviderAvailable),
|
||||
error_details: None,
|
||||
});
|
||||
}
|
||||
|
||||
let result = handle_artcraft(
|
||||
&request,
|
||||
&app_env_configs,
|
||||
&storyteller_creds_manager,
|
||||
).await;
|
||||
|
||||
match result {
|
||||
Ok(success) => handle_success_behavior(&app, &task_database, &request, success).await,
|
||||
Err(err) => handle_error_behavior(&app, err).await,
|
||||
}
|
||||
}
|
||||
|
||||
// ── Result mapping ──
|
||||
|
||||
async fn handle_success_behavior(
|
||||
app: &AppHandle,
|
||||
task_database: &TaskDatabase,
|
||||
request: &TauriGenerateImageRequest,
|
||||
success: TaskEnqueueSuccess,
|
||||
) -> Response<TauriGenerateImageResponse, TauriGenerateImageErrorType, ()> {
|
||||
|
||||
|
||||
// Insert into task database
|
||||
let db_result = success
|
||||
.insert_into_task_database_with_frontend_payload(
|
||||
&task_database,
|
||||
request.frontend_caller,
|
||||
request.frontend_subscriber_id.as_deref(),
|
||||
request.frontend_subscriber_payload.as_deref(),
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Err(err) = db_result {
|
||||
error!("Failed to create task in database: {:?}", err);
|
||||
}
|
||||
|
||||
/*
|
||||
let usage_type = if is_image_to_image {
|
||||
ArtcraftUsageType::ImageToResult
|
||||
} else {
|
||||
ArtcraftUsageType::TextToResult
|
||||
};
|
||||
|
||||
if let Err(err) = artcraft_usage_tracker.record_image_generation(num_images, usage_type, ArtcraftUsagePage::ImagePage) {
|
||||
// NB: Fail open.
|
||||
warn!("Failed to report usage: {:?}", err);
|
||||
}
|
||||
*/
|
||||
|
||||
let event = GenerationEnqueueSuccessEvent {
|
||||
action: success.to_frontend_event_action(),
|
||||
service: success.to_frontend_event_service(),
|
||||
model: success.model,
|
||||
};
|
||||
|
||||
if let Err(err) = event.send(&app) {
|
||||
error!("Failed to emit event: {:?}", err);
|
||||
}
|
||||
|
||||
CreditsBalanceChangedEvent{}.send_infallible(&app);
|
||||
|
||||
Ok(TauriGenerateImageResponse {}.into())
|
||||
}
|
||||
|
||||
async fn handle_error_behavior(
|
||||
app: &AppHandle,
|
||||
err: GenerateError,
|
||||
) -> Response<TauriGenerateImageResponse, TauriGenerateImageErrorType, ()> {
|
||||
error!("generate_image_command error: {:?}", err);
|
||||
|
||||
notify_frontend_of_errors(&app, &err).await;
|
||||
|
||||
let error_type = match &err {
|
||||
GenerateError::BadInput(_) => TauriGenerateImageErrorType::BadInput,
|
||||
GenerateError::MissingCredentials(_) => TauriGenerateImageErrorType::NeedsStorytellerCredentials,
|
||||
GenerateError::NoProviderAvailable => TauriGenerateImageErrorType::NoProviderAvailable,
|
||||
GenerateError::BillingIssue(_) => TauriGenerateImageErrorType::BillingIssue,
|
||||
_ => TauriGenerateImageErrorType::ServerError,
|
||||
};
|
||||
|
||||
// TODO:
|
||||
/*
|
||||
let mut status = CommandErrorStatus::ServerError;
|
||||
let mut error_type = EnqueueTextToImageErrorType::ServerError;
|
||||
let mut error_message = "A server error occurred. Please try again. If it continues, please tell our staff about the problem.";
|
||||
|
||||
match err {
|
||||
GenerateError::BadInput(BadInputReason::NoModelSpecified) => {
|
||||
status = CommandErrorStatus::BadRequest;
|
||||
error_type = EnqueueTextToImageErrorType::ModelNotSpecified;
|
||||
error_message = "No model specified for image generation";
|
||||
}
|
||||
GenerateError::NoProviderAvailable => {
|
||||
status = CommandErrorStatus::ServerError;
|
||||
error_type = EnqueueTextToImageErrorType::NoProviderAvailable;
|
||||
error_message = "No configured provider available for image generation";
|
||||
}
|
||||
GenerateError::MissingCredentials(MissingCredentialsReason::NeedsFalApiKey) => {
|
||||
status = CommandErrorStatus::Unauthorized;
|
||||
error_type = EnqueueTextToImageErrorType::NeedsFalApiKey;
|
||||
error_message = "You need to set a FAL api key";
|
||||
},
|
||||
_ => {}, // Fall-through
|
||||
}
|
||||
|
||||
Err(CommandErrorResponseWrapper {
|
||||
status,
|
||||
error_message: Some(error_message.to_string()),
|
||||
error_type: Some(error_type),
|
||||
error_details: None,
|
||||
})
|
||||
*/
|
||||
|
||||
Err(CommandErrorResponseWrapper {
|
||||
status: CommandErrorStatus::ServerError,
|
||||
error_message: Some(format!("{:?}", err)),
|
||||
error_type: Some(error_type),
|
||||
error_details: None,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod generate_image_command;
|
||||
pub mod providers;
|
||||
pub mod tauri_generate_image_request;
|
||||
pub mod tauri_image_model;
|
||||
pub mod utils;
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
use log::info;
|
||||
|
||||
use crate::core::commands::enqueue::generate_error::{GenerateError, MissingCredentialsReason};
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::generate::generate_image::providers::artcraft::handle_artcraft_via_legacy::handle_artcraft_via_legacy;
|
||||
use crate::core::commands::generate::generate_image::providers::artcraft::handle_artcraft_via_omni_endpoint::handle_artcraft_via_omni_endpoint;
|
||||
use crate::core::commands::generate::generate_image::tauri_generate_image_request::TauriGenerateImageRequest;
|
||||
use crate::core::commands::generate::generate_image::tauri_image_model::TauriImageModel;
|
||||
use crate::core::commands::generate::generate_image::utils::parse_semantic_media_files::parse_semantic_media_files;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
|
||||
|
||||
/// Dispatch an image generation request to Artcraft.
|
||||
pub async fn handle_artcraft(
|
||||
request: &TauriGenerateImageRequest,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
// Resolve credentials.
|
||||
let creds = match storyteller_creds_manager.get_credentials()? {
|
||||
Some(creds) => creds,
|
||||
None => return Err(GenerateError::MissingCredentials(MissingCredentialsReason::NeedsStorytellerCredentials)),
|
||||
};
|
||||
|
||||
// Resolve semantic media files (upload raw bytes if provided).
|
||||
let semantic_media_files = parse_semantic_media_files(
|
||||
request,
|
||||
&creds,
|
||||
&app_env_configs.storyteller_host,
|
||||
).await?;
|
||||
|
||||
let use_legacy = request.model
|
||||
.is_some_and(|m| is_legacy_only_model(m));
|
||||
|
||||
if use_legacy {
|
||||
info!("Model {:?} is legacy-only, routing to artcraft_router path.", request.model);
|
||||
handle_artcraft_via_legacy(
|
||||
request,
|
||||
&semantic_media_files,
|
||||
&creds,
|
||||
app_env_configs,
|
||||
).await
|
||||
} else {
|
||||
handle_artcraft_via_omni_endpoint(
|
||||
request,
|
||||
&semantic_media_files,
|
||||
&creds,
|
||||
app_env_configs,
|
||||
).await
|
||||
}
|
||||
}
|
||||
|
||||
fn is_legacy_only_model(model: TauriImageModel) -> bool {
|
||||
matches!(
|
||||
model,
|
||||
TauriImageModel::GrokImage
|
||||
| TauriImageModel::Recraft3
|
||||
| TauriImageModel::Midjourney
|
||||
| TauriImageModel::FluxProKontextMax
|
||||
| TauriImageModel::QwenEdit2511Angles
|
||||
| TauriImageModel::Flux2LoraAngles
|
||||
| TauriImageModel::FluxDevJuggernaut
|
||||
)
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
|
||||
use crate::core::commands::enqueue::generate_error::GenerateError;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::generate::generate_image::providers::artcraft::legacy::artcraft_flux_2_lora_angles::handle_flux_2_lora_angles;
|
||||
use crate::core::commands::generate::generate_image::providers::artcraft::legacy::artcraft_flux_dev_juggernaut_inpaint::handle_flux_dev_juggernaut_inpaint;
|
||||
use crate::core::commands::generate::generate_image::providers::artcraft::legacy::artcraft_flux_pro_kontext_edit::handle_flux_pro_kontext_edit;
|
||||
use crate::core::commands::generate::generate_image::providers::artcraft::legacy::artcraft_qwen_edit_2511_angles::handle_qwen_edit_2511_angles;
|
||||
use crate::core::commands::generate::generate_image::tauri_generate_image_request::TauriGenerateImageRequest;
|
||||
use crate::core::commands::generate::generate_image::tauri_image_model::TauriImageModel;
|
||||
use crate::core::commands::generate::generate_image::utils::parse_semantic_media_files::SemanticMediaFiles;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
|
||||
/// Handle image generation via legacy model-specific paths.
|
||||
///
|
||||
/// Dispatches to dedicated handlers for each supported legacy model.
|
||||
pub async fn handle_artcraft_via_legacy(
|
||||
request: &TauriGenerateImageRequest,
|
||||
semantic_media_files: &SemanticMediaFiles,
|
||||
creds: &StorytellerCredentialSet,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
let model = request.model.ok_or(GenerateError::no_model_specified())?;
|
||||
|
||||
match model {
|
||||
TauriImageModel::FluxDevJuggernaut => {
|
||||
handle_flux_dev_juggernaut_inpaint(request, semantic_media_files, creds, app_env_configs).await
|
||||
}
|
||||
TauriImageModel::FluxProKontextMax => {
|
||||
handle_flux_pro_kontext_edit(request, semantic_media_files, creds, app_env_configs).await
|
||||
}
|
||||
TauriImageModel::QwenEdit2511Angles => {
|
||||
handle_qwen_edit_2511_angles(request, semantic_media_files, creds, app_env_configs).await
|
||||
}
|
||||
TauriImageModel::Flux2LoraAngles => {
|
||||
handle_flux_2_lora_angles(request, semantic_media_files, creds, app_env_configs).await
|
||||
}
|
||||
TauriImageModel::Midjourney | TauriImageModel::Recraft3 => {
|
||||
Err(GenerateError::NotYetImplemented(
|
||||
format!("Model {:?} is not supported by provider Artcraft", model),
|
||||
))
|
||||
}
|
||||
other => {
|
||||
Err(GenerateError::NotYetImplemented(
|
||||
format!("Model {:?} was incorrectly handled", other),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest;
|
||||
use artcraft_client::endpoints::omni_gen::generate::image::omni_gen_image::omni_gen_image_generate;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use log::{error, info};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use uuid_utils::uuid::generate_random_uuid;
|
||||
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use crate::core::commands::enqueue::generate_error::GenerateError;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::api_adapters::models::image::tauri_image_model_to_enums_model::tauri_image_model_to_enums_model;
|
||||
use crate::core::api_adapters::models::image::tauri_image_model_to_generation_model::tauri_image_model_to_generation_model;
|
||||
use crate::core::commands::generate::generate_image::tauri_generate_image_request::TauriGenerateImageRequest;
|
||||
use crate::core::commands::generate::generate_image::utils::parse_semantic_media_files::SemanticMediaFiles;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
|
||||
pub async fn handle_artcraft_via_omni_endpoint(
|
||||
request: &TauriGenerateImageRequest,
|
||||
semantic_media_files: &SemanticMediaFiles,
|
||||
creds: &StorytellerCredentialSet,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
let tauri_model = request.model.ok_or(GenerateError::no_model_specified())?;
|
||||
|
||||
let omni_api_model = tauri_image_model_to_enums_model(tauri_model)
|
||||
.ok_or(GenerateError::NotYetImplemented(
|
||||
format!("Model {:?} is not supported via the omni endpoint", tauri_model),
|
||||
))?;
|
||||
|
||||
let uuid_idempotency_token = generate_random_uuid();
|
||||
|
||||
let image_media_tokens = get_image_media_tokens(request, semantic_media_files);
|
||||
|
||||
let omni_request = OmniGenImageCostAndGenerateRequest {
|
||||
idempotency_token: Some(uuid_idempotency_token),
|
||||
model: Some(omni_api_model),
|
||||
prompt: request.prompt.clone(),
|
||||
image_media_tokens,
|
||||
resolution: request.resolution,
|
||||
aspect_ratio: request.aspect_ratio,
|
||||
quality: request.quality,
|
||||
image_batch_count: request.batch_size.map(|n| n as u16),
|
||||
adjust_horizontal_angle: request.adjust_horizontal_angle,
|
||||
adjust_vertical_angle: request.adjust_vertical_angle,
|
||||
adjust_zoom: request.adjust_zoom,
|
||||
};
|
||||
|
||||
info!("Sending image generation via omni endpoint: model={:?}", omni_api_model);
|
||||
|
||||
let response = omni_gen_image_generate(
|
||||
&app_env_configs.storyteller_host,
|
||||
Some(creds),
|
||||
omni_request,
|
||||
).await.map_err(|err| {
|
||||
error!("Omni image generation failed: {:?}", err);
|
||||
GenerateError::from(err)
|
||||
})?;
|
||||
|
||||
info!("Omni image generation succeeded: job_token={}", response.inference_job_token.as_str());
|
||||
|
||||
let generation_model = tauri_image_model_to_generation_model(tauri_model);
|
||||
|
||||
Ok(TaskEnqueueSuccess {
|
||||
task_type: TaskType::ImageGeneration,
|
||||
model: Some(generation_model),
|
||||
provider: GenerationProvider::Artcraft,
|
||||
provider_job_id: Some(response.inference_job_token.to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
fn get_image_media_tokens(
|
||||
request: &TauriGenerateImageRequest,
|
||||
semantic_media_files: &SemanticMediaFiles,
|
||||
) -> Option<Vec<MediaFileToken>> {
|
||||
let num_images = request.image_media_tokens.as_deref()
|
||||
.map(|t| t.len())
|
||||
.unwrap_or(0);
|
||||
|
||||
let mut image_media_tokens = Vec::with_capacity(num_images + 2);
|
||||
|
||||
if let Some(canvas_token) = &semantic_media_files.canvas_image_media_token {
|
||||
image_media_tokens.push(canvas_token.clone());
|
||||
}
|
||||
|
||||
if let Some(scene_token) = &semantic_media_files.scene_image_media_token {
|
||||
image_media_tokens.push(scene_token.clone());
|
||||
}
|
||||
|
||||
if let Some(media_tokens) = &request.image_media_tokens {
|
||||
image_media_tokens.extend(media_tokens.clone());
|
||||
}
|
||||
|
||||
if image_media_tokens.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(image_media_tokens)
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
use log::{error, info};
|
||||
|
||||
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use artcraft_router::api::common_image_model::CommonImageModel;
|
||||
use artcraft_router::api::image_list_ref::ImageListRef;
|
||||
use artcraft_router::api::provider::Provider;
|
||||
use artcraft_router::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use artcraft_router::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use artcraft_router::client::router_client::RouterClient;
|
||||
use artcraft_router::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
|
||||
use crate::core::commands::enqueue::generate_error::GenerateError;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::generate::generate_image::tauri_generate_image_request::TauriGenerateImageRequest;
|
||||
use crate::core::commands::generate::generate_image::utils::parse_semantic_media_files::SemanticMediaFiles;
|
||||
use crate::core::events::generation_events::common::GenerationModel;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
|
||||
/// Handle Flux2LoraAngles image editing via the artcraft_router.
|
||||
pub async fn handle_flux_2_lora_angles(
|
||||
request: &TauriGenerateImageRequest,
|
||||
semantic_media_files: &SemanticMediaFiles,
|
||||
creds: &StorytellerCredentialSet,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
let client = RouterClient::Artcraft(RouterArtcraftClient::new(
|
||||
app_env_configs.storyteller_host.clone(),
|
||||
creds.clone(),
|
||||
));
|
||||
|
||||
let image_inputs = build_image_inputs(request, semantic_media_files);
|
||||
|
||||
let router_request = GenerateImageRequest {
|
||||
model: CommonImageModel::Flux2LoraAngles,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: request.prompt.clone(),
|
||||
image_inputs,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: request.batch_size.map(|n| n as u16),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: request.adjust_horizontal_angle,
|
||||
vertical_angle: request.adjust_vertical_angle,
|
||||
zoom: request.adjust_zoom,
|
||||
};
|
||||
|
||||
let plan = router_request.build()?;
|
||||
|
||||
info!("Flux2LoraAngles plan: {:?}", plan);
|
||||
|
||||
let response = plan.generate_image(&client).await.map_err(|err| {
|
||||
error!("Flux2LoraAngles generation failed: {:?}", err);
|
||||
GenerateError::from(err)
|
||||
})?;
|
||||
|
||||
let job_id = response
|
||||
.get_artcraft_payload()
|
||||
.map(|p| p.inference_job_token.to_string())
|
||||
.ok_or(GenerateError::ResponseHadNoJobTokens)?;
|
||||
|
||||
info!("Flux2LoraAngles succeeded: job_id={}", job_id);
|
||||
|
||||
Ok(TaskEnqueueSuccess {
|
||||
task_type: TaskType::ImageGeneration,
|
||||
model: Some(GenerationModel::Flux2LoraAngles),
|
||||
provider: GenerationProvider::Artcraft,
|
||||
provider_job_id: Some(job_id),
|
||||
})
|
||||
}
|
||||
|
||||
fn build_image_inputs(
|
||||
request: &TauriGenerateImageRequest,
|
||||
semantic_media_files: &SemanticMediaFiles,
|
||||
) -> Option<ImageListRef> {
|
||||
let mut tokens = Vec::new();
|
||||
|
||||
if let Some(scene_token) = &semantic_media_files.scene_image_media_token {
|
||||
tokens.push(scene_token.clone());
|
||||
}
|
||||
if let Some(canvas_token) = &semantic_media_files.canvas_image_media_token {
|
||||
tokens.push(canvas_token.clone());
|
||||
}
|
||||
if let Some(media_tokens) = &request.image_media_tokens {
|
||||
tokens.extend(media_tokens.clone());
|
||||
}
|
||||
|
||||
if tokens.is_empty() { None } else { Some(ImageListRef::MediaFileTokens(tokens)) }
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
use log::{error, info};
|
||||
|
||||
use artcraft_api_defs::generate::image::inpaint::flux_dev_juggernaut_inpaint_image::{
|
||||
FluxDevJuggernautInpaintImageNumImages, FluxDevJuggernautInpaintImageRequest,
|
||||
};
|
||||
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use artcraft_client::endpoints::generate::image::inpaint::flux_dev_juggernaut_inpaint_image::flux_dev_juggernaut_inpaint_image;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use uuid_utils::uuid::generate_random_uuid;
|
||||
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::generate::generate_image::tauri_generate_image_request::TauriGenerateImageRequest;
|
||||
use crate::core::commands::generate::generate_image::utils::parse_semantic_media_files::SemanticMediaFiles;
|
||||
use crate::core::events::generation_events::common::GenerationModel;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
|
||||
/// Handle FluxDevJuggernaut inpainting via the legacy dedicated endpoint.
|
||||
///
|
||||
/// Requires a source image (from scene, canvas, or image_media_tokens) and an inpainting mask.
|
||||
pub async fn handle_flux_dev_juggernaut_inpaint(
|
||||
request: &TauriGenerateImageRequest,
|
||||
semantic_media_files: &SemanticMediaFiles,
|
||||
creds: &StorytellerCredentialSet,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
let image_media_token = get_first_image_token(request, semantic_media_files)
|
||||
.ok_or(GenerateError::required_source_image_not_provided())?;
|
||||
|
||||
let mask_media_token = semantic_media_files.inpainting_mask_image_media_token.clone()
|
||||
.ok_or(GenerateError::required_source_image_mask_not_provided())?;
|
||||
|
||||
let num_images = match request.batch_size {
|
||||
None => None,
|
||||
Some(1) => Some(FluxDevJuggernautInpaintImageNumImages::One),
|
||||
Some(2) => Some(FluxDevJuggernautInpaintImageNumImages::Two),
|
||||
Some(3) => Some(FluxDevJuggernautInpaintImageNumImages::Three),
|
||||
Some(4) => Some(FluxDevJuggernautInpaintImageNumImages::Four),
|
||||
Some(other) => {
|
||||
return Err(GenerateError::BadInput(BadInputReason::InvalidNumberOfRequestedImages {
|
||||
min: 1,
|
||||
max: 4,
|
||||
requested: other,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
let uuid_idempotency_token = generate_random_uuid();
|
||||
|
||||
let inpaint_request = FluxDevJuggernautInpaintImageRequest {
|
||||
uuid_idempotency_token,
|
||||
prompt: request.prompt.clone(),
|
||||
image_media_token,
|
||||
mask_media_token,
|
||||
num_images,
|
||||
};
|
||||
|
||||
info!("Calling FluxDevJuggernaut inpaint...");
|
||||
|
||||
let response = flux_dev_juggernaut_inpaint_image(
|
||||
&app_env_configs.storyteller_host,
|
||||
Some(creds),
|
||||
inpaint_request,
|
||||
).await.map_err(|err| {
|
||||
error!("FluxDevJuggernaut inpaint failed: {:?}", err);
|
||||
GenerateError::from(err)
|
||||
})?;
|
||||
|
||||
info!("FluxDevJuggernaut inpaint succeeded: job_token={}", response.inference_job_token);
|
||||
|
||||
Ok(TaskEnqueueSuccess {
|
||||
task_type: TaskType::ImageInpaintEdit,
|
||||
model: Some(GenerationModel::FluxDevJuggernaut),
|
||||
provider: GenerationProvider::Artcraft,
|
||||
provider_job_id: Some(response.inference_job_token.to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
fn get_first_image_token(
|
||||
request: &TauriGenerateImageRequest,
|
||||
semantic_media_files: &SemanticMediaFiles,
|
||||
) -> Option<MediaFileToken> {
|
||||
if let Some(scene_token) = &semantic_media_files.scene_image_media_token {
|
||||
return Some(scene_token.clone());
|
||||
}
|
||||
if let Some(canvas_token) = &semantic_media_files.canvas_image_media_token {
|
||||
return Some(canvas_token.clone());
|
||||
}
|
||||
if let Some(media_tokens) = &request.image_media_tokens {
|
||||
if let Some(token) = media_tokens.first() {
|
||||
return Some(token.clone());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
use log::{error, info};
|
||||
|
||||
use artcraft_api_defs::generate::image::edit::flux_pro_kontext_max_edit_image::{
|
||||
FluxProKontextMaxEditImageNumImages, FluxProKontextMaxEditImageRequest,
|
||||
};
|
||||
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use artcraft_client::endpoints::generate::image::edit::flux_pro_kontext_max_edit_image::flux_pro_kontext_max_edit_image;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use uuid_utils::uuid::generate_random_uuid;
|
||||
|
||||
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::generate::generate_image::tauri_generate_image_request::TauriGenerateImageRequest;
|
||||
use crate::core::commands::generate::generate_image::utils::parse_semantic_media_files::SemanticMediaFiles;
|
||||
use crate::core::events::generation_events::common::GenerationModel;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
|
||||
/// Handle FluxProKontextMax image editing via the legacy dedicated endpoint.
|
||||
///
|
||||
/// Consolidates both the "edit" and "inpaint" legacy paths — both call the same
|
||||
/// Kontext Max edit endpoint with a single source image.
|
||||
pub async fn handle_flux_pro_kontext_edit(
|
||||
request: &TauriGenerateImageRequest,
|
||||
semantic_media_files: &SemanticMediaFiles,
|
||||
creds: &StorytellerCredentialSet,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
let image_media_token = get_first_image_token(request, semantic_media_files)
|
||||
.ok_or(GenerateError::BadInput(BadInputReason::WrongImageArguments(
|
||||
"No input image specified for Flux Kontext Max edit".to_string(),
|
||||
)))?;
|
||||
|
||||
let num_images = match request.batch_size {
|
||||
None => None,
|
||||
Some(1) => Some(FluxProKontextMaxEditImageNumImages::One),
|
||||
Some(2) => Some(FluxProKontextMaxEditImageNumImages::Two),
|
||||
Some(3) => Some(FluxProKontextMaxEditImageNumImages::Three),
|
||||
Some(4) => Some(FluxProKontextMaxEditImageNumImages::Four),
|
||||
Some(other) => {
|
||||
return Err(GenerateError::BadInput(BadInputReason::InvalidNumberOfRequestedImages {
|
||||
min: 1,
|
||||
max: 4,
|
||||
requested: other,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
let uuid_idempotency_token = generate_random_uuid();
|
||||
|
||||
let kontext_request = FluxProKontextMaxEditImageRequest {
|
||||
uuid_idempotency_token,
|
||||
prompt: request.prompt.clone(),
|
||||
image_media_token,
|
||||
num_images,
|
||||
};
|
||||
|
||||
info!("Calling FluxProKontextMax edit...");
|
||||
|
||||
let response = flux_pro_kontext_max_edit_image(
|
||||
&app_env_configs.storyteller_host,
|
||||
Some(creds),
|
||||
kontext_request,
|
||||
).await.map_err(|err| {
|
||||
error!("FluxProKontextMax edit failed: {:?}", err);
|
||||
GenerateError::from(err)
|
||||
})?;
|
||||
|
||||
info!("FluxProKontextMax edit succeeded: job_token={}", response.inference_job_token);
|
||||
|
||||
Ok(TaskEnqueueSuccess {
|
||||
task_type: TaskType::ImageGeneration,
|
||||
model: Some(GenerationModel::FluxProKontextMax),
|
||||
provider: GenerationProvider::Artcraft,
|
||||
provider_job_id: Some(response.inference_job_token.to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
fn get_first_image_token(
|
||||
request: &TauriGenerateImageRequest,
|
||||
semantic_media_files: &SemanticMediaFiles,
|
||||
) -> Option<MediaFileToken> {
|
||||
if let Some(scene_token) = &semantic_media_files.scene_image_media_token {
|
||||
return Some(scene_token.clone());
|
||||
}
|
||||
if let Some(canvas_token) = &semantic_media_files.canvas_image_media_token {
|
||||
return Some(canvas_token.clone());
|
||||
}
|
||||
if let Some(media_tokens) = &request.image_media_tokens {
|
||||
if let Some(token) = media_tokens.first() {
|
||||
return Some(token.clone());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
use log::{error, info};
|
||||
|
||||
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use artcraft_router::api::common_image_model::CommonImageModel;
|
||||
use artcraft_router::api::image_list_ref::ImageListRef;
|
||||
use artcraft_router::api::provider::Provider;
|
||||
use artcraft_router::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use artcraft_router::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use artcraft_router::client::router_client::RouterClient;
|
||||
use artcraft_router::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
|
||||
use crate::core::commands::enqueue::generate_error::GenerateError;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::generate::generate_image::tauri_generate_image_request::TauriGenerateImageRequest;
|
||||
use crate::core::commands::generate::generate_image::utils::parse_semantic_media_files::SemanticMediaFiles;
|
||||
use crate::core::events::generation_events::common::GenerationModel;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
|
||||
/// Handle QwenEdit2511Angles image editing via the artcraft_router.
|
||||
pub async fn handle_qwen_edit_2511_angles(
|
||||
request: &TauriGenerateImageRequest,
|
||||
semantic_media_files: &SemanticMediaFiles,
|
||||
creds: &StorytellerCredentialSet,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
let client = RouterClient::Artcraft(RouterArtcraftClient::new(
|
||||
app_env_configs.storyteller_host.clone(),
|
||||
creds.clone(),
|
||||
));
|
||||
|
||||
let image_inputs = build_image_inputs(request, semantic_media_files);
|
||||
|
||||
let router_request = GenerateImageRequest {
|
||||
model: CommonImageModel::QwenEdit2511Angles,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: request.prompt.clone(),
|
||||
image_inputs,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: request.batch_size.map(|n| n as u16),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: request.adjust_horizontal_angle,
|
||||
vertical_angle: request.adjust_vertical_angle,
|
||||
zoom: request.adjust_zoom,
|
||||
};
|
||||
|
||||
let plan = router_request.build()?;
|
||||
|
||||
info!("QwenEdit2511Angles plan: {:?}", plan);
|
||||
|
||||
let response = plan.generate_image(&client).await.map_err(|err| {
|
||||
error!("QwenEdit2511Angles generation failed: {:?}", err);
|
||||
GenerateError::from(err)
|
||||
})?;
|
||||
|
||||
let job_id = response
|
||||
.get_artcraft_payload()
|
||||
.map(|p| p.inference_job_token.to_string())
|
||||
.ok_or(GenerateError::ResponseHadNoJobTokens)?;
|
||||
|
||||
info!("QwenEdit2511Angles succeeded: job_id={}", job_id);
|
||||
|
||||
Ok(TaskEnqueueSuccess {
|
||||
task_type: TaskType::ImageGeneration,
|
||||
model: Some(GenerationModel::QwenEdit2511Angles),
|
||||
provider: GenerationProvider::Artcraft,
|
||||
provider_job_id: Some(job_id),
|
||||
})
|
||||
}
|
||||
|
||||
fn build_image_inputs(
|
||||
request: &TauriGenerateImageRequest,
|
||||
semantic_media_files: &SemanticMediaFiles,
|
||||
) -> Option<ImageListRef> {
|
||||
let mut tokens = Vec::new();
|
||||
|
||||
if let Some(scene_token) = &semantic_media_files.scene_image_media_token {
|
||||
tokens.push(scene_token.clone());
|
||||
}
|
||||
if let Some(canvas_token) = &semantic_media_files.canvas_image_media_token {
|
||||
tokens.push(canvas_token.clone());
|
||||
}
|
||||
if let Some(media_tokens) = &request.image_media_tokens {
|
||||
tokens.extend(media_tokens.clone());
|
||||
}
|
||||
|
||||
if tokens.is_empty() { None } else { Some(ImageListRef::MediaFileTokens(tokens)) }
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
pub mod artcraft_flux_2_lora_angles;
|
||||
pub mod artcraft_flux_dev_juggernaut_inpaint;
|
||||
pub mod artcraft_flux_pro_kontext_edit;
|
||||
pub mod artcraft_qwen_edit_2511_angles;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
mod handle_artcraft;
|
||||
mod handle_artcraft_via_legacy;
|
||||
mod handle_artcraft_via_omni_endpoint;
|
||||
mod legacy;
|
||||
|
||||
pub use handle_artcraft::handle_artcraft;
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
use crate::core::api_adapters::aspect_ratio::convert::aspect_ratio_to_grok_image::aspect_ratio_to_grok_image;
|
||||
use crate::core::commands::enqueue::generate_error::GenerateError;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::deprecated::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
|
||||
use crate::core::events::generation_events::common::GenerationModel;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::services::grok::state::grok_credential_manager::GrokCredentialManager;
|
||||
use crate::services::grok::state::grok_image_prompt_queue::{GrokImagePromptQueue, PromptItem};
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use grok_client::requests::image_websocket::messages::websocket_client_message::ClientMessageAspectRatio;
|
||||
use uuid_utils::uuid::generate_random_uuid;
|
||||
use tauri::AppHandle;
|
||||
|
||||
pub async fn handle_grok(
|
||||
app: &AppHandle,
|
||||
request: &EnqueueTextToImageRequest,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
creds_manager: &GrokCredentialManager,
|
||||
grok_image_prompt_queue: &GrokImagePromptQueue,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
|
||||
|
||||
//// TODO: We can request population of the user info if absent or expired.
|
||||
//let websocket = get_websocket(
|
||||
// app,
|
||||
// creds_manager,
|
||||
// websocket_manager,
|
||||
//).await?;
|
||||
//
|
||||
//let _result = prompt_websocket_image(PromptWebsocketImageArgs {
|
||||
// websocket_wrapped: &websocket,
|
||||
// prompt: request.prompt.as_deref().unwrap_or_else(||""),
|
||||
//}).await?;
|
||||
//
|
||||
//let request_id = listen_for_websocket_request_id(ListenForWebsocketRequestIdArgs {
|
||||
// websocket: &websocket,
|
||||
// timeout: Duration::from_millis(10_000),
|
||||
//}).await?;
|
||||
//
|
||||
//let request_id = match request_id.request_id {
|
||||
// Some(request_id) => request_id.to_string(),
|
||||
// None => {
|
||||
// warn!("No request id from Grok image generation (websocket)");
|
||||
// return Err(GenerateError::ProviderFailure(ProviderFailureReason::GrokJobEnqueueFailed));
|
||||
// }
|
||||
//};
|
||||
//
|
||||
//info!("Successfully enqueued MidJourney. Job token: {}", request_id);
|
||||
|
||||
let job_id = generate_random_uuid();
|
||||
|
||||
let prompt = request.prompt
|
||||
.as_deref()
|
||||
.map(|prompt| prompt.trim().to_string())
|
||||
.unwrap_or_else(|| "".to_string());
|
||||
|
||||
let aspect_ratio = get_aspect_ratio(request);
|
||||
|
||||
grok_image_prompt_queue.enqueue(PromptItem {
|
||||
task_id: job_id.clone(),
|
||||
prompt,
|
||||
aspect_ratio: aspect_ratio.unwrap_or(ClientMessageAspectRatio::Square),
|
||||
})?;
|
||||
|
||||
Ok(TaskEnqueueSuccess {
|
||||
provider: GenerationProvider::Grok,
|
||||
model: Some(GenerationModel::GrokImage),
|
||||
provider_job_id: Some(job_id),
|
||||
task_type: TaskType::ImageGeneration,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_aspect_ratio(request: &EnqueueTextToImageRequest) -> Option<ClientMessageAspectRatio> {
|
||||
if let Some(common_aspect_ratio) = request.common_aspect_ratio {
|
||||
// Handle modern aspect ratio
|
||||
let aspect = aspect_ratio_to_grok_image(common_aspect_ratio);
|
||||
return Some(aspect);
|
||||
}
|
||||
|
||||
if let Some(aspect_ratio) = request.aspect_ratio {
|
||||
// Handle deprecated aspect ratio
|
||||
return match aspect_ratio {
|
||||
TextToImageSize::Auto => Some(ClientMessageAspectRatio::Square),
|
||||
TextToImageSize::Square => Some(ClientMessageAspectRatio::Square),
|
||||
TextToImageSize::Wide => Some(ClientMessageAspectRatio::WideThreeByTwo),
|
||||
TextToImageSize::Tall => Some(ClientMessageAspectRatio::TallTwoByThree),
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
pub (super) mod handle_grok;
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
use crate::core::commands::enqueue::generate_error::{GenerateError, MissingCredentialsReason, ProviderFailureReason};
|
||||
use crate::core::commands::deprecated::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageSize};
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::deprecated::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::functional_events::canvas_background_removal_complete_event::CanvasBackgroundRemovalCompleteEvent;
|
||||
use crate::core::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
|
||||
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
|
||||
use crate::core::events::warning_events::flash_user_input_error_event::FlashUserInputErrorEvent;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::ProviderPriorityStore;
|
||||
use crate::services::midjourney::state::midjourney_credential_manager::MidjourneyCredentialManager;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use uuid_utils::uuid::generate_random_uuid;
|
||||
use log::{error, info};
|
||||
use midjourney_client::client::midjourney_hostname::MidjourneyHostname;
|
||||
use midjourney_client::endpoints::submit_job::{submit_job, SubmitJobRequest};
|
||||
use midjourney_client::error::midjourney_api_error::MidjourneyApiError;
|
||||
use midjourney_client::recipes::channel_id::ChannelId;
|
||||
use midjourney_client::recipes::text_to_image::{text_to_image, TextToImageError, TextToImageRequest};
|
||||
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
|
||||
use openai_sora_client::recipes::simple_image_gen_with_session_auto_renew::{simple_image_gen_with_session_auto_renew, SimpleImageGenAutoRenewRequest};
|
||||
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
|
||||
use std::time::Duration;
|
||||
use tauri::AppHandle;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
pub async fn handle_midjourney(
|
||||
app: &AppHandle,
|
||||
request: &EnqueueTextToImageRequest,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
mj_creds_manager: &MidjourneyCredentialManager,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
|
||||
let creds = match mj_creds_manager.maybe_copy_cookie_store() {
|
||||
Ok(Some(creds)) => creds,
|
||||
Ok(None) => {
|
||||
error!("Midjourney credentials not found.");
|
||||
ShowProviderLoginModalEvent::send_for_provider(GenerationProvider::Midjourney, &app);
|
||||
return Err(GenerateError::needs_midjourney_credentials());
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Error reading Midjourney credentials: {:?}", err);
|
||||
ShowProviderLoginModalEvent::send_for_provider(GenerationProvider::Midjourney, &app);
|
||||
return Err(GenerateError::needs_midjourney_credentials());
|
||||
},
|
||||
};
|
||||
|
||||
// TODO: We can request population of the user info if absent or expired.
|
||||
|
||||
let user_info = match mj_creds_manager.maybe_copy_user_info() {
|
||||
Ok(Some(user_info)) => user_info,
|
||||
Ok(None) => {
|
||||
return Err(GenerateError::MissingCredentials(MissingCredentialsReason::NeedsMidjourneyUserInfo));
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Error reading Midjourney user info: {:?}", err);
|
||||
return Err(GenerateError::MissingCredentials(MissingCredentialsReason::NeedsMidjourneyUserInfo));
|
||||
},
|
||||
};
|
||||
|
||||
let channel_id = match user_info.user_id {
|
||||
Some(user_id) => ChannelId::UserId(user_id),
|
||||
None => {
|
||||
error!("Midjourney user info does not contain a user ID.");
|
||||
return Err(GenerateError::MissingCredentials(MissingCredentialsReason::NeedsMidjourneyUserId));
|
||||
}
|
||||
};
|
||||
|
||||
info!("Calling midjourney ...");
|
||||
|
||||
let cookie_header = creds.to_cookie_string();
|
||||
|
||||
let prompt = request.prompt
|
||||
.as_deref()
|
||||
.unwrap_or("");
|
||||
|
||||
let result = text_to_image(TextToImageRequest {
|
||||
prompt,
|
||||
channel_id: &channel_id,
|
||||
hostname: MidjourneyHostname::Standard,
|
||||
cookie_header,
|
||||
}).await;
|
||||
|
||||
let result = match result {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
error!("Failed to use MidJourney: {:?}", err);
|
||||
return Err(GenerateError::from(err));
|
||||
}
|
||||
};
|
||||
|
||||
let job_id = match result.maybe_job_id {
|
||||
Some(job_id) => job_id,
|
||||
None => {
|
||||
error!("Failed to enqueue MidJourney: No job ID returned.");
|
||||
return handle_midjourney_errors(app, result.maybe_errors);
|
||||
}
|
||||
};
|
||||
|
||||
info!("Successfully enqueued MidJourney. Job token: {}", job_id);
|
||||
|
||||
Ok(TaskEnqueueSuccess {
|
||||
provider: GenerationProvider::Midjourney,
|
||||
model: Some(GenerationModel::Midjourney),
|
||||
provider_job_id: Some(job_id),
|
||||
task_type: TaskType::ImageGeneration,
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_midjourney_errors(
|
||||
app: &AppHandle,
|
||||
maybe_errors: Option<Vec<TextToImageError>>
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
if let Some(errors) = maybe_errors {
|
||||
if !errors.is_empty() {
|
||||
let messages: Vec<String> = errors.iter()
|
||||
.map(|e| format!("{:?}", e))
|
||||
.collect();
|
||||
|
||||
let combined_message = messages.join("; ");
|
||||
|
||||
let event = FlashUserInputErrorEvent {
|
||||
message: format!("Midjourney Error: {}", combined_message),
|
||||
};
|
||||
|
||||
if let Err(err) = event.send(&app) {
|
||||
error!("Failed to send FlashUserInputErrorEvent: {:?}", err); // Fail open
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(GenerateError::ProviderFailure(ProviderFailureReason::MidjourneyJobEnqueueFailed))
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
pub (super) mod handle_midjourney;
|
||||
@@ -0,0 +1 @@
|
||||
pub mod artcraft;
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
use crate::core::api_adapters::aspect_ratio::convert::aspect_ratio_to_grok_image::aspect_ratio_to_grok_image;
|
||||
use crate::core::api_adapters::aspect_ratio::convert::aspect_ratio_to_sora_native_gpt_image_1::aspect_ratio_to_sora_native_gpt_image_1;
|
||||
use crate::core::commands::enqueue::generate_error::GenerateError;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::deprecated::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
|
||||
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use errors::AnyhowResult;
|
||||
use grok_client::requests::image_websocket::messages::websocket_client_message::ClientMessageAspectRatio;
|
||||
use log::{error, info};
|
||||
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
|
||||
use openai_sora_client::recipes::simple_image_gen_with_session_auto_renew::{simple_image_gen_with_session_auto_renew, SimpleImageGenAutoRenewRequest};
|
||||
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
|
||||
use std::time::Duration;
|
||||
use tauri::AppHandle;
|
||||
|
||||
const SORA_SIMPLE_IMAGE_GEN_TIMEOUT : Duration = Duration::from_millis(1000 * 30); // 30 seconds
|
||||
|
||||
pub async fn handle_gpt_image_1_sora_text_to_image(
|
||||
request: &EnqueueTextToImageRequest,
|
||||
app: &AppHandle,
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
sora_task_queue: &SoraTaskQueue,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
|
||||
let mut creds = match sora_creds_manager.get_credentials() {
|
||||
Ok(Some(creds)) => creds,
|
||||
Ok(None) => {
|
||||
error!("Sora credentials not found.");
|
||||
ShowProviderLoginModalEvent::send_for_provider(GenerationProvider::Sora, &app);
|
||||
return Err(GenerateError::needs_sora_credentials());
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Error reading Sora credentials: {:?}", err);
|
||||
ShowProviderLoginModalEvent::send_for_provider(GenerationProvider::Sora, &app);
|
||||
return Err(GenerateError::needs_sora_credentials());
|
||||
}
|
||||
};
|
||||
|
||||
let credential_updated = maybe_upgrade_or_renew_session(&mut creds)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Failed to upgrade or renew session: {:?}", err);
|
||||
err
|
||||
})?;
|
||||
|
||||
if credential_updated {
|
||||
info!("Storing updated credentials");
|
||||
sora_creds_manager.set_credentials(&creds)?;
|
||||
}
|
||||
|
||||
let prompt = request.prompt.as_deref().unwrap_or("");
|
||||
|
||||
let aspect_ratio = get_aspect_ratio(request)
|
||||
.unwrap_or(ImageSize::Square);
|
||||
|
||||
let num_images = match request.number_images {
|
||||
None => NumImages::One,
|
||||
Some(1) => NumImages::One,
|
||||
Some(2) => NumImages::Two,
|
||||
Some(3) => NumImages::Two, // NB: There is no option for three images!
|
||||
Some(4) => NumImages::Four,
|
||||
_ => NumImages::One,
|
||||
};
|
||||
|
||||
let result =
|
||||
simple_image_gen_with_session_auto_renew(SimpleImageGenAutoRenewRequest {
|
||||
prompt: prompt.to_string(),
|
||||
num_images,
|
||||
image_size: aspect_ratio,
|
||||
credentials: &creds,
|
||||
request_timeout: Some(SORA_SIMPLE_IMAGE_GEN_TIMEOUT),
|
||||
}).await;
|
||||
|
||||
let (response, maybe_new_creds) =
|
||||
match result {
|
||||
Ok((response, maybe_new_creds)) => (response, maybe_new_creds),
|
||||
Err(err) => {
|
||||
let event = GenerationEnqueueFailureEvent {
|
||||
action: GenerationAction::GenerateImage,
|
||||
service: GenerationServiceProvider::Sora,
|
||||
model: None,
|
||||
reason: None,
|
||||
};
|
||||
|
||||
if let Err(err) = event.send(app) {
|
||||
error!("Failed to emit event: {:?}", err); // Fail open.
|
||||
}
|
||||
|
||||
return Err(GenerateError::from(err));
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(new_creds) = maybe_new_creds {
|
||||
info!("Storing updated credentials.");
|
||||
sora_creds_manager.set_credentials(&new_creds)?;
|
||||
}
|
||||
|
||||
info!("New Sora Task ID: {:?} ", response.task_id);
|
||||
|
||||
sora_task_queue.insert(&response.task_id)?;
|
||||
|
||||
Ok(TaskEnqueueSuccess {
|
||||
provider: GenerationProvider::Sora,
|
||||
task_type: TaskType::ImageGeneration,
|
||||
model: Some(GenerationModel::GptImage1),
|
||||
provider_job_id: Some(response.task_id.to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
fn get_aspect_ratio(request: &EnqueueTextToImageRequest) -> Option<ImageSize> {
|
||||
if let Some(common_aspect_ratio) = request.common_aspect_ratio {
|
||||
// Handle modern aspect ratio
|
||||
let aspect = aspect_ratio_to_sora_native_gpt_image_1(common_aspect_ratio);
|
||||
return Some(aspect);
|
||||
}
|
||||
|
||||
if let Some(aspect_ratio) = request.aspect_ratio {
|
||||
// Handle deprecated aspect ratio
|
||||
return match aspect_ratio {
|
||||
TextToImageSize::Auto => Some(ImageSize::Square),
|
||||
TextToImageSize::Square => Some(ImageSize::Square),
|
||||
TextToImageSize::Wide => Some(ImageSize::Wide),
|
||||
TextToImageSize::Tall => Some(ImageSize::Tall),
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
use crate::core::commands::enqueue::generate_error::GenerateError;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::EnqueueTextToImageRequest;
|
||||
use crate::core::commands::enqueue::text_to_image::sora::handle_gpt_image_1_sora_text_to_image::handle_gpt_image_1_sora_text_to_image;
|
||||
use crate::core::commands::deprecated::text_to_image::enqueue_text_to_image_command::EnqueueTextToImageRequest;
|
||||
use crate::core::commands::generate::generate_image::sora::handle_gpt_image_1_sora_text_to_image::handle_gpt_image_1_sora_text_to_image;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use tauri::AppHandle;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
pub (super) mod handle_text_to_image_sora;
|
||||
mod handle_gpt_image_1_sora_text_to_image;
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
use enums::common::generation::common_quality::CommonQuality;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::ux::tauri_command_caller::TauriCommandCaller;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::core::commands::generate::generate_image::tauri_image_model::TauriImageModel;
|
||||
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
|
||||
|
||||
// ── Request ──
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct TauriGenerateImageRequest {
|
||||
/// The provider to use (defaults to Artcraft/Storyteller).
|
||||
/// Not all (provider, model) combinations are valid.
|
||||
pub provider: Option<GenerationProvider>,
|
||||
|
||||
/// The model to use.
|
||||
pub model: Option<TauriImageModel>,
|
||||
|
||||
/// Text prompt for the image generation.
|
||||
pub prompt: Option<String>,
|
||||
|
||||
/// Aspect ratio.
|
||||
pub aspect_ratio: Option<CommonAspectRatio>,
|
||||
|
||||
/// Resolution.
|
||||
pub resolution: Option<CommonResolution>,
|
||||
|
||||
/// Quality (used by OpenAI models).
|
||||
pub quality: Option<CommonQuality>,
|
||||
|
||||
/// The number of images to generate.
|
||||
pub batch_size: Option<u32>,
|
||||
|
||||
/// Reference images (without semantics).
|
||||
/// The purpose varies on a model-by-model basis.
|
||||
pub image_media_tokens: Option<Vec<MediaFileToken>>,
|
||||
|
||||
// ── Canvas / scene images ──
|
||||
|
||||
/// Supply this *XOR* `canvas_image_raw_bytes`.
|
||||
/// Becomes the first image reference (pushing back `image_media_tokens` by one).
|
||||
pub canvas_image_media_token: Option<MediaFileToken>,
|
||||
|
||||
/// Supply this *XOR* `canvas_image_media_token`.
|
||||
/// Raw bytes of a canvas image.
|
||||
pub canvas_image_raw_bytes: Option<Vec<u8>>,
|
||||
|
||||
/// Supply this *XOR* `scene_image_raw_bytes`.
|
||||
/// Becomes a scene reference image.
|
||||
pub scene_image_media_token: Option<MediaFileToken>,
|
||||
|
||||
/// Supply this *XOR* `scene_image_media_token`.
|
||||
/// Raw bytes of a scene image.
|
||||
pub scene_image_raw_bytes: Option<Vec<u8>>,
|
||||
|
||||
// ── Inpainting ──
|
||||
|
||||
/// Supply this *XOR* `inpainting_mask_image_raw_bytes`.
|
||||
/// The mask to focus the edit (already uploaded).
|
||||
pub inpainting_mask_image_media_token: Option<MediaFileToken>,
|
||||
|
||||
/// Supply this *XOR* `inpainting_mask_image_media_token`.
|
||||
/// The mask to focus the edit (raw bytes).
|
||||
pub inpainting_mask_image_raw_bytes: Option<Vec<u8>>,
|
||||
|
||||
// ── Angle adjustment (for edit models like QwenEdit, Flux2LoraAngles) ──
|
||||
|
||||
/// Horizontal angle adjustment.
|
||||
pub adjust_horizontal_angle: Option<f64>,
|
||||
|
||||
/// Vertical angle adjustment.
|
||||
pub adjust_vertical_angle: Option<f64>,
|
||||
|
||||
/// Zoom adjustment.
|
||||
pub adjust_zoom: Option<f64>,
|
||||
|
||||
/// Turn on the system prompt.
|
||||
pub enable_system_prompt: Option<bool>,
|
||||
|
||||
// ── Frontend metadata ──
|
||||
|
||||
/// Name of the frontend caller.
|
||||
pub frontend_caller: Option<TauriCommandCaller>,
|
||||
|
||||
/// A frontend-defined identifier sent back as a Tauri event on task completion.
|
||||
pub frontend_subscriber_id: Option<String>,
|
||||
|
||||
/// A frontend-defined payload sent back as a Tauri event on task completion.
|
||||
pub frontend_subscriber_payload: Option<String>,
|
||||
}
|
||||
|
||||
// ── Response ──
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct TauriGenerateImageResponse {
|
||||
}
|
||||
|
||||
impl SerializeMarker for TauriGenerateImageResponse {}
|
||||
|
||||
// ── Error ──
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TauriGenerateImageErrorType {
|
||||
/// Caller didn't specify a model
|
||||
ModelNotSpecified,
|
||||
/// Bad input
|
||||
BadInput,
|
||||
/// No provider available
|
||||
NoProviderAvailable,
|
||||
/// Generic server error
|
||||
ServerError,
|
||||
/// Needs to be logged into Artcraft
|
||||
NeedsStorytellerCredentials,
|
||||
/// Needs Grok credentials
|
||||
NeedsGrokCredentials,
|
||||
/// Billing issue
|
||||
BillingIssue,
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
use enums::common::generation::common_model_type::CommonModelType;
|
||||
use serde_derive::Deserialize;
|
||||
|
||||
/// Unified image model enum covering text-to-image, image edit, and inpainting.
|
||||
///
|
||||
/// This is used in the Tauri command bridge.
|
||||
/// Don't change the serializations without coordinating with the frontend.
|
||||
#[derive(Deserialize, Debug, Copy, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TauriImageModel {
|
||||
// Text-to-image models
|
||||
|
||||
#[serde(rename = "flux_1_dev")]
|
||||
Flux1Dev,
|
||||
#[serde(rename = "flux_1_schnell")]
|
||||
Flux1Schnell,
|
||||
#[serde(rename = "flux_pro_11")]
|
||||
FluxPro11,
|
||||
#[serde(rename = "flux_pro_11_ultra")]
|
||||
FluxPro11Ultra,
|
||||
#[serde(rename = "grok_image")]
|
||||
GrokImage,
|
||||
#[serde(rename = "recraft_3")]
|
||||
Recraft3,
|
||||
#[serde(rename = "gpt_image_1")]
|
||||
GptImage1,
|
||||
#[serde(rename = "gpt_image_1p5")]
|
||||
GptImage1p5,
|
||||
#[serde(rename = "gpt_image_2")]
|
||||
GptImage2,
|
||||
#[serde(rename = "gemini_25_flash")]
|
||||
Gemini25Flash,
|
||||
#[serde(rename = "nano_banana")]
|
||||
NanoBanana,
|
||||
#[serde(rename = "nano_banana_2")]
|
||||
NanoBanana2,
|
||||
#[serde(rename = "nano_banana_pro")]
|
||||
NanoBananaPro,
|
||||
#[serde(rename = "seedream_4")]
|
||||
Seedream4,
|
||||
#[serde(rename = "seedream_4p5")]
|
||||
Seedream4p5,
|
||||
#[serde(rename = "seedream_5_lite")]
|
||||
Seedream5Lite,
|
||||
#[serde(rename = "midjourney")]
|
||||
Midjourney,
|
||||
|
||||
// Image edit models
|
||||
|
||||
#[serde(rename = "flux_pro_kontext_max")]
|
||||
FluxProKontextMax,
|
||||
#[serde(rename = "qwen_edit_2511_angles")]
|
||||
QwenEdit2511Angles,
|
||||
#[serde(rename = "flux_2_lora_angles")]
|
||||
Flux2LoraAngles,
|
||||
|
||||
// Inpainting models
|
||||
|
||||
#[serde(rename = "flux_dev_juggernaut")]
|
||||
FluxDevJuggernaut,
|
||||
#[serde(rename = "flux_pro_1")]
|
||||
FluxPro1,
|
||||
}
|
||||
|
||||
impl TauriImageModel {
|
||||
pub fn to_common_model_type(&self) -> CommonModelType {
|
||||
match self {
|
||||
Self::Flux1Dev => CommonModelType::Flux1Dev,
|
||||
Self::Flux1Schnell => CommonModelType::Flux1Schnell,
|
||||
Self::FluxPro11 => CommonModelType::FluxPro11,
|
||||
Self::FluxPro11Ultra => CommonModelType::FluxPro11Ultra,
|
||||
Self::GrokImage => CommonModelType::GrokImage,
|
||||
Self::Recraft3 => CommonModelType::Recraft3,
|
||||
Self::GptImage1 => CommonModelType::GptImage1,
|
||||
Self::GptImage1p5 => CommonModelType::GptImage1p5,
|
||||
Self::GptImage2 => CommonModelType::GptImage2,
|
||||
Self::Gemini25Flash => CommonModelType::NanoBanana,
|
||||
Self::NanoBanana => CommonModelType::NanoBanana,
|
||||
Self::NanoBanana2 => CommonModelType::NanoBanana2,
|
||||
Self::NanoBananaPro => CommonModelType::NanoBananaPro,
|
||||
Self::Seedream4 => CommonModelType::Seedream4,
|
||||
Self::Seedream4p5 => CommonModelType::Seedream4p5,
|
||||
Self::Seedream5Lite => CommonModelType::Seedream5Lite,
|
||||
Self::Midjourney => CommonModelType::Midjourney,
|
||||
Self::FluxProKontextMax => CommonModelType::FluxProKontextMax,
|
||||
Self::QwenEdit2511Angles => CommonModelType::QwenEdit2511Angles,
|
||||
Self::Flux2LoraAngles => CommonModelType::Flux2LoraAngles,
|
||||
Self::FluxDevJuggernaut => CommonModelType::FluxDevJuggernaut,
|
||||
Self::FluxPro1 => CommonModelType::FluxPro1,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod parse_semantic_media_files;
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
use log::info;
|
||||
|
||||
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use artcraft_client::utils::api_host::ApiHost;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
|
||||
use crate::core::commands::generate::generate_image::tauri_generate_image_request::TauriGenerateImageRequest;
|
||||
use crate::core::utils::upload_bytes_to_media_file::upload_image_bytes_as_media_file::upload_image_bytes_as_media_file;
|
||||
use crate::core::utils::upload_bytes_to_media_file::upload_mask_image_bytes_as_media_file::upload_mask_image_bytes_as_media_file;
|
||||
|
||||
/// Resolved semantic media file tokens for canvas, scene, and inpainting mask.
|
||||
///
|
||||
/// Each field is populated either from an already-uploaded token on the request
|
||||
/// or by uploading raw bytes on the fly.
|
||||
pub struct SemanticMediaFiles {
|
||||
pub canvas_image_media_token: Option<MediaFileToken>,
|
||||
pub scene_image_media_token: Option<MediaFileToken>,
|
||||
pub inpainting_mask_image_media_token: Option<MediaFileToken>,
|
||||
}
|
||||
|
||||
/// Resolve semantic media files from the request.
|
||||
///
|
||||
/// For each of (canvas, scene, mask): the request may supply a pre-uploaded
|
||||
/// token XOR raw bytes. If both are supplied, returns an error. If raw bytes
|
||||
/// are supplied, uploads them and returns the resulting token.
|
||||
pub async fn parse_semantic_media_files(
|
||||
request: &TauriGenerateImageRequest,
|
||||
creds: &StorytellerCredentialSet,
|
||||
api_host: &ApiHost,
|
||||
) -> Result<SemanticMediaFiles, GenerateError> {
|
||||
// Canvas image
|
||||
let canvas_image_media_token = resolve_image_field(
|
||||
"canvas_image",
|
||||
request.canvas_image_media_token.as_ref(),
|
||||
request.canvas_image_raw_bytes.as_deref(),
|
||||
creds,
|
||||
api_host,
|
||||
).await?;
|
||||
|
||||
// Scene image
|
||||
let scene_image_media_token = resolve_image_field(
|
||||
"scene_image",
|
||||
request.scene_image_media_token.as_ref(),
|
||||
request.scene_image_raw_bytes.as_deref(),
|
||||
creds,
|
||||
api_host,
|
||||
).await?;
|
||||
|
||||
// Inpainting mask
|
||||
let inpainting_mask_image_media_token = resolve_mask_field(
|
||||
request.inpainting_mask_image_media_token.as_ref(),
|
||||
request.inpainting_mask_image_raw_bytes.as_deref(),
|
||||
creds,
|
||||
api_host,
|
||||
).await?;
|
||||
|
||||
Ok(SemanticMediaFiles {
|
||||
canvas_image_media_token,
|
||||
scene_image_media_token,
|
||||
inpainting_mask_image_media_token,
|
||||
})
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
async fn resolve_image_field(
|
||||
field_name: &str,
|
||||
maybe_token: Option<&MediaFileToken>,
|
||||
maybe_raw_bytes: Option<&[u8]>,
|
||||
creds: &StorytellerCredentialSet,
|
||||
api_host: &ApiHost,
|
||||
) -> Result<Option<MediaFileToken>, GenerateError> {
|
||||
if maybe_token.is_some() && maybe_raw_bytes.is_some() {
|
||||
return Err(GenerateError::BadInput(BadInputReason::WrongImageArguments(format!(
|
||||
"Cannot supply both {}_media_token and {}_raw_bytes",
|
||||
field_name, field_name
|
||||
))));
|
||||
}
|
||||
|
||||
if let Some(token) = maybe_token {
|
||||
return Ok(Some(token.clone()));
|
||||
}
|
||||
|
||||
if let Some(raw_bytes) = maybe_raw_bytes {
|
||||
info!("Uploading {} raw bytes ({} bytes)...", field_name, raw_bytes.len());
|
||||
let token = upload_image_bytes_as_media_file(
|
||||
creds,
|
||||
api_host,
|
||||
raw_bytes.to_vec(),
|
||||
).await?;
|
||||
return Ok(Some(token));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn resolve_mask_field(
|
||||
maybe_token: Option<&MediaFileToken>,
|
||||
maybe_raw_bytes: Option<&[u8]>,
|
||||
creds: &StorytellerCredentialSet,
|
||||
api_host: &ApiHost,
|
||||
) -> Result<Option<MediaFileToken>, GenerateError> {
|
||||
if maybe_token.is_some() && maybe_raw_bytes.is_some() {
|
||||
return Err(GenerateError::BadInput(BadInputReason::BothImageMaskMediaTokenAndBytesSupplied));
|
||||
}
|
||||
|
||||
if let Some(token) = maybe_token {
|
||||
return Ok(Some(token.clone()));
|
||||
}
|
||||
|
||||
if let Some(raw_bytes) = maybe_raw_bytes {
|
||||
info!("Uploading inpainting mask raw bytes ({} bytes)...", raw_bytes.len());
|
||||
let token = upload_mask_image_bytes_as_media_file(
|
||||
creds,
|
||||
api_host,
|
||||
raw_bytes,
|
||||
).await?;
|
||||
return Ok(Some(token));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
use crate::core::commands::enqueue::generate_error::GenerateError;
|
||||
use crate::core::commands::enqueue::image_to_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
|
||||
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::{EnqueueImageToVideoRequest, SoraOrientation};
|
||||
use crate::core::commands::generate::generate_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
|
||||
use crate::core::commands::generate::generate_video::request::{TauriGenerateVideoRequest, SoraOrientation};
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
|
||||
@@ -26,7 +26,7 @@ use tauri::AppHandle;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
pub (super) async fn handle_artcraft_kling_1p6_pro(
|
||||
request: &EnqueueImageToVideoRequest,
|
||||
request: &TauriGenerateVideoRequest,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
creds: &StorytellerCredentialSet,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
use crate::core::commands::enqueue::generate_error::GenerateError;
|
||||
use crate::core::commands::enqueue::image_to_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
|
||||
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::{EnqueueImageToVideoRequest, SoraOrientation};
|
||||
use crate::core::commands::generate::generate_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
|
||||
use crate::core::commands::generate::generate_video::request::{TauriGenerateVideoRequest, SoraOrientation};
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
|
||||
@@ -26,7 +26,7 @@ use tauri::AppHandle;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
pub (super) async fn handle_artcraft_kling_2p1_master(
|
||||
request: &EnqueueImageToVideoRequest,
|
||||
request: &TauriGenerateVideoRequest,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
creds: &StorytellerCredentialSet,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
use crate::core::commands::enqueue::generate_error::GenerateError;
|
||||
use crate::core::commands::enqueue::image_to_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
|
||||
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::{EnqueueImageToVideoRequest, SoraOrientation};
|
||||
use crate::core::commands::generate::generate_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
|
||||
use crate::core::commands::generate::generate_video::request::{TauriGenerateVideoRequest, SoraOrientation};
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
|
||||
@@ -25,7 +25,7 @@ use tauri::AppHandle;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
pub (super) async fn handle_artcraft_kling_2p1_pro(
|
||||
request: &EnqueueImageToVideoRequest,
|
||||
request: &TauriGenerateVideoRequest,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
creds: &StorytellerCredentialSet,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
use crate::core::commands::enqueue::generate_error::GenerateError;
|
||||
use crate::core::commands::enqueue::image_to_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
|
||||
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::{EnqueueImageToVideoRequest, SoraOrientation};
|
||||
use crate::core::commands::generate::generate_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
|
||||
use crate::core::commands::generate::generate_video::request::{TauriGenerateVideoRequest, SoraOrientation};
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
|
||||
@@ -24,7 +24,7 @@ use tauri::AppHandle;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
pub (super) async fn handle_artcraft_kling_2p5_turbo_pro(
|
||||
request: &EnqueueImageToVideoRequest,
|
||||
request: &TauriGenerateVideoRequest,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
creds: &StorytellerCredentialSet,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user