mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-27 12:10:08 -04:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f767ef655 | ||
|
|
da859fc2f8 | ||
|
|
5f832d5d17 | ||
|
|
612e58483c | ||
|
|
11b07b5833 | ||
|
|
a3f3bd8b3f | ||
|
|
a93e921b51 | ||
|
|
58d5a172c4 | ||
|
|
012b7a9567 | ||
|
|
18f4411f2a | ||
|
|
0ed4d58387 |
@@ -64,7 +64,7 @@ jobs:
|
||||
npm --version
|
||||
|
||||
# Remove mismatched @nx packages and reinstall with correct versions
|
||||
npm install --save-dev @nx/remix@20.7.2 @nx/js@20.7.2 @nx/react@20.7.2 @nx/vite@20.7.2 @nx/eslint@20.7.2
|
||||
npm install --save-dev @nx/remix@21.1.1 @nx/js@21.1.1 @nx/react@21.1.1 @nx/vite@21.1.1 @nx/eslint@21.1.1
|
||||
|
||||
echo "=== Nx versions after adjustment ==="
|
||||
npm list --depth=0 | grep nx
|
||||
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
npm --version
|
||||
|
||||
# Remove mismatched @nx packages and reinstall with correct versions
|
||||
npm install --save-dev @nx/remix@20.7.2 @nx/js@20.7.2 @nx/react@20.7.2 @nx/vite@20.7.2 @nx/eslint@20.7.2
|
||||
npm install --save-dev @nx/remix@21.1.1 @nx/js@21.1.1 @nx/react@21.1.1 @nx/vite@21.1.1 @nx/eslint@21.1.1
|
||||
|
||||
echo "=== Nx versions after adjustment ==="
|
||||
npm list --depth=0 | findstr nx
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
# Update Tauri config to skip the build (since we already built)
|
||||
cd ${{ github.workspace }}/crates/desktop/artcraft
|
||||
Copy-Item tauri.conf.json tauri.conf.json.bak
|
||||
|
||||
|
||||
# Set beforeBuildCommand to empty using PowerShell
|
||||
$config = Get-Content tauri.conf.json | ConvertFrom-Json
|
||||
$config.build.beforeBuildCommand = ""
|
||||
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO tasks (\n id,\n task_status,\n task_type,\n provider,\n provider_job_id,\n frontend_subscriber_id,\n frontend_subscriber_payload\n )\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 7
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0848cd9ddf8f61f2dd4d57f0c58b66a60a985acaa1ee5753f30f5e6e107e080e"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO tasks (\n id,\n task_status,\n task_type,\n model_type,\n provider,\n provider_job_id,\n frontend_subscriber_id,\n frontend_subscriber_payload\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 8
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "22b91a21cbb88abc9bc17b338cf2f9418e08e414667d44476623eb141ab17b87"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n UPDATE tasks\n SET task_status = ?\n WHERE id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "dc0367e9e43c3d7dda3623e77337ab4529eac2192475be7d08589886703ba1cb"
|
||||
}
|
||||
Generated
+11
-1
@@ -698,11 +698,13 @@ dependencies = [
|
||||
name = "artcraft_api_defs"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"enums",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"tokens",
|
||||
"url",
|
||||
"utoipa",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
@@ -1916,6 +1918,13 @@ dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cloudflare_errors"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coarsetime"
|
||||
version = "0.1.34"
|
||||
@@ -6839,6 +6848,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"cloudflare_errors",
|
||||
"cookie 0.18.1",
|
||||
"errors",
|
||||
"idempotency",
|
||||
@@ -9632,7 +9642,6 @@ dependencies = [
|
||||
"sqlx",
|
||||
"tempfile",
|
||||
"tokens",
|
||||
"tokio",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
@@ -9990,6 +9999,7 @@ dependencies = [
|
||||
"artcraft_api_defs",
|
||||
"backoff",
|
||||
"chrono",
|
||||
"cloudflare_errors",
|
||||
"cookie 0.18.1",
|
||||
"enums",
|
||||
"enums_public",
|
||||
|
||||
@@ -40,6 +40,7 @@ members = [
|
||||
"crates/lib/caching/memory_caching",
|
||||
"crates/lib/caching/memory_store",
|
||||
"crates/lib/caching/redis_caching",
|
||||
"crates/lib/cloudflare_errors",
|
||||
"crates/lib/concurrency",
|
||||
"crates/lib/cookies",
|
||||
"crates/lib/data/collections",
|
||||
@@ -216,6 +217,7 @@ url = { version = "2.5.4", features = ["serde"] }
|
||||
artcraft_api_defs = { path = "crates/schema/public/api/artcraft_api_defs" }
|
||||
build_metadata = { path = "crates/lib/build_metadata" }
|
||||
cloud_storage = { path = "crates/api_clients/cloud_storage" }
|
||||
cloudflare_errors = { path = "crates/lib/cloudflare_errors" }
|
||||
crockford = { path = "crates/lib/data/crockford" }
|
||||
enums = { path = "crates/schema/public/enums" }
|
||||
enums_public = { path = "crates/schema/public/enums_public" }
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- Migration history:
|
||||
-- tasks_v1.sqlite - initial version
|
||||
-- tasks_v2.sqlite - added model_type (nullable)
|
||||
|
||||
CREATE TABLE tasks (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
@@ -6,6 +9,8 @@ CREATE TABLE tasks (
|
||||
task_status TEXT NOT NULL,
|
||||
task_type TEXT NOT NULL,
|
||||
|
||||
model_type TEXT,
|
||||
|
||||
provider TEXT,
|
||||
provider_job_id TEXT,
|
||||
|
||||
|
||||
+4
-4
@@ -37,10 +37,10 @@ pub async fn enqueue_veo_2_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
let duration = match args.duration {
|
||||
Veo2Duration::Default => None,
|
||||
Veo2Duration::FiveSeconds => Some("5".to_string()),
|
||||
Veo2Duration::SixSeconds => Some("6".to_string()),
|
||||
Veo2Duration::SevenSeconds => Some("7".to_string()),
|
||||
Veo2Duration::EightSeconds => Some("8".to_string()),
|
||||
Veo2Duration::FiveSeconds => Some("5s".to_string()),
|
||||
Veo2Duration::SixSeconds => Some("6s".to_string()),
|
||||
Veo2Duration::SevenSeconds => Some("7s".to_string()),
|
||||
Veo2Duration::EightSeconds => Some("8s".to_string()),
|
||||
};
|
||||
|
||||
let aspect_ratio = match args.aspect_ratio {
|
||||
|
||||
@@ -23,6 +23,7 @@ idempotency.workspace = true
|
||||
anyhow.workspace = true
|
||||
base64.workspace = true
|
||||
chrono.workspace = true
|
||||
cloudflare_errors.workspace = true
|
||||
cookie.workspace = true
|
||||
log.workspace = true
|
||||
once_cell.workspace = true
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use cloudflare_errors::cloudflare_error::CloudflareError;
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::io;
|
||||
@@ -42,15 +43,9 @@ pub enum SoraError {
|
||||
/// Typically served from Cloudflare
|
||||
/// We preserve the message for debugging.
|
||||
BadGateway(String),
|
||||
|
||||
/// HTTP 403 + Cloudflare Challenge Interstitial
|
||||
/// Cloudflare decided to block the request and present a challenge.
|
||||
CloudFlareChallenge,
|
||||
|
||||
/// HTTP 524 (Cloudflare-specific)
|
||||
/// Cloudflare formed a TCP connection to Sora, but no payload was delivered before timeout
|
||||
/// We preserve the message for debugging
|
||||
CloudFlareTimeout(String),
|
||||
|
||||
/// Specific Cloudflare errors.
|
||||
CloudflareError(CloudflareError),
|
||||
|
||||
/// Another error occurred.
|
||||
OtherBadStatus(anyhow::Error),
|
||||
@@ -72,6 +67,17 @@ pub enum SoraError {
|
||||
JsonErrorWithBody(serde_json::Error, String),
|
||||
}
|
||||
|
||||
impl SoraError {
|
||||
pub fn is_sora_having_downtime_issues(&self) -> bool {
|
||||
match self {
|
||||
Self::BadGateway(_) => true,
|
||||
Self::CloudflareError(CloudflareError::GatewayTimeout504) => true,
|
||||
Self::CloudflareError(CloudflareError::TimeoutOccurred524) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for SoraError {}
|
||||
|
||||
impl Display for SoraError {
|
||||
@@ -98,11 +104,8 @@ impl Display for SoraError {
|
||||
Self::BadGateway(message) => {
|
||||
write!(f, "Bad Gateway; Sora is likely having issues: {:?}", message)
|
||||
}
|
||||
Self::CloudFlareChallenge => {
|
||||
write!(f, "Cloudflare Challenge (403); Cloudflare wants to verify the request with a CAPTCHA challenge.")
|
||||
}
|
||||
Self::CloudFlareTimeout(message) => {
|
||||
write!(f, "Cloudflare Timeout (524); Sora is likely having issues: {:?}", message)
|
||||
Self::CloudflareError(error) => {
|
||||
write!(f, "Cloudflare error: {}", error)
|
||||
}
|
||||
Self::OtherBadStatus(err) => {
|
||||
write!(f, "Other error: {}", err)
|
||||
@@ -149,3 +152,9 @@ impl From<serde_json::Error> for SoraError {
|
||||
Self::JsonError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CloudflareError> for SoraError {
|
||||
fn from(err: CloudflareError) -> Self {
|
||||
Self::CloudflareError(err)
|
||||
}
|
||||
}
|
||||
|
||||
+6
-23
@@ -1,6 +1,6 @@
|
||||
use crate::sora_error::SoraError;
|
||||
use cloudflare_errors::filter_cloudflare_errors::filter_cloudflare_errors;
|
||||
use reqwest::StatusCode;
|
||||
|
||||
/*
|
||||
|
||||
Old cookie -
|
||||
@@ -50,32 +50,15 @@ pub async fn classify_general_http_status_code_and_body(status: StatusCode, resp
|
||||
}
|
||||
|
||||
let status_code = status.as_u16();
|
||||
|
||||
|
||||
if let Err(err) = filter_cloudflare_errors(status_code, &response_body) {
|
||||
return SoraError::CloudflareError(err);
|
||||
}
|
||||
|
||||
match status_code {
|
||||
403 => {
|
||||
if message.contains("challenge-platform")
|
||||
|| message.contains("challenge-error-text")
|
||||
|| message.contains("cType: 'managed'")
|
||||
|| message.contains("Just a moment...") {
|
||||
return SoraError::CloudFlareChallenge;
|
||||
}
|
||||
}
|
||||
502 => {
|
||||
return SoraError::BadGateway(message);
|
||||
}
|
||||
504 => {
|
||||
if message.contains("cloudflare")
|
||||
|| message.contains("Cloudflare") {
|
||||
return SoraError::CloudFlareTimeout(message);
|
||||
}
|
||||
}
|
||||
524 => {
|
||||
// NB: 524 is a cloudflare specific gateway timeout error with slightly different semantics than 504.
|
||||
if message.contains("cloudflare")
|
||||
|| message.contains("Cloudflare") {
|
||||
return SoraError::CloudFlareTimeout(message);
|
||||
}
|
||||
}
|
||||
_ => {}, // Fall-through
|
||||
}
|
||||
|
||||
|
||||
@@ -24,12 +24,13 @@ workspace-build-acceleration.workspace = true
|
||||
|
||||
# Internal
|
||||
artcraft_api_defs.workspace = true
|
||||
cloudflare_errors.workspace = true
|
||||
enums.workspace = true
|
||||
enums_public.workspace = true
|
||||
errors.workspace = true
|
||||
idempotency.workspace = true
|
||||
tokens.workspace = true
|
||||
mimetypes.workspace = true
|
||||
tokens.workspace = true
|
||||
|
||||
# External
|
||||
anyhow.workspace = true
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use cloudflare_errors::cloudflare_error::CloudflareError;
|
||||
use errors::AnyhowError;
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::io;
|
||||
use errors::AnyhowError;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ApiError {
|
||||
@@ -26,6 +27,9 @@ pub enum ApiError {
|
||||
backend_hostname: Option<String>,
|
||||
},
|
||||
|
||||
/// Cloudflare errors.
|
||||
CloudflareError(CloudflareError),
|
||||
|
||||
/// A deserialization error with the response.
|
||||
DeserializationError(serde_json::Error),
|
||||
|
||||
@@ -63,6 +67,8 @@ impl Display for ApiError {
|
||||
// Network errors
|
||||
ApiError::Timeout(msg) => write!(f, "Timeout: {}", msg),
|
||||
ApiError::NetworkError(msg) => write!(f, "Network error: {}", msg),
|
||||
// Cloudflare errors
|
||||
ApiError::CloudflareError(error) => write!(f, "Cloudflare Error: {}", error),
|
||||
// I/O errors
|
||||
ApiError::IoError(error) => write!(f, "IO error: {}", error),
|
||||
// Other
|
||||
@@ -95,3 +101,9 @@ impl From<io::Error> for ApiError {
|
||||
ApiError::IoError(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CloudflareError> for ApiError {
|
||||
fn from(error: CloudflareError) -> Self {
|
||||
ApiError::CloudflareError(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use crate::error::storyteller_error::StorytellerError;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
use crate::utils::basic_json_get_request::basic_json_get_request;
|
||||
use artcraft_api_defs::jobs::list_session_jobs::{ListSessionJobsSuccessResponse, LIST_SESSION_JOBS_URL_PATH};
|
||||
use enums::common::job_status_plus::JobStatusPlus;
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum States {
|
||||
All,
|
||||
Include(HashSet<JobStatusPlus>),
|
||||
Exclude(HashSet<JobStatusPlus>),
|
||||
}
|
||||
|
||||
pub async fn list_session_jobs(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
job_states: States,
|
||||
) -> Result<ListSessionJobsSuccessResponse, StorytellerError> {
|
||||
|
||||
let url = match job_states {
|
||||
States::All =>
|
||||
LIST_SESSION_JOBS_URL_PATH.to_string(),
|
||||
States::Include(states) =>
|
||||
format!("{}?include_states={}", LIST_SESSION_JOBS_URL_PATH, states_string(&states)),
|
||||
States::Exclude(states) =>
|
||||
format!("{}?exclude_states={}", LIST_SESSION_JOBS_URL_PATH, states_string(&states)),
|
||||
};
|
||||
|
||||
Ok(basic_json_get_request(
|
||||
api_host,
|
||||
&url,
|
||||
maybe_creds,
|
||||
).await?)
|
||||
}
|
||||
|
||||
fn states_string(states: &HashSet<JobStatusPlus>) -> String {
|
||||
states.iter()
|
||||
.map(|state| state.to_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod list_session_jobs;
|
||||
@@ -3,6 +3,7 @@
|
||||
pub mod credentials;
|
||||
pub mod error;
|
||||
pub mod generate;
|
||||
pub mod jobs;
|
||||
pub mod media_files;
|
||||
pub mod recipes;
|
||||
pub mod shared_response_types;
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use crate::error::api_error::ApiError;
|
||||
use crate::error::client_error::ClientError;
|
||||
use crate::error::storyteller_error::StorytellerError;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
|
||||
use crate::utils::filter_bad_response::filter_bad_response;
|
||||
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::{GenerateFlux1SchnellTextToImageRequest, GenerateFlux1SchnellTextToImageResponse, GENERATE_FLUX_1_SCHNELL_TEXT_TO_IMAGE_PATH};
|
||||
use log::debug;
|
||||
use reqwest::Client;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub async fn basic_json_get_request<Res: DeserializeOwned>(
|
||||
api_host: &ApiHost,
|
||||
route_path: &str,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
) -> Result<Res, StorytellerError> {
|
||||
|
||||
let url = get_route(api_host, route_path);
|
||||
|
||||
debug!("Requesting {:?}", &url);
|
||||
|
||||
let client = Client::builder()
|
||||
.gzip(true)
|
||||
.build()
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
|
||||
let mut request_builder = client.get(url)
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.header("Accept", APPLICATION_JSON);
|
||||
|
||||
if let Some(creds) = maybe_creds {
|
||||
if let Some(header) = &creds.maybe_as_cookie_header() {
|
||||
request_builder = request_builder.header("Cookie", header);
|
||||
}
|
||||
}
|
||||
|
||||
let response = request_builder
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
let response = filter_bad_response(response)
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
let response_body = &response.text()
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
debug!("Response body: {:?}", response_body);
|
||||
|
||||
let response = serde_json::from_str(&response_body)
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn get_route(api_host: &ApiHost, route_path: &str) -> String {
|
||||
let api_hostname = api_host.to_api_hostname_and_scheme();
|
||||
format!("{}{}", api_hostname, route_path)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::error::api_error::ApiError;
|
||||
use crate::utils::status_codes::*;
|
||||
use anyhow::anyhow;
|
||||
use cloudflare_errors::filter_cloudflare_errors::filter_cloudflare_errors;
|
||||
use log::error;
|
||||
|
||||
/// Pass request errors to this method for standard error handling behavior.
|
||||
@@ -13,30 +14,34 @@ use log::error;
|
||||
pub async fn filter_bad_response(response: reqwest::Response) -> Result<reqwest::Response, ApiError> {
|
||||
let status = response.status();
|
||||
|
||||
if !status.is_success() {
|
||||
// The host in the fleet that handled the request.
|
||||
let backend_hostname= response.headers()
|
||||
.get("x-backend-hostname")
|
||||
.map(|header| header.to_str().unwrap_or_else(|err| {
|
||||
error!("Failed to parse x-backend-hostname header: {:?}", err);
|
||||
"unknown"
|
||||
}))
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let response_body = match response.text().await {
|
||||
Ok(text) => text.to_string(),
|
||||
Err(err) => format!("Could not read response body: {:?}", err),
|
||||
};
|
||||
|
||||
return match status.as_u16() {
|
||||
STATUS_401_UNAUTHORIZED => Err(ApiError::Unauthorized(response_body)),
|
||||
STATUS_403_FORBIDDEN => Err(ApiError::Forbidden(response_body)),
|
||||
STATUS_404_NOT_FOUND => Err(ApiError::NotFound(response_body)),
|
||||
STATUS_429_TOO_MANY_REQUESTS => Err(ApiError::TooManyRequests(response_body)),
|
||||
STATUS_500_INTERNAL_SERVER_ERROR => Err(ApiError::InternalServerError { body: response_body, backend_hostname }),
|
||||
_ => Err(ApiError::Other(anyhow!("Bad status code: {}; message: {:?}", status, response_body))),
|
||||
}
|
||||
if status.is_success() {
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
// The host in the fleet that handled the request.
|
||||
let backend_hostname= response.headers()
|
||||
.get("x-backend-hostname")
|
||||
.map(|header| header.to_str().unwrap_or_else(|err| {
|
||||
error!("Failed to parse x-backend-hostname header: {:?}", err);
|
||||
"unknown"
|
||||
}))
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let response_body = match response.text().await {
|
||||
Ok(text) => text.to_string(),
|
||||
Err(err) => format!("Could not read response body: {:?}", err),
|
||||
};
|
||||
|
||||
let status_code = status.as_u16();
|
||||
|
||||
filter_cloudflare_errors(status_code, &response_body)?;
|
||||
|
||||
match status_code {
|
||||
STATUS_401_UNAUTHORIZED => Err(ApiError::Unauthorized(response_body)),
|
||||
STATUS_403_FORBIDDEN => Err(ApiError::Forbidden(response_body)),
|
||||
STATUS_404_NOT_FOUND => Err(ApiError::NotFound(response_body)),
|
||||
STATUS_429_TOO_MANY_REQUESTS => Err(ApiError::TooManyRequests(response_body)),
|
||||
STATUS_500_INTERNAL_SERVER_ERROR => Err(ApiError::InternalServerError { body: response_body, backend_hostname }),
|
||||
_ => Err(ApiError::Other(anyhow!("Bad status code: {}; message: {:?}", status, response_body))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod api_host;
|
||||
pub mod basic_json_get_request;
|
||||
pub mod basic_json_post_request;
|
||||
pub mod constants;
|
||||
pub mod filter_bad_response;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
|
||||
use crate::core::state::task_database::TaskDatabase;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_model_type::TaskModelType;
|
||||
use enums::tauri::tasks::task_status::TaskStatus;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use sqlite_tasks::error::SqliteTasksError;
|
||||
@@ -34,10 +35,33 @@ impl TaskEnqueueSuccess{
|
||||
}
|
||||
|
||||
pub async fn insert_into_task_database(&self, task_database: &TaskDatabase) -> Result<TaskId, SqliteTasksError> {
|
||||
// TODO: Move this mapping elsewhere, or remove the other models.
|
||||
let model_type = match self.model {
|
||||
None => None,
|
||||
Some(GenerationModel::Flux1Dev) => Some(TaskModelType::Flux1Dev),
|
||||
Some(GenerationModel::Flux1Schnell) => Some(TaskModelType::Flux1Schnell),
|
||||
Some(GenerationModel::FluxPro11) => Some(TaskModelType::FluxPro11),
|
||||
Some(GenerationModel::FluxPro11Ultra) => Some(TaskModelType::FluxPro11Ultra),
|
||||
Some(GenerationModel::GptImage1) => Some(TaskModelType::GptImage1),
|
||||
Some(GenerationModel::Recraft3) => Some(TaskModelType::Recraft3),
|
||||
Some(GenerationModel::Kling21Pro) => Some(TaskModelType::Kling21Pro),
|
||||
Some(GenerationModel::Kling21Master) => Some(TaskModelType::Kling21Master),
|
||||
Some(GenerationModel::Seedance10Lite) => Some(TaskModelType::Seedance10Lite),
|
||||
Some(GenerationModel::Veo2) => Some(TaskModelType::Veo2),
|
||||
Some(GenerationModel::Hunyuan3d2_0) => Some(TaskModelType::Hunyuan3d2_0),
|
||||
Some(GenerationModel::Hunyuan3d2_1) => Some(TaskModelType::Hunyuan3d2_1),
|
||||
|
||||
// TODO: These seem wrong -
|
||||
Some(GenerationModel::Kling1_6) => Some(TaskModelType::Kling16Pro), // NB: `VideoModel::Kling16Pro`.
|
||||
Some(GenerationModel::Kling2_0) => None, // TODO: unused elsewhere?
|
||||
Some(GenerationModel::Sora) => None, // TODO: unused elsewhere?
|
||||
};
|
||||
|
||||
create_task(CreateTaskArgs {
|
||||
db: task_database.get_connection(),
|
||||
status: TaskStatus::Pending,
|
||||
task_type: self.task_type,
|
||||
model_type: model_type,
|
||||
provider: self.provider,
|
||||
provider_job_id: self.provider_job_id.as_deref(),
|
||||
frontend_subscriber_id: None,
|
||||
|
||||
@@ -25,8 +25,11 @@ pub enum GenerationModel {
|
||||
Hunyuan3d2_0,
|
||||
#[serde(rename = "hunyuan_3d_2_1")]
|
||||
Hunyuan3d2_1,
|
||||
|
||||
// TODO: Should be Kling16Pro
|
||||
#[serde(rename = "kling_1.6")]
|
||||
Kling1_6,
|
||||
|
||||
#[serde(rename = "kling_2.0")]
|
||||
Kling2_0,
|
||||
#[serde(rename = "kling_2.1_master")]
|
||||
|
||||
@@ -1,9 +1,80 @@
|
||||
use crate::core::lifecycle::startup::bootstrap_task_database::bootstrap_task_database;
|
||||
use crate::core::lifecycle::startup::tasks::bootstrap_task_database::bootstrap_task_database;
|
||||
use crate::core::lifecycle::startup::tasks::load_provider_priority_state::load_provider_priority_state;
|
||||
use crate::core::lifecycle::startup::tasks::set_app_log_level::set_app_log_level;
|
||||
use crate::core::lifecycle::startup::tasks::initially_size_and_position_windows::initially_size_and_position_windows;
|
||||
use crate::core::lifecycle::startup::tasks::spawn_discord_presence_thread::spawn_discord_presence_thread;
|
||||
use crate::core::lifecycle::startup::tasks::spawn_fal_task_polling_thread::spawn_fal_task_polling_thread;
|
||||
use crate::core::lifecycle::startup::tasks::spawn_main_window_thread::spawn_main_window_thread;
|
||||
use crate::core::lifecycle::startup::tasks::spawn_sora_task_polling_thread::spawn_sora_task_polling_thread;
|
||||
use crate::core::lifecycle::startup::tasks::spawn_storytoller_task_polling_thread::spawn_storyteller_task_polling_thread;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
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 errors::AnyhowResult;
|
||||
use tauri::AppHandle;
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
pub async fn handle_tauri_startup(
|
||||
app: AppHandle,
|
||||
root: AppDataRoot,
|
||||
app_env_configs: AppEnvConfigs,
|
||||
storyteller_creds_manager: StorytellerCredentialManager,
|
||||
sora_credential_manager: SoraCredentialManager,
|
||||
sora_task_queue: SoraTaskQueue,
|
||||
fal_credential_manager: FalCredentialManager,
|
||||
fal_task_queue: FalTaskQueue,
|
||||
) -> AnyhowResult<()> {
|
||||
|
||||
set_app_log_level(
|
||||
&app,
|
||||
&root,
|
||||
)?;
|
||||
|
||||
let task_database =
|
||||
bootstrap_task_database(&app, &root).await?;
|
||||
|
||||
load_provider_priority_state(
|
||||
&app,
|
||||
&root,
|
||||
)?;
|
||||
|
||||
spawn_main_window_thread(
|
||||
&app,
|
||||
&root,
|
||||
&storyteller_creds_manager,
|
||||
)?;
|
||||
|
||||
spawn_storyteller_task_polling_thread(
|
||||
&app,
|
||||
&app_env_configs,
|
||||
&task_database,
|
||||
&storyteller_creds_manager,
|
||||
)?;
|
||||
|
||||
spawn_sora_task_polling_thread(
|
||||
&app,
|
||||
&root,
|
||||
&app_env_configs,
|
||||
&sora_credential_manager,
|
||||
&storyteller_creds_manager,
|
||||
&sora_task_queue,
|
||||
)?;
|
||||
|
||||
spawn_fal_task_polling_thread(
|
||||
&app,
|
||||
&root,
|
||||
&app_env_configs,
|
||||
&fal_credential_manager,
|
||||
&fal_task_queue,
|
||||
&storyteller_creds_manager,
|
||||
)?;
|
||||
|
||||
spawn_discord_presence_thread()?;
|
||||
|
||||
initially_size_and_position_windows(&app, &root);
|
||||
|
||||
pub async fn handle_tauri_startup(app: AppHandle, root: AppDataRoot) -> AnyhowResult<()> {
|
||||
bootstrap_task_database(&app, &root).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
pub mod bootstrap_task_database;
|
||||
pub mod handle_tauri_startup;
|
||||
pub mod handle_tauri_startup;
|
||||
mod tasks;
|
||||
+3
-3
@@ -3,8 +3,8 @@ use crate::core::state::task_database::TaskDatabase;
|
||||
use errors::AnyhowResult;
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
pub async fn bootstrap_task_database(app: &AppHandle, root: &AppDataRoot) -> AnyhowResult<()> {
|
||||
pub async fn bootstrap_task_database(app: &AppHandle, root: &AppDataRoot) -> AnyhowResult<TaskDatabase> {
|
||||
let task_database = TaskDatabase::connect(root).await?;
|
||||
app.manage(task_database);
|
||||
Ok(())
|
||||
app.manage(task_database.clone());
|
||||
Ok(task_database)
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::window::main_window_position::MainWindowPosition;
|
||||
use crate::core::state::window::main_window_size::MainWindowSize;
|
||||
use crate::core::utils::window::position_main_window::position_main_window;
|
||||
use crate::core::utils::window::resize_main_window::resize_main_window;
|
||||
use tauri::AppHandle;
|
||||
|
||||
pub fn initially_size_and_position_windows(
|
||||
app: &AppHandle,
|
||||
root: &AppDataRoot,
|
||||
) {
|
||||
|
||||
println!("Sizing and positioning window...");
|
||||
|
||||
match MainWindowSize::from_filesystem_configs(&root) {
|
||||
Ok(None) => {}
|
||||
Ok(Some(size)) => {
|
||||
println!("Resizing window to: {:?}", size);
|
||||
let result = resize_main_window(app, &size);
|
||||
if let Err(err) = result {
|
||||
eprintln!("Could not set window size: {:?}", err);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Failed to read window size from disk: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
match MainWindowPosition::from_filesystem_configs(&root) {
|
||||
Ok(None) => {}
|
||||
Ok(Some(pos)) => {
|
||||
println!("Moving window to: {:?}", pos);
|
||||
let result = position_main_window(app, &pos);
|
||||
if let Err(err) = result {
|
||||
eprintln!("Could not set window position: {:?}", err);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Failed to read window position from disk: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::ProviderPriorityStore;
|
||||
use anyhow::anyhow;
|
||||
use errors::AnyhowResult;
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
pub fn load_provider_priority_state(
|
||||
app: &AppHandle,
|
||||
root: &AppDataRoot,
|
||||
) -> AnyhowResult<()> {
|
||||
let provider_priority = match ProviderPriorityStore::from_filesystem_configs(root) {
|
||||
Ok(Some(priority)) => {
|
||||
println!("Loaded provider priority from disk: {:?}", priority.get_priority());
|
||||
priority
|
||||
}
|
||||
Ok(None) => {
|
||||
println!("No provider priority found on disk, using default.");
|
||||
ProviderPriorityStore::default()
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Failed to read provider priority from disk: {:?}", err);
|
||||
ProviderPriorityStore::default()
|
||||
}
|
||||
};
|
||||
|
||||
let success = app.manage(provider_priority);
|
||||
|
||||
if !success {
|
||||
eprintln!("Failed to manage provider priority state.");
|
||||
return Err(anyhow!("Failed to manage provider priority state."));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
pub (super) mod bootstrap_task_database;
|
||||
pub (super) mod set_app_log_level;
|
||||
pub (super) mod initially_size_and_position_windows;
|
||||
pub (super) mod spawn_discord_presence_thread;
|
||||
pub (super) mod spawn_fal_task_polling_thread;
|
||||
pub (super) mod spawn_main_window_thread;
|
||||
pub (super) mod spawn_sora_task_polling_thread;
|
||||
pub (super) mod spawn_storytoller_task_polling_thread;
|
||||
pub (super) mod load_provider_priority_state;
|
||||
@@ -0,0 +1,73 @@
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use errors::AnyhowResult;
|
||||
use log::LevelFilter;
|
||||
use std::env;
|
||||
use std::env::VarError;
|
||||
use tauri::AppHandle;
|
||||
use tauri_plugin_log::{Target, TargetKind};
|
||||
|
||||
const RUST_LOG: &str = "RUST_LOG";
|
||||
|
||||
pub fn set_app_log_level(
|
||||
app: &AppHandle,
|
||||
root: &AppDataRoot,
|
||||
) -> AnyhowResult<()> {
|
||||
|
||||
//print_env_var(RUST_LOG);
|
||||
|
||||
// NB: The semantics of this do not match `RUST_LOG` exactly as it's not set per-package.
|
||||
let maybe_level = env::var(RUST_LOG)
|
||||
.map(|value| value.trim().to_ascii_lowercase());
|
||||
|
||||
let mut log_level = LevelFilter::Info;
|
||||
|
||||
match maybe_level {
|
||||
Ok(level) => {
|
||||
println!("The `RUST_LOG` environment variable was set to: `{}`.", level);
|
||||
|
||||
if level.contains("debug") {
|
||||
log_level = LevelFilter::Debug;
|
||||
} else if level.contains("info") {
|
||||
log_level = LevelFilter::Info;
|
||||
} else if level.contains("warn") {
|
||||
log_level = LevelFilter::Warn;
|
||||
} else if level.contains("error") {
|
||||
log_level = LevelFilter::Error;
|
||||
} else if level.contains("off") {
|
||||
log_level = LevelFilter::Off;
|
||||
}
|
||||
}
|
||||
Err(VarError::NotPresent) => {
|
||||
// NB: This demonstrably tests stderr output in the default case.
|
||||
eprintln!("No `RUST_LOG` environment variable found.");
|
||||
}
|
||||
Err(VarError::NotUnicode(_)) => {
|
||||
// NB: This demonstrably tests stderr output in the default case.
|
||||
eprintln!("Could not parse `RUST_LOG` environment variable; not unicode.");
|
||||
}
|
||||
}
|
||||
|
||||
println!("Setting app log level to: {:?}", log_level);
|
||||
|
||||
app.plugin(
|
||||
tauri_plugin_log::Builder::default()
|
||||
.level(log_level)
|
||||
.targets(vec![
|
||||
Target::new(TargetKind::Stdout),
|
||||
Target::new(TargetKind::LogDir {
|
||||
file_name: Some(root.log_file_name_str().to_string())
|
||||
}),
|
||||
])
|
||||
.build(),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// pub fn print_env_var(var_name: &str) {
|
||||
// match env::var(var_name) {
|
||||
// Ok(value) => println!("Environment variable `{}` = {}", var_name, value),
|
||||
// Err(VarError::NotPresent) => println!("Environment variable `{}` is not set.", var_name),
|
||||
// Err(VarError::NotUnicode(_)) => println!("Environment variable `{}` is not a valid Unicode string", var_name),
|
||||
// }
|
||||
// }
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
use crate::core::threads::discord_presence_thread::discord_presence_thread;
|
||||
use errors::AnyhowResult;
|
||||
|
||||
pub fn spawn_discord_presence_thread(
|
||||
) -> AnyhowResult<()> {
|
||||
|
||||
tauri::async_runtime::spawn(discord_presence_thread());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
use crate::services::fal::threads::fal_task_polling_thread::fal_task_polling_thread;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use errors::AnyhowResult;
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
pub fn spawn_fal_task_polling_thread(
|
||||
app: &AppHandle,
|
||||
root: &AppDataRoot,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
fal_credential_manager: &FalCredentialManager,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
) -> AnyhowResult<()> {
|
||||
|
||||
tauri::async_runtime::spawn(fal_task_polling_thread(
|
||||
app.clone(),
|
||||
app_env_configs.clone(),
|
||||
root.clone(),
|
||||
fal_credential_manager.clone(),
|
||||
fal_task_queue.clone(),
|
||||
storyteller_creds_manager.clone(),
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::threads::main_window_thread::main_window_thread::main_window_thread;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use errors::AnyhowResult;
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
pub fn spawn_main_window_thread(
|
||||
app: &AppHandle,
|
||||
root: &AppDataRoot,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
) -> AnyhowResult<()> {
|
||||
|
||||
tauri::async_runtime::spawn(main_window_thread(
|
||||
app.clone(),
|
||||
root.clone(),
|
||||
storyteller_creds_manager.clone(),
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::sora::threads::sora_task_polling_thread::sora_task_polling_thread;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use errors::AnyhowResult;
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
pub fn spawn_sora_task_polling_thread(
|
||||
app: &AppHandle,
|
||||
root: &AppDataRoot,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
sora_credential_manager: &SoraCredentialManager,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
sora_task_queue: &SoraTaskQueue,
|
||||
) -> AnyhowResult<()> {
|
||||
|
||||
tauri::async_runtime::spawn(sora_task_polling_thread(
|
||||
app.clone(),
|
||||
app_env_configs.clone(),
|
||||
root.clone(),
|
||||
sora_credential_manager.clone(),
|
||||
storyteller_creds_manager.clone(),
|
||||
sora_task_queue.clone(),
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
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 crate::services::storyteller::threads::storyteller_task_polling_thread::storyteller_task_polling_thread;
|
||||
use errors::AnyhowResult;
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
pub fn spawn_storyteller_task_polling_thread(
|
||||
app: &AppHandle,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
task_database: &TaskDatabase,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
) -> AnyhowResult<()> {
|
||||
|
||||
tauri::async_runtime::spawn(storyteller_task_polling_thread(
|
||||
app.clone(),
|
||||
app_env_configs.clone(),
|
||||
task_database.clone(),
|
||||
storyteller_creds_manager.clone(),
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -22,7 +22,7 @@ impl DataSubdir for AppStateDir{
|
||||
|
||||
impl AppStateDir {
|
||||
pub fn get_tasks_sqlite_database_path(&self) -> PathBuf {
|
||||
self.path.join("tasks_v1.sqlite")
|
||||
self.path.join("tasks_v2.sqlite")
|
||||
}
|
||||
|
||||
pub fn get_window_size_config_file(&self) -> PathBuf {
|
||||
|
||||
@@ -3,8 +3,7 @@ pub mod app_preferences;
|
||||
pub mod app_startup_time;
|
||||
pub mod data_dir;
|
||||
pub mod expanduser;
|
||||
pub mod main_window_position;
|
||||
pub mod main_window_size;
|
||||
pub mod os_platform;
|
||||
pub mod provider_priority;
|
||||
pub mod task_database;
|
||||
pub mod task_database;
|
||||
pub mod window;
|
||||
@@ -2,6 +2,7 @@ use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use errors::AnyhowResult;
|
||||
use sqlite_tasks::connection::TaskDbConnection;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TaskDatabase {
|
||||
connection: TaskDbConnection,
|
||||
}
|
||||
|
||||
-8
@@ -42,14 +42,6 @@ impl MainWindowPosition {
|
||||
Ok(Some(pos))
|
||||
}
|
||||
|
||||
pub fn apply_to_main_window(&self, app: &AppHandle) -> AnyhowResult<()> {
|
||||
let windows = app.windows();
|
||||
let window = windows.get(MAIN_WINDOW_NAME)
|
||||
.ok_or_else(|| anyhow::anyhow!("Main window not found"))?;
|
||||
window.set_position(PhysicalPosition::new(self.x, self.y))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn persist_to_filesystem(&self, app_data_root: &AppDataRoot) -> AnyhowResult<()> {
|
||||
let filename = app_data_root.get_window_position_config_file();
|
||||
let json = serde_json::to_string(self)?;
|
||||
+19
-11
@@ -1,12 +1,13 @@
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::utils::best_window_size_heuristic::best_window_size_heuristic;
|
||||
use crate::core::windows::main_window::constants::MAIN_WINDOW_NAME;
|
||||
use crate::core::utils::window::get_window_size_heuristic::get_window_size_heuristic;
|
||||
use crate::core::windows::main_window::constants::{MAIN_WINDOW_MIN_HEIGHT, MAIN_WINDOW_MIN_WIDTH, MAIN_WINDOW_NAME};
|
||||
use errors::AnyhowResult;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use tauri::{AppHandle, Manager, PhysicalSize, Window};
|
||||
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
|
||||
pub struct MainWindowSize {
|
||||
pub width: u32,
|
||||
@@ -26,7 +27,7 @@ impl MainWindowSize {
|
||||
}
|
||||
|
||||
pub fn from_window(window: &Window) -> AnyhowResult<Self> {
|
||||
let size = best_window_size_heuristic(window)?;
|
||||
let size = get_window_size_heuristic(window)?;
|
||||
Ok(Self {
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
@@ -43,14 +44,6 @@ impl MainWindowSize {
|
||||
Ok(Some(size))
|
||||
}
|
||||
|
||||
pub fn apply_to_main_window(&self, app: &AppHandle) -> AnyhowResult<()> {
|
||||
let windows = app.windows();
|
||||
let window = windows.get(MAIN_WINDOW_NAME)
|
||||
.ok_or_else(|| anyhow::anyhow!("Main window not found"))?;
|
||||
window.set_size(PhysicalSize::new(self.width, self.height))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn persist_to_filesystem(&self, app_data_root: &AppDataRoot) -> AnyhowResult<()> {
|
||||
let filename = app_data_root.get_window_size_config_file();
|
||||
let json = serde_json::to_string(self)?;
|
||||
@@ -64,6 +57,21 @@ impl MainWindowSize {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Window size is not wide enough to persist or apply.
|
||||
pub fn is_not_wide_enough(&self) -> bool {
|
||||
self.width < MAIN_WINDOW_MIN_WIDTH
|
||||
}
|
||||
|
||||
/// Window size is not tall enough to persist or apply.
|
||||
pub fn is_not_tall_enough(&self) -> bool {
|
||||
self.height < MAIN_WINDOW_MIN_HEIGHT
|
||||
}
|
||||
|
||||
/// Window size is not big enough (tall or wide) to persist or apply.
|
||||
pub fn is_not_big_enough(&self) -> bool {
|
||||
self.is_not_wide_enough() || self.is_not_tall_enough()
|
||||
}
|
||||
|
||||
pub fn to_physical_size(&self) -> PhysicalSize<u32> {
|
||||
PhysicalSize::new(self.width, self.height)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod main_window_position;
|
||||
pub mod main_window_size;
|
||||
@@ -1,10 +0,0 @@
|
||||
pub mod frontend_subscriber_id;
|
||||
pub mod frontend_subscriber_payload;
|
||||
pub mod task_provider;
|
||||
pub mod task;
|
||||
pub mod task_id;
|
||||
pub mod task_pool;
|
||||
pub mod task_type;
|
||||
mod task_provider_job_id;
|
||||
mod task_state;
|
||||
pub mod task_database;
|
||||
@@ -1,42 +0,0 @@
|
||||
use multi_index_map::MultiIndexMap;
|
||||
use crate::core::tasks::task_id::TaskId;
|
||||
use crate::core::tasks::task_provider::TaskProvider;
|
||||
use crate::core::tasks::task_provider_job_id::TaskProviderJobId;
|
||||
use crate::core::tasks::task_state::TaskState;
|
||||
use crate::core::tasks::task_type::TaskType;
|
||||
|
||||
/// The provider's native identifier for the task.
|
||||
pub type ProviderTaskId = String;
|
||||
|
||||
#[derive(MultiIndexMap, Debug, Clone)]
|
||||
#[multi_index_derive(Debug)]
|
||||
#[multi_index_hash(rustc_hash::FxBuildHasher)]
|
||||
pub struct Task {
|
||||
#[multi_index(hashed_unique)]
|
||||
pub id: TaskId,
|
||||
|
||||
#[multi_index(hashed_non_unique)]
|
||||
pub task_state: TaskState,
|
||||
|
||||
pub task_type: Option<TaskType>,
|
||||
|
||||
#[multi_index(hashed_non_unique)]
|
||||
pub provider: Option<TaskProvider>,
|
||||
|
||||
/// Identifier used by the provider to track the job.
|
||||
#[multi_index(hashed_unique)]
|
||||
pub provider_job_id: Option<TaskProviderJobId>,
|
||||
|
||||
/// If the frontend is subscribing, it might have an identifier
|
||||
pub frontend_subscriber_id: Option<String>,
|
||||
|
||||
/// If the frontend is subscribing, it might have a payload.
|
||||
/// This is opaque, but could be for instance base64-encoded JSON.
|
||||
pub frontend_subscriber_payload: Option<String>,
|
||||
|
||||
// TODO: Submitted and completion datetimes
|
||||
|
||||
// TODO: Progress
|
||||
|
||||
// TODO: Completion media file, completion batch token
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
|
||||
pub struct TaskDatabase {
|
||||
pub pool: Pool<Sqlite>,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
pub async fn new(app_handle: &AppHandle) -> Result<Self> {
|
||||
let app_dir = app_handle
|
||||
.path_resolver()
|
||||
.app_data_dir()
|
||||
.expect("failed to get app dir");
|
||||
|
||||
// Ensure the app directory exists
|
||||
fs::create_dir_all(&app_dir)?;
|
||||
|
||||
let db_path = app_dir.join("grid_search.db");
|
||||
|
||||
// Set the DATABASE_URL environment variable to point to this SQLite file
|
||||
env::set_var("DATABASE_URL", format!("sqlite://{}", db_path.display()));
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
|
||||
/// Our identifier for tasks.
|
||||
/// These are ephemeral (for now), but may be peristed later on.
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct TaskId(String);
|
||||
|
||||
impl TaskId {
|
||||
pub fn generate() -> Self {
|
||||
let task_id = format!("tauri_task_{}", generate_random_uuid());
|
||||
Self(task_id)
|
||||
}
|
||||
|
||||
pub fn from_str(task_id: &str) -> Self {
|
||||
Self(task_id.to_string())
|
||||
}
|
||||
|
||||
pub fn to_string(&self) -> String {
|
||||
self.0.clone()
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
|
||||
/// Hand-rolling this instead of SQLite for now.
|
||||
/// Not sure if SQLite will create crazy build issues.
|
||||
/// Can replace with SQLite later, perhaps.
|
||||
pub struct TaskPool {
|
||||
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
|
||||
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum TaskProvider {
|
||||
Artcraft,
|
||||
Fal,
|
||||
Sora,
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct TaskProviderJobId(String);
|
||||
|
||||
impl TaskProviderJobId {
|
||||
pub fn from_str(job_id: &str) -> Self {
|
||||
Self(job_id.to_string())
|
||||
}
|
||||
|
||||
pub fn from_artcraft_job_id(job_id: InferenceJobToken) -> Self {
|
||||
Self(job_id.to_string())
|
||||
}
|
||||
|
||||
pub fn to_string(&self) -> String {
|
||||
self.0.clone()
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum TaskState {
|
||||
Submitted,
|
||||
InProgress,
|
||||
|
||||
// Terminal states
|
||||
|
||||
Complete,
|
||||
Failed,
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum TaskType {
|
||||
BackgroundRemoval,
|
||||
ImageGeneration,
|
||||
VideoGeneration,
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::core::state::app_startup_time::AppStartupTime;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::main_window_position::MainWindowPosition;
|
||||
use crate::core::state::main_window_size::MainWindowSize;
|
||||
use crate::core::state::window::main_window_position::MainWindowPosition;
|
||||
use crate::core::state::window::main_window_size::MainWindowSize;
|
||||
use crate::core::threads::main_window_thread::persist_storyteller_cookies_task::persist_storyteller_cookies_task;
|
||||
use crate::core::threads::main_window_thread::persist_window_position_task::persist_window_position_task;
|
||||
use crate::core::threads::main_window_thread::persist_window_resize_task::persist_window_resize_task;
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::main_window_position::MainWindowPosition;
|
||||
use crate::core::state::window::main_window_position::MainWindowPosition;
|
||||
use errors::AnyhowResult;
|
||||
use log::info;
|
||||
use memory_store::clone_slot::CloneSlot;
|
||||
|
||||
+7
-3
@@ -1,6 +1,6 @@
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::main_window_size::MainWindowSize;
|
||||
use crate::core::utils::best_window_size_heuristic::best_window_size_heuristic;
|
||||
use crate::core::state::window::main_window_size::MainWindowSize;
|
||||
use crate::core::utils::window::get_window_size_heuristic::get_window_size_heuristic;
|
||||
use errors::AnyhowResult;
|
||||
use log::info;
|
||||
use memory_store::clone_slot::CloneSlot;
|
||||
@@ -13,7 +13,7 @@ pub async fn persist_window_resize_task(
|
||||
) -> AnyhowResult<()> {
|
||||
|
||||
let current_window_size = MainWindowSize::from_window(window)?;
|
||||
let current_physical_size = best_window_size_heuristic(window)?;
|
||||
let current_physical_size = get_window_size_heuristic(window)?;
|
||||
|
||||
let mut save_size_to_disk = true;
|
||||
|
||||
@@ -22,6 +22,10 @@ pub async fn persist_window_resize_task(
|
||||
save_size_to_disk = false;
|
||||
}
|
||||
}
|
||||
|
||||
if current_window_size.is_not_big_enough() {
|
||||
save_size_to_disk = false;
|
||||
}
|
||||
|
||||
if save_size_to_disk {
|
||||
info!("Saving window size configs to disk...");
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
use crate::core::events::generation_events::common::GenerationServiceProvider;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
|
||||
// TODO(bt,2025-07-15): Get rid of GenerationServiceProvider
|
||||
pub fn to_generation_service_provider(provider: GenerationProvider) -> GenerationServiceProvider {
|
||||
match provider {
|
||||
GenerationProvider::Artcraft => GenerationServiceProvider::Artcraft,
|
||||
GenerationProvider::Fal => GenerationServiceProvider::Fal,
|
||||
GenerationProvider::Sora => GenerationServiceProvider::Sora,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod generation_provider;
|
||||
pub mod task_type;
|
||||
@@ -0,0 +1,11 @@
|
||||
use crate::core::events::generation_events::common::GenerationAction;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
|
||||
pub fn to_generation_action(task_type: TaskType) -> GenerationAction {
|
||||
match task_type {
|
||||
TaskType::ImageGeneration => GenerationAction::GenerateImage,
|
||||
TaskType::VideoGeneration => GenerationAction::GenerateVideo,
|
||||
TaskType::BackgroundRemoval => GenerationAction::RemoveBackground,
|
||||
TaskType::ObjectGeneration => GenerationAction::ImageTo3d,
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
pub mod best_window_size_heuristic;
|
||||
pub mod clear_all_webview_cookies;
|
||||
pub mod download_media_file_to_temp_dir;
|
||||
pub mod download_url_to_temp_dir;
|
||||
pub mod enum_conversion;
|
||||
pub mod get_url_file_extension;
|
||||
pub mod image;
|
||||
pub mod large_file_transfer;
|
||||
@@ -9,4 +9,4 @@ pub mod object_store;
|
||||
pub mod save_base64_image_to_temp_dir;
|
||||
pub mod simple_http_download;
|
||||
pub mod simple_http_download_to_tempfile;
|
||||
pub mod webview_unsafe;
|
||||
pub mod window;
|
||||
@@ -1,38 +0,0 @@
|
||||
use errors::AnyhowResult;
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
const MAIN_WINDOW_NAME: &str = "main";
|
||||
|
||||
pub fn webview_unsafe_for_app(app: &AppHandle) -> AnyhowResult<()> {
|
||||
let windows = app.windows();
|
||||
let maybe_window = windows.get(MAIN_WINDOW_NAME);
|
||||
|
||||
/*if let Some(window) = maybe_window {
|
||||
let webview = window.get_webview(MAIN_WINDOW_NAME);
|
||||
if let Some(webview) = webview {
|
||||
webview_unsafe(&webview);
|
||||
}
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("Main window not found"));
|
||||
}*/
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/*pub fn webview_unsafe(webview: &Webview) {
|
||||
let r = webview.with_webview(|webview| {
|
||||
webview.controller();
|
||||
#[cfg(target_os = "macos")]
|
||||
unsafe {
|
||||
let view: &objc2_web_kit::WKWebView = &*webview.inner().cast();
|
||||
let controller: &objc2_web_kit::WKUserContentController = &*webview.controller().cast();
|
||||
let window: &objc2_app_kit::NSWindow = &*webview.ns_window().cast();
|
||||
|
||||
//view.setPageZoom(40.);
|
||||
//controller.removeAllUserScripts();
|
||||
let bg_color = objc2_app_kit::NSColor::colorWithDeviceRed_green_blue_alpha(1.0, 0.2, 0.4, 1.);
|
||||
window.setBackgroundColor(Some(&bg_color));
|
||||
}
|
||||
|
||||
});
|
||||
}*/
|
||||
+4
-4
@@ -4,18 +4,18 @@ use tauri::{PhysicalSize, Window};
|
||||
// A typical height difference when dev tools are open:
|
||||
// Window inner size = PhysicalSize { width: 3704, height: 732 }
|
||||
// Window outer size = PhysicalSize { width: 3704, height: 1476 }
|
||||
const HEIGHT_DIFFERENCE_THRESHOLD: u32 = 300;
|
||||
const DEV_TOOLS_HEIGHT_DIFFERENCE_THRESHOLD: u32 = 300;
|
||||
|
||||
/// Get the best window size
|
||||
pub fn best_window_size_heuristic(window: &Window) -> AnyhowResult<PhysicalSize<u32>> {
|
||||
pub fn get_window_size_heuristic(window: &Window) -> AnyhowResult<PhysicalSize<u32>> {
|
||||
// NB: Ordinarily we'd use inner size, as the outer size includes the window decorations.
|
||||
// However, dev tools contract the inner size, and in the event they're open, we should
|
||||
// prefer the outer size.
|
||||
let inner = window.inner_size()?;
|
||||
let outer = window.outer_size()?;
|
||||
|
||||
|
||||
let height_delta = outer.height.saturating_sub(inner.height);
|
||||
if height_delta > HEIGHT_DIFFERENCE_THRESHOLD {
|
||||
if height_delta > DEV_TOOLS_HEIGHT_DIFFERENCE_THRESHOLD {
|
||||
// Dev tools are open, use the outer size
|
||||
Ok(outer)
|
||||
} else {
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod get_window_size_heuristic;
|
||||
pub mod position_main_window;
|
||||
pub mod resize_main_window;
|
||||
@@ -0,0 +1,65 @@
|
||||
use crate::core::state::window::main_window_position::MainWindowPosition;
|
||||
use crate::core::windows::main_window::constants::MAIN_WINDOW_NAME;
|
||||
use errors::AnyhowResult;
|
||||
use tauri::{AppHandle, Manager, PhysicalPosition};
|
||||
|
||||
/// Try to position the main window at startup.
|
||||
/// If it doesn't meet certain placement heuristics, abandon manual placement and let the OS
|
||||
/// decide where to place the app.
|
||||
pub fn position_main_window(
|
||||
app: &AppHandle,
|
||||
position: &MainWindowPosition,
|
||||
) -> AnyhowResult<()> {
|
||||
|
||||
// NB: This non-negative position heuristic is good for MacBook (single screen).
|
||||
// Not sure how this fares on Windows, Linux, or multiscreen setups.
|
||||
// On Mac, this prevents the windows from being positioned off to the top-left,
|
||||
// as (0,0) is the top left directly under the top menu bar.
|
||||
if position.x < 0 {
|
||||
// TODO(bt,2025-07-16): just in the case of x/width, allow slight placement off
|
||||
// screen as long as the app is wide enough.
|
||||
return Err(anyhow::anyhow!("X position must be non-negative"));
|
||||
} else if position.y < 0 {
|
||||
return Err(anyhow::anyhow!("Y position must be non-negative"));
|
||||
}
|
||||
|
||||
let windows = app.windows();
|
||||
|
||||
let window = windows.get(MAIN_WINDOW_NAME)
|
||||
.ok_or_else(|| anyhow::anyhow!("Main window not found"))?;
|
||||
|
||||
if let Ok(Some(monitor)) = window.current_monitor() {
|
||||
// Reports the size of the monitor, e.g. PhysicalSize { width: 4112, height: 2658 }
|
||||
// This is not impacted by `scale_factor`, even if that's a multiple like "2".
|
||||
// i.e., positioning the app at `monitor.size().width/2` and `monitor.size().height/2`
|
||||
// will place the **top left corner of the app** in roughly the center of the monitor.
|
||||
let monitor_size = monitor.size();
|
||||
|
||||
println!("Current monitor - size: {:?}", monitor_size);
|
||||
println!("Current monitor - scale factor: {:?}", monitor.scale_factor());
|
||||
println!("Current monitor - position: {:?}", monitor.position());
|
||||
|
||||
let width_padding = monitor_size.width.div_ceil(6);
|
||||
let height_padding = monitor_size.height.div_ceil(6);
|
||||
|
||||
let max_width = monitor_size.width.saturating_sub(width_padding);
|
||||
let max_height = monitor_size.height.saturating_sub(height_padding);
|
||||
|
||||
let max_width = i32::try_from(max_width)?;
|
||||
let max_height = i32::try_from(max_height)?;
|
||||
|
||||
// Prevent the window from being positioned off the screen to the bottom, right, and bottom right.
|
||||
// This works on MacBook (single screen), but should be tested on Windows, Linux, and multiscreen setups.
|
||||
if position.x > max_width {
|
||||
return Err(anyhow::anyhow!("X position must be less than or equal to {} (monitor width {})",
|
||||
max_width, monitor_size.width));
|
||||
} else if position.y > max_height {
|
||||
return Err(anyhow::anyhow!("Y position must be less than or equal to {} (monitor height {})",
|
||||
max_height, monitor_size.height));
|
||||
}
|
||||
}
|
||||
|
||||
window.set_position(PhysicalPosition::new(position.x, position.y))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
use crate::core::state::window::main_window_size::MainWindowSize;
|
||||
use crate::core::windows::main_window::constants::{MAIN_WINDOW_MIN_HEIGHT, MAIN_WINDOW_MIN_WIDTH, MAIN_WINDOW_NAME};
|
||||
use tauri::{AppHandle, Manager, PhysicalSize};
|
||||
|
||||
/// Try to resize the main window at startup.
|
||||
/// If it doesn't meet certain sizing heuristics, abandon manual resize and let the OS
|
||||
/// decide how to size the app.
|
||||
pub fn resize_main_window(
|
||||
app: &AppHandle,
|
||||
size: &MainWindowSize,
|
||||
) -> errors::AnyhowResult<()> {
|
||||
|
||||
let mut resize_width = size.width;
|
||||
let mut resize_height = size.height;
|
||||
let mut resize_was_corrected = false;
|
||||
|
||||
if resize_width < MAIN_WINDOW_MIN_WIDTH {
|
||||
resize_width = MAIN_WINDOW_MIN_WIDTH;
|
||||
resize_was_corrected = true;
|
||||
}
|
||||
if resize_height < MAIN_WINDOW_MIN_HEIGHT {
|
||||
resize_height = MAIN_WINDOW_MIN_HEIGHT;
|
||||
resize_was_corrected = true;
|
||||
}
|
||||
|
||||
let windows = app.windows();
|
||||
let window = windows.get(MAIN_WINDOW_NAME)
|
||||
.ok_or_else(|| anyhow::anyhow!("Main window not found"))?;
|
||||
|
||||
if let Ok(Some(monitor)) = window.current_monitor() {
|
||||
let scale_factor = monitor.scale_factor();
|
||||
|
||||
if scale_factor > 1.0 {
|
||||
|
||||
let min_width = MAIN_WINDOW_MIN_WIDTH as f64 * scale_factor;
|
||||
let min_height = MAIN_WINDOW_MIN_HEIGHT as f64 * scale_factor;
|
||||
|
||||
// Is this safe? https://users.rust-lang.org/t/convert-an-f64-to-an-i32/50991/4
|
||||
let min_width = min_width as u32;
|
||||
let min_height = min_height as u32;
|
||||
|
||||
if resize_width < min_width {
|
||||
resize_width = min_width;
|
||||
resize_was_corrected = true;
|
||||
}
|
||||
if resize_height < min_height {
|
||||
resize_height = min_height;
|
||||
resize_was_corrected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if resize_was_corrected {
|
||||
println!("Resizing window to: {}x{} (corrected from {}x{})",
|
||||
resize_width, resize_height, size.width, size.height);
|
||||
} else {
|
||||
println!("Resizing window to: {}x{} (no correction needed)", resize_width, resize_height);
|
||||
}
|
||||
|
||||
window.set_size(PhysicalSize::new(resize_width, resize_height))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -2,3 +2,11 @@
|
||||
/// The name of the main application window.
|
||||
/// This is the Artcraft application UX itself.
|
||||
pub const MAIN_WINDOW_NAME : &str = "main";
|
||||
|
||||
// TODO: handle pixel density, eg. retina displays
|
||||
/// This is the minimum width to resize the main window in an automated fashion
|
||||
pub const MAIN_WINDOW_MIN_WIDTH : u32 = 800;
|
||||
|
||||
// TODO: handle pixel density, eg. retina displays
|
||||
/// This is the minimum height to resize the main window in an automated fashion
|
||||
pub const MAIN_WINDOW_MIN_HEIGHT : u32 = 600;
|
||||
|
||||
@@ -19,12 +19,9 @@ use crate::core::lifecycle::startup::handle_tauri_startup::handle_tauri_startup;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::app_preferences::app_preferences_manager::load_app_preferences_or_default;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::main_window_position::MainWindowPosition;
|
||||
use crate::core::state::main_window_size::MainWindowSize;
|
||||
use crate::core::state::provider_priority::ProviderPriorityStore;
|
||||
use crate::core::threads::discord_presence_thread::discord_presence_thread;
|
||||
use crate::core::threads::main_window_thread::main_window_thread::main_window_thread;
|
||||
use crate::core::utils::webview_unsafe::webview_unsafe_for_app;
|
||||
use crate::services::fal::commands::fal_background_removal_command::fal_background_removal_command;
|
||||
use crate::services::fal::commands::fal_hunyuan_image_to_3d_command::fal_hunyuan_image_to_3d_command;
|
||||
use crate::services::fal::commands::fal_kling_image_to_video_command::fal_kling_image_to_video_command;
|
||||
@@ -87,36 +84,12 @@ pub fn run() {
|
||||
|
||||
let app_env_configs_2 = app_env_configs.clone();
|
||||
|
||||
let provider_priority = match ProviderPriorityStore::from_filesystem_configs(&app_data_root) {
|
||||
Ok(Some(priority)) => {
|
||||
println!("Loaded provider priority from disk: {:?}", priority.get_priority());
|
||||
priority
|
||||
}
|
||||
Ok(None) => {
|
||||
println!("No provider priority found on disk, using default.");
|
||||
ProviderPriorityStore::default()
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Failed to read provider priority from disk: {:?}", err);
|
||||
ProviderPriorityStore::default()
|
||||
}
|
||||
};
|
||||
|
||||
println!("Initializing backend runtime...");
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_http::init())
|
||||
.plugin(tauri_plugin_upload::init())
|
||||
.plugin(tauri_plugin_log::Builder::new()
|
||||
.level(log::LevelFilter::Info)
|
||||
.targets(vec![
|
||||
Target::new(TargetKind::Stdout),
|
||||
Target::new(TargetKind::LogDir {
|
||||
file_name: Some(app_data_root.log_file_name_str().to_string())
|
||||
}),
|
||||
])
|
||||
.build())
|
||||
.setup(move |app| {
|
||||
// TODO(bt): This is broken on windows
|
||||
// log_environment_details();
|
||||
@@ -129,68 +102,32 @@ pub fn run() {
|
||||
// )?;
|
||||
//}
|
||||
let app = app.handle().clone();
|
||||
let app2 = app.clone();
|
||||
let handle = app.clone();
|
||||
let root = app_data_root_2.clone();
|
||||
let env_config = app_env_configs_2.clone();
|
||||
let storyteller_creds = storyteller_creds_manager_2.clone();
|
||||
let sora_creds = sora_creds_manager_2.clone();
|
||||
let sora_tasks = sora_task_queue_2.clone();
|
||||
let fal_creds = fal_creds_manager_2.clone();
|
||||
let fal_tasks = fal_task_queue_2.clone();
|
||||
|
||||
tauri::async_runtime::block_on(async move {
|
||||
if let Err(err) = handle_tauri_startup(app2, root).await {
|
||||
let result = handle_tauri_startup(
|
||||
handle,
|
||||
root,
|
||||
env_config,
|
||||
storyteller_creds,
|
||||
sora_creds,
|
||||
sora_tasks,
|
||||
fal_creds,
|
||||
fal_tasks,
|
||||
).await;
|
||||
|
||||
if let Err(err) = result {
|
||||
error!("Failed to handle Tauri startup: {:?}", err);
|
||||
panic!("Failed to handle Tauri startup: {:?}", err);
|
||||
}
|
||||
});
|
||||
|
||||
let result = webview_unsafe_for_app(&app);
|
||||
if let Err(err) = result {
|
||||
eprintln!("Error setting webview unsafe: {:?}", err);
|
||||
}
|
||||
|
||||
// TODO(bt): Clean this up. We can just clone at the callsite. Also clean initialization
|
||||
let app_2 = app.clone();
|
||||
let app_3 = app.clone();
|
||||
let app_4 = app.clone();
|
||||
let app_5 = app.clone();
|
||||
let app_env_configs_3 = app_env_configs_2.clone();
|
||||
let app_data_root_3 = app_data_root_2.clone();
|
||||
let app_data_root_4 = app_data_root_2.clone();
|
||||
let app_data_root_5 = app_data_root_2.clone();
|
||||
let sora_creds_manager_3 = sora_creds_manager_2.clone();
|
||||
let storyteller_creds_manager_3 = storyteller_creds_manager.clone();
|
||||
let storyteller_creds_manager_4 = storyteller_creds_manager.clone();
|
||||
let fal_creds_manager_3 = fal_creds_manager_2.clone();
|
||||
let fal_task_queue_3 = fal_task_queue_2.clone();
|
||||
|
||||
match MainWindowSize::from_filesystem_configs(&app_data_root_3) {
|
||||
Ok(None) => {}
|
||||
Ok(Some(size)) => {
|
||||
println!("Resizing window to: {:?}", size);
|
||||
let result = size.apply_to_main_window(&app);
|
||||
if let Err(err) = result {
|
||||
eprintln!("Could not set window size: {:?}", err);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Failed to read window size from disk: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
match MainWindowPosition::from_filesystem_configs(&app_data_root_3) {
|
||||
Ok(None) => {}
|
||||
Ok(Some(pos)) => {
|
||||
println!("Moving window to: {:?}", pos);
|
||||
let result = pos.apply_to_main_window(&app);
|
||||
if let Err(err) = result {
|
||||
eprintln!("Could not set window position: {:?}", err);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Failed to read window position from disk: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
tauri::async_runtime::spawn(main_window_thread(app_3, app_data_root_3, storyteller_creds_manager_2));
|
||||
tauri::async_runtime::spawn(sora_task_polling_thread(app_4, app_env_configs_2, app_data_root_4, sora_creds_manager_3, storyteller_creds_manager_3, sora_task_queue_2));
|
||||
tauri::async_runtime::spawn(discord_presence_thread());
|
||||
tauri::async_runtime::spawn(fal_task_polling_thread(app_5, app_env_configs_3, app_data_root_5, fal_creds_manager_3, fal_task_queue_3, storyteller_creds_manager_4));
|
||||
|
||||
Ok(())
|
||||
})
|
||||
@@ -199,7 +136,6 @@ pub fn run() {
|
||||
.manage(app_preferences)
|
||||
.manage(fal_creds_manager)
|
||||
.manage(fal_task_queue)
|
||||
.manage(provider_priority)
|
||||
.manage(sora_creds_manager)
|
||||
.manage(sora_task_queue)
|
||||
.manage(storyteller_creds_manager_3)
|
||||
|
||||
@@ -145,10 +145,12 @@ pub async fn sora_image_remix_command(
|
||||
status = CommandErrorStatus::BadRequest;
|
||||
error_message = "Your Sora username is not yet created. Please visit the Sora.com website to create it first.";
|
||||
},
|
||||
InnerError::SoraError(SoraError::BadGateway(_) | SoraError::CloudFlareTimeout(_)) => {
|
||||
error_type = SoraImageRemixErrorType::SoraIsHavingProblems;
|
||||
status = CommandErrorStatus::ServerError;
|
||||
error_message = "Sora is having problems. Please try again later.";
|
||||
InnerError::SoraError(err) => {
|
||||
if err.is_sora_having_downtime_issues() {
|
||||
error_type = SoraImageRemixErrorType::SoraIsHavingProblems;
|
||||
status = CommandErrorStatus::ServerError;
|
||||
error_message = "Sora is having problems. Please try again later.";
|
||||
}
|
||||
}
|
||||
_ => {},
|
||||
}
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pub mod state;
|
||||
pub mod state;
|
||||
pub mod threads;
|
||||
@@ -0,0 +1 @@
|
||||
pub mod storyteller_task_polling_thread;
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
|
||||
use crate::core::events::generation_events::generation_complete_event::GenerationCompleteEvent;
|
||||
use crate::core::events::generation_events::generation_failed_event::GenerationFailedEvent;
|
||||
use crate::core::events::sendable_event_trait::SendableEvent;
|
||||
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::data_dir::trait_data_subdir::DataSubdir;
|
||||
use crate::core::state::task_database::TaskDatabase;
|
||||
use crate::core::utils::enum_conversion::generation_provider::to_generation_service_provider;
|
||||
use crate::core::utils::enum_conversion::task_type::to_generation_action;
|
||||
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 anyhow::anyhow;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::common::job_status_plus::JobStatusPlus;
|
||||
use enums::tauri::tasks::task_status::TaskStatus;
|
||||
use errors::AnyhowResult;
|
||||
use log::{error, info};
|
||||
use reqwest::Url;
|
||||
use sqlite_tasks::queries::list_tasks_by_provider_and_tokens::{list_tasks_by_provider_and_tokens, ListTasksArgs, Task};
|
||||
use sqlite_tasks::queries::update_task_status::{update_task_status, UpdateTaskArgs};
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use storyteller_client::error::api_error::ApiError;
|
||||
use storyteller_client::error::storyteller_error::StorytellerError;
|
||||
use storyteller_client::jobs::list_session_jobs::{list_session_jobs, States};
|
||||
use storyteller_client::media_files::upload_image_media_file_from_file::upload_image_media_file_from_file;
|
||||
use tauri::AppHandle;
|
||||
use tempdir::TempDir;
|
||||
|
||||
pub async fn storyteller_task_polling_thread(
|
||||
app_handle: AppHandle,
|
||||
app_env_configs: AppEnvConfigs,
|
||||
task_database: TaskDatabase,
|
||||
storyteller_creds_manager: StorytellerCredentialManager,
|
||||
) -> ! {
|
||||
loop {
|
||||
let res = polling_loop(
|
||||
&app_handle,
|
||||
&app_env_configs,
|
||||
&task_database,
|
||||
&storyteller_creds_manager,
|
||||
).await;
|
||||
if let Err(err) = res {
|
||||
error!("An error occurred: {:?}", err);
|
||||
}
|
||||
// NB: Only sleep if an error occurs.
|
||||
tokio::time::sleep(std::time::Duration::from_millis(30_000)).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn polling_loop(
|
||||
app_handle: &AppHandle,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
task_database: &TaskDatabase,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
) -> AnyhowResult<()> {
|
||||
loop {
|
||||
// Wait before next request for jobs.
|
||||
tokio::time::sleep(std::time::Duration::from_millis(5_000)).await;
|
||||
|
||||
let creds = storyteller_creds_manager.get_credentials()?;
|
||||
|
||||
let result = list_session_jobs(
|
||||
&app_env_configs.storyteller_host,
|
||||
creds.as_ref(),
|
||||
States::All,
|
||||
).await;
|
||||
|
||||
let jobs = match result {
|
||||
Ok(result) => result.jobs,
|
||||
Err(err) => {
|
||||
match &err {
|
||||
StorytellerError::Api(ApiError::TooManyRequests(message)) => {
|
||||
error!("Too many requests (sleeping): {:?}", message);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(60_000)).await;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
return Err(anyhow!(err));
|
||||
}
|
||||
};
|
||||
|
||||
let job_ids = jobs.iter()
|
||||
.map(|job| job.job_token.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let tasks = list_tasks_by_provider_and_tokens(ListTasksArgs {
|
||||
db: task_database.get_connection(),
|
||||
provider: GenerationProvider::Artcraft,
|
||||
provider_job_ids: Some(job_ids),
|
||||
}).await?;
|
||||
|
||||
let tasks = tasks.tasks;
|
||||
|
||||
let jobs_by_id = jobs.iter()
|
||||
.map(|job| (job.job_token.to_string(), job))
|
||||
.collect::<HashMap<String, _>>();
|
||||
|
||||
let tasks_by_provider_job_id = tasks.iter()
|
||||
.filter_map(|task| {
|
||||
if let Some(provider_job_id) = &task.provider_job_id {
|
||||
Some((provider_job_id.clone(), task.clone()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
//.map(|task| (task.provider_job_id.clone(), task.clone()))
|
||||
.collect::<HashMap<String, Task>>();
|
||||
|
||||
for job in jobs.iter() {
|
||||
match job.status.status {
|
||||
JobStatusPlus::CompleteSuccess => {} // Fall-through.
|
||||
_ => continue,
|
||||
}
|
||||
|
||||
let task = match tasks_by_provider_job_id.get(job.job_token.as_str()) {
|
||||
Some(job) => job,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
match task.status {
|
||||
TaskStatus::CompleteSuccess => continue,
|
||||
_ => {} // Fall-through.
|
||||
}
|
||||
|
||||
let updated = update_task_status(UpdateTaskArgs {
|
||||
db: task_database.get_connection(),
|
||||
task_id: &task.id,
|
||||
status: TaskStatus::CompleteSuccess,
|
||||
}).await?;
|
||||
|
||||
if !updated {
|
||||
continue; // If anything breaks with queries, don't spam events.
|
||||
}
|
||||
|
||||
let service = to_generation_service_provider(task.provider);
|
||||
let action = to_generation_action(task.task_type);
|
||||
|
||||
let event = GenerationCompleteEvent {
|
||||
action: Some(action),
|
||||
service,
|
||||
model: None, // TODO
|
||||
};
|
||||
|
||||
if let Err(err) = event.send(&app_handle) {
|
||||
error!("Failed to send GenerationCompleteEvent: {:?}", err); // Fail open
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "cloudflare_errors"
|
||||
edition = "2024"
|
||||
version = "0.0.1"
|
||||
publish = false
|
||||
|
||||
[package.metadata.cargo-udeps.ignore]
|
||||
normal = ["workspace-build-acceleration"]
|
||||
|
||||
[lib]
|
||||
name = "cloudflare_errors"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
# cargo-hakari workspace hack (faster compile times)
|
||||
workspace-build-acceleration.workspace = true
|
||||
|
||||
# Internal
|
||||
# (None)
|
||||
|
||||
# External
|
||||
# (None)
|
||||
|
||||
[dev-dependencies]
|
||||
# (None)
|
||||
@@ -0,0 +1,32 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CloudflareError {
|
||||
/// Cloudflare wants to verify the request with a CAPTCHA challenge.
|
||||
ChallengeInterstitial403,
|
||||
|
||||
/// Cloudflare could not form a connection to the backend server.
|
||||
GatewayTimeout504,
|
||||
|
||||
/// Cloudflare formed a TCP connection to the backend server, but no payload was delivered before timeout
|
||||
TimeoutOccurred524,
|
||||
}
|
||||
|
||||
impl Error for CloudflareError {}
|
||||
|
||||
impl Display for CloudflareError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::ChallengeInterstitial403 => {
|
||||
write!(f, "Cloudflare Challenge Interstitial (403); Cloudflare wants to verify the request with a CAPTCHA challenge.")
|
||||
}
|
||||
Self::GatewayTimeout504 => {
|
||||
write!(f, "Cloudflare Gateway Timeout (504); This is likely a backend server issue.")
|
||||
}
|
||||
Self::TimeoutOccurred524 => {
|
||||
write!(f, "Cloudflare Timeout (524); This is likely a backend server issue. Cloudflare connected, but did not receive a response from the server in time.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use crate::cloudflare_error::CloudflareError;
|
||||
|
||||
/// This only filters out Cloudflare errors. Any other errors will be returned as `Ok(())`.
|
||||
pub fn filter_cloudflare_errors(status_code: u16, body: &str) -> Result<(), CloudflareError> {
|
||||
if status_code >= 200 && status_code < 300 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match status_code {
|
||||
403 => {
|
||||
if body.contains("challenge-platform")
|
||||
|| body.contains("challenge-error-text")
|
||||
|| body.contains("cType: 'managed'")
|
||||
|| body.contains("Just a moment...") {
|
||||
return Err(CloudflareError::ChallengeInterstitial403);
|
||||
}
|
||||
}
|
||||
_ => {}, // Fall-through
|
||||
}
|
||||
|
||||
// TODO: This is a really bad heuristic.
|
||||
let is_cloudflare = body.contains("cloudflare")
|
||||
|| body.contains("Cloudflare");
|
||||
|
||||
// let is_cloudflare = body.contains("cloudflare.com")
|
||||
// || body.contains("Cloudflare Ray ID");
|
||||
|
||||
if is_cloudflare {
|
||||
match status_code {
|
||||
504 => return Err(CloudflareError::GatewayTimeout504),
|
||||
524 => return Err(CloudflareError::TimeoutOccurred524),
|
||||
_ => {}, // Fall-through
|
||||
}
|
||||
|
||||
if body.contains("errorcode_504")
|
||||
|| body.contains("Gateway time-out")
|
||||
|| body.contains("Error code 504")
|
||||
{
|
||||
return Err(CloudflareError::GatewayTimeout504);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
//! cloudflare_errors
|
||||
//!
|
||||
//! Classify common cloudflare errors so clients can handle appropriately.
|
||||
//!
|
||||
|
||||
// Never allow these
|
||||
#![forbid(private_bounds)]
|
||||
#![forbid(private_interfaces)]
|
||||
#![forbid(unused_must_use)] // NB: It's unsafe to not close/check some things
|
||||
|
||||
// Okay to toggle
|
||||
#![forbid(unreachable_patterns)]
|
||||
#![forbid(unused_imports)]
|
||||
#![forbid(unused_mut)]
|
||||
#![forbid(unused_variables)]
|
||||
|
||||
// Always allow
|
||||
#![allow(dead_code)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
pub mod cloudflare_error;
|
||||
pub mod filter_cloudflare_errors;
|
||||
@@ -38,9 +38,6 @@ serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
# For the bootstrap tool (TODO: Move this)
|
||||
tokio = { version = "1.42", features = ["rt", "rt-multi-thread", "fs"] }
|
||||
|
||||
[dev-dependencies]
|
||||
# serial_test = "3.0.0"
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode};
|
||||
use sqlx::{Pool, Sqlite, SqlitePool};
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TaskDbConnection {
|
||||
pool: Pool<Sqlite>,
|
||||
}
|
||||
|
||||
@@ -3,12 +3,13 @@ use std::fmt::{Display, Formatter};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SqliteTasksError {
|
||||
SqlxError(sqlx::Error),
|
||||
//TaskNotFound,
|
||||
//TaskAlreadyExists,
|
||||
//InvalidTaskStatus,
|
||||
//InvalidTaskType,
|
||||
//InvalidGenerationProvider,
|
||||
SqlxError(sqlx::Error),
|
||||
TaskParseError(String),
|
||||
//TaskNotFound,
|
||||
//TaskAlreadyExists,
|
||||
//InvalidTaskStatus,
|
||||
//InvalidTaskType,
|
||||
//InvalidGenerationProvider,
|
||||
}
|
||||
|
||||
impl Error for SqliteTasksError {}
|
||||
@@ -17,6 +18,7 @@ impl Display for SqliteTasksError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
SqliteTasksError::SqlxError(err) => write!(f, "SQLx error: {}", err),
|
||||
SqliteTasksError::TaskParseError(err) => write!(f, "Task parse error: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::connection::TaskDbConnection;
|
||||
use crate::error::SqliteTasksError;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_model_type::TaskModelType;
|
||||
use enums::tauri::tasks::task_status::TaskStatus;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use tokens::tokens::sqlite::tasks::TaskId;
|
||||
@@ -9,6 +10,7 @@ pub struct CreateTaskArgs<'a> {
|
||||
pub db: &'a TaskDbConnection,
|
||||
pub status: TaskStatus,
|
||||
pub task_type: TaskType,
|
||||
pub model_type: Option<TaskModelType>,
|
||||
pub provider: GenerationProvider,
|
||||
pub provider_job_id: Option<&'a str>,
|
||||
pub frontend_subscriber_id: Option<&'a str>,
|
||||
@@ -24,6 +26,7 @@ pub async fn create_task(
|
||||
let task_id_temp = task_id.as_str();
|
||||
let status_temp = args.status.to_str();
|
||||
let task_type_temp = args.task_type.to_str();
|
||||
let model_type_temp = args.model_type.map(|s| s.to_str());
|
||||
let provider_temp = args.provider.to_string();
|
||||
|
||||
let query = sqlx::query!(r#"
|
||||
@@ -31,16 +34,18 @@ pub async fn create_task(
|
||||
id,
|
||||
task_status,
|
||||
task_type,
|
||||
model_type,
|
||||
provider,
|
||||
provider_job_id,
|
||||
frontend_subscriber_id,
|
||||
frontend_subscriber_payload
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"#,
|
||||
task_id_temp,
|
||||
status_temp,
|
||||
task_type_temp,
|
||||
model_type_temp,
|
||||
provider_temp,
|
||||
args.provider_job_id,
|
||||
args.frontend_subscriber_id,
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
use crate::connection::TaskDbConnection;
|
||||
use crate::error::SqliteTasksError;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_model_type::TaskModelType;
|
||||
use enums::tauri::tasks::task_status::TaskStatus;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use sqlx::{QueryBuilder, Sqlite};
|
||||
use tokens::tokens::sqlite::tasks::TaskId;
|
||||
|
||||
pub struct ListTasksArgs<'a> {
|
||||
pub db: &'a TaskDbConnection,
|
||||
pub provider: GenerationProvider,
|
||||
pub provider_job_ids: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
pub struct TaskList {
|
||||
pub tasks: Vec<Task>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Task {
|
||||
pub id: TaskId,
|
||||
pub status: TaskStatus,
|
||||
pub task_type: TaskType,
|
||||
pub model_type: TaskModelType,
|
||||
pub provider: GenerationProvider,
|
||||
pub provider_job_id: Option<String>,
|
||||
pub frontend_subscriber_id: Option<String>,
|
||||
pub frontend_subscriber_payload: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(sqlx::FromRow)]
|
||||
struct RawTask {
|
||||
id: String,
|
||||
task_status: String,
|
||||
task_type: String,
|
||||
model_type: String,
|
||||
provider: String,
|
||||
provider_job_id: Option<String>,
|
||||
frontend_subscriber_id: Option<String>,
|
||||
frontend_subscriber_payload: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn list_tasks_by_provider_and_tokens(
|
||||
args: ListTasksArgs<'_>,
|
||||
) -> Result<TaskList, SqliteTasksError> {
|
||||
|
||||
let mut query_builder: QueryBuilder<Sqlite> = QueryBuilder::new(r#"
|
||||
SELECT
|
||||
id,
|
||||
task_status,
|
||||
task_type,
|
||||
model_type,
|
||||
provider,
|
||||
provider_job_id,
|
||||
frontend_subscriber_id,
|
||||
frontend_subscriber_payload
|
||||
FROM tasks
|
||||
WHERE provider =
|
||||
"#);
|
||||
|
||||
// TODO(bt,2025-07-15): Fix this. The sqlx mysql queries never required temporaries
|
||||
let provider = args.provider.to_string();
|
||||
|
||||
query_builder.push_bind(provider);
|
||||
|
||||
if let Some(provider_job_ids) = args.provider_job_ids {
|
||||
query_builder.push(" AND provider_job_id IN (");
|
||||
let mut separated = query_builder.separated(", ");
|
||||
|
||||
for job_id in provider_job_ids.into_iter() {
|
||||
separated.push_bind(job_id);
|
||||
}
|
||||
|
||||
separated.push_unseparated(") ");
|
||||
}
|
||||
|
||||
let query = query_builder.build_query_as::<RawTask>();
|
||||
|
||||
// info!("Query: {:?}", query.sql());
|
||||
|
||||
let results = query.fetch_all(args.db.get_pool()).await?;
|
||||
|
||||
let mut tasks: Vec<Task> = Vec::new();
|
||||
|
||||
for task in results {
|
||||
tasks.push(Task {
|
||||
id: TaskId::new_from_str(&task.id),
|
||||
status: TaskStatus::from_str(&task.task_status)
|
||||
.map_err(|err| SqliteTasksError::TaskParseError(err))?,
|
||||
task_type: TaskType::from_str(&task.task_type)
|
||||
.map_err(|err| SqliteTasksError::TaskParseError(err))?,
|
||||
model_type: TaskModelType::from_str(&task.model_type)
|
||||
.map_err(|err| SqliteTasksError::TaskParseError(err))?,
|
||||
provider: GenerationProvider::from_str(&task.provider)
|
||||
.map_err(|err| SqliteTasksError::TaskParseError(err))?,
|
||||
provider_job_id: task.provider_job_id,
|
||||
frontend_subscriber_id: task.frontend_subscriber_id,
|
||||
frontend_subscriber_payload: task.frontend_subscriber_payload,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(TaskList {
|
||||
tasks,
|
||||
})
|
||||
}
|
||||
@@ -1 +1,3 @@
|
||||
pub mod create_task;
|
||||
pub mod create_task;
|
||||
pub mod list_tasks_by_provider_and_tokens;
|
||||
pub mod update_task_status;
|
||||
@@ -0,0 +1,39 @@
|
||||
use crate::connection::TaskDbConnection;
|
||||
use crate::error::SqliteTasksError;
|
||||
use enums::tauri::tasks::task_status::TaskStatus;
|
||||
use tokens::tokens::sqlite::tasks::TaskId;
|
||||
|
||||
pub struct UpdateTaskArgs<'a> {
|
||||
pub db: &'a TaskDbConnection,
|
||||
pub task_id: &'a TaskId,
|
||||
pub status: TaskStatus,
|
||||
}
|
||||
|
||||
/// Returns true if rows were updated.
|
||||
pub async fn update_task_status(
|
||||
args: UpdateTaskArgs<'_>,
|
||||
) -> Result<bool, SqliteTasksError> {
|
||||
|
||||
// TODO(bt,2025-07-12): Fix this. The sqlx mysql queries never required temporaries
|
||||
let task_id_temp = args.task_id.as_str();
|
||||
let status_temp = args.status.to_str();
|
||||
|
||||
// info!("Update task id: {}, status: {}", task_id_temp, status_temp);
|
||||
|
||||
// TODO(bt,2025-07-15): We can't set a LIMIT without a certain compiler flag for SQLite ?
|
||||
let query = sqlx::query!(r#"
|
||||
UPDATE tasks
|
||||
SET task_status = ?
|
||||
WHERE id = ?
|
||||
"#,
|
||||
status_temp,
|
||||
task_id_temp,
|
||||
);
|
||||
|
||||
// info!("query: {:?}", query.sql());
|
||||
|
||||
let res = query.execute(args.db.get_pool()).await?;
|
||||
let rows_updated = res.rows_affected() > 0;
|
||||
|
||||
Ok(rows_updated)
|
||||
}
|
||||
@@ -8,25 +8,29 @@ authors = [
|
||||
]
|
||||
publish = false
|
||||
|
||||
[package.metadata.cargo-udeps.ignore]
|
||||
normal = ["workspace-build-acceleration"]
|
||||
|
||||
[lib]
|
||||
name = "artcraft_api_defs"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[package.metadata.cargo-udeps.ignore]
|
||||
normal = ["workspace-build-acceleration"]
|
||||
|
||||
[dependencies]
|
||||
# Hackari workspace (faster compile times)
|
||||
workspace-build-acceleration = { workspace = true }
|
||||
workspace-build-acceleration.workspace = true
|
||||
|
||||
# Internal
|
||||
enums = { workspace = true }
|
||||
tokens = { workspace = true }
|
||||
enums.workspace = true
|
||||
tokens.workspace = true
|
||||
|
||||
# External
|
||||
chrono.workspace = true
|
||||
url.workspace = true
|
||||
|
||||
# Serialization
|
||||
serde = { workspace = true }
|
||||
serde_derive = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
utoipa = { version = "4", features = ["chrono", "actix_extras"] }
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pub mod responses;
|
||||
+16
-3
@@ -1,9 +1,10 @@
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
/// Details about submitted live portrait jobs (request arguments only)
|
||||
#[derive(Serialize, ToSchema)]
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct JobDetailsLivePortraitRequest {
|
||||
/// Source media token
|
||||
/// This can be an image or a video media file token
|
||||
@@ -18,3 +19,15 @@ pub struct JobDetailsLivePortraitRequest {
|
||||
/// This video must contain a face.
|
||||
pub face_driver_media_file_token: MediaFileToken,
|
||||
}
|
||||
|
||||
/// Details about submitted lipsync jobs (request arguments only)
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct JobDetailsLipsyncRequest {
|
||||
/// Media file token for the source audio.
|
||||
/// This is probably an audio file, but in the future we might pull audio from video.
|
||||
pub audio_source_token: MediaFileToken,
|
||||
|
||||
/// Media file token for the source visuals.
|
||||
/// This is either an image or video.
|
||||
pub image_or_video_source_token: MediaFileToken,
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use url::Url;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Links to media file locations (bucket, CDN, etc.)
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug, Clone, Eq, PartialEq)]
|
||||
pub struct MediaLinks {
|
||||
/// Primary link to the asset via the CDN.
|
||||
pub cdn_url: Url,
|
||||
|
||||
/// Template to construct thumbnail URLs.
|
||||
/// Replace the string `{WIDTH}` with the desired width.
|
||||
/// Only relevant for image media files. (Video media files instead have
|
||||
/// video previews, which, in turn, have their own thumbnail templates.)
|
||||
pub maybe_thumbnail_template: Option<String>,
|
||||
|
||||
/// Video preview images (still and animated gif) for mp4 video files.
|
||||
/// These are only set for video media files.
|
||||
/// These are not set for "cover images" that are gif or webms, but rather video files.
|
||||
pub maybe_video_previews: Option<VideoPreviews>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug, Clone, Eq, PartialEq)]
|
||||
pub struct VideoPreviews {
|
||||
/// A static single frame preview image of the video.
|
||||
pub still: Url,
|
||||
|
||||
/// An animated gif preview of the video.
|
||||
pub animated: Url,
|
||||
|
||||
/// A template used to construct the still thumbnail URL.
|
||||
/// Replace the string `{WIDTH}` with the desired width.
|
||||
pub still_thumbnail_template: String,
|
||||
|
||||
/// A template used to construct the animated thumbnail URL.
|
||||
/// Replace the string `{WIDTH}` with the desired width.
|
||||
pub animated_thumbnail_template: String,
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod job_details;
|
||||
pub mod media_links;
|
||||
@@ -0,0 +1,124 @@
|
||||
use crate::common::responses::job_details::{JobDetailsLipsyncRequest, JobDetailsLivePortraitRequest};
|
||||
use crate::common::responses::media_links::MediaLinks;
|
||||
use chrono::{DateTime, Utc};
|
||||
use enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
|
||||
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
|
||||
use enums::common::job_status_plus::JobStatusPlus;
|
||||
use enums::no_table::style_transfer::style_transfer_name::StyleTransferName;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
pub const LIST_SESSION_JOBS_URL_PATH: &str = "/v1/jobs/session";
|
||||
|
||||
#[derive(Deserialize, ToSchema, IntoParams)]
|
||||
pub struct ListSessionJobsQueryParams {
|
||||
//pub sort_ascending: Option<bool>,
|
||||
//pub page_size: Option<usize>, // TODO(bt,2024-04-23): One page for now.
|
||||
//pub page_index: Option<usize>, // TODO(bt,2024-04-23): One page for now.
|
||||
|
||||
/// NB: This can be one (or more comma-separated values) from `JobStatusPlus`.
|
||||
/// ?include_states=pending or ?include_states=pending,started,complete_success (etc.)
|
||||
pub include_states: Option<String>,
|
||||
|
||||
/// The opposite of include_states, this filters out states from view.
|
||||
pub exclude_states: Option<String>,
|
||||
|
||||
// TODO(bt,2024-04-23): Add the ability for users to dismiss completed/dead jobs from view.
|
||||
}
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct ListSessionJobsSuccessResponse {
|
||||
pub success: bool,
|
||||
|
||||
/// This is not paginated and is limited to showing 100 jobs.
|
||||
pub jobs: Vec<ListSessionJobsItem>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct ListSessionJobsItem {
|
||||
pub job_token: InferenceJobToken,
|
||||
|
||||
pub request: ListSessionRequestDetailsResponse,
|
||||
pub status: ListSessionStatusDetailsResponse,
|
||||
pub maybe_result: Option<ListSessionResultDetailsResponse>,
|
||||
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Details about what the user requested for generation
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct ListSessionRequestDetailsResponse {
|
||||
pub inference_category: InferenceCategory,
|
||||
pub maybe_model_type: Option<String>,
|
||||
pub maybe_model_token: Option<String>,
|
||||
|
||||
/// OPTIONAL. Title of the model, if it has one
|
||||
pub maybe_model_title: Option<String>,
|
||||
|
||||
/// OPTIONAL. If the result was TTS, this is the raw inference text.
|
||||
pub maybe_raw_inference_text: Option<String>,
|
||||
|
||||
/// OPTIONAL. For Comfy / Video Style Transfer jobs, this might include
|
||||
/// the name of the selected style.
|
||||
pub maybe_style_name: Option<StyleTransferName>,
|
||||
|
||||
/// OPTIONAL. For Live Portrait jobs, this is additional information on the request.
|
||||
pub maybe_live_portrait_details: Option<JobDetailsLivePortraitRequest>,
|
||||
|
||||
/// OPTIONAL. For lipsync jobs (face fusion and sad talker), this is additional
|
||||
/// information on the request.
|
||||
pub maybe_lipsync_details: Option<JobDetailsLipsyncRequest>,
|
||||
}
|
||||
|
||||
/// Details about the ongoing job status
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct ListSessionStatusDetailsResponse {
|
||||
/// Primary status from the database (a state machine).
|
||||
pub status: JobStatusPlus,
|
||||
|
||||
/// Extra, temporary status from Redis.
|
||||
/// This can denote inference progress, and the Python code can write to it.
|
||||
pub maybe_extra_status_description: Option<String>,
|
||||
|
||||
pub maybe_assigned_worker: Option<String>,
|
||||
pub maybe_assigned_cluster: Option<String>,
|
||||
|
||||
pub maybe_first_started_at: Option<DateTime<Utc>>,
|
||||
|
||||
/// If the job is currently running, this is how long it has been running in seconds.
|
||||
/// This is heuristic estimate since we don't record the precise start time across runs.
|
||||
pub maybe_current_execution_duration_seconds: Option<u64>,
|
||||
|
||||
pub attempt_count: u8,
|
||||
|
||||
/// Whether the frontend needs to maintain a keepalive check.
|
||||
/// This is typically only for non-premium users.
|
||||
pub requires_keepalive: bool,
|
||||
|
||||
/// An enum the frontend can use to display localized/I18N error
|
||||
/// messages. These pertain to both transient and permanent failures.
|
||||
pub maybe_failure_category: Option<FrontendFailureCategory>,
|
||||
|
||||
/// This is an integer number between 0 and 100 (both inclusive) that
|
||||
/// reports the completeness.
|
||||
pub progress_percentage: u8,
|
||||
}
|
||||
|
||||
/// Details about the completed result (if any)
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct ListSessionResultDetailsResponse {
|
||||
pub entity_type: String,
|
||||
pub entity_token: String,
|
||||
|
||||
/// (DEPRECATED) URL path to the media file
|
||||
#[deprecated(note="This field doesn't point to the full URL. Use media_links instead to leverage the CDN.")]
|
||||
pub maybe_public_bucket_media_path: Option<String>,
|
||||
|
||||
/// Rich CDN links to the media, including thumbnails, previews, and more.
|
||||
pub media_links: MediaLinks,
|
||||
|
||||
pub maybe_successfully_completed_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pub mod list_session_jobs;
|
||||
@@ -1 +1,3 @@
|
||||
pub mod generate;
|
||||
pub mod common;
|
||||
pub mod generate;
|
||||
pub mod jobs;
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod task_model_type;
|
||||
pub mod task_status;
|
||||
pub mod task_type;
|
||||
@@ -0,0 +1,241 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
#[cfg(test)]
|
||||
use strum::EnumCount;
|
||||
#[cfg(test)]
|
||||
use strum::EnumIter;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[cfg_attr(test, derive(EnumIter, EnumCount))]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TaskModelType {
|
||||
// Image models
|
||||
#[serde(rename = "flux_1_dev")]
|
||||
Flux1Dev,
|
||||
#[serde(rename = "flux_1_schnell")]
|
||||
Flux1Schnell,
|
||||
#[serde(rename = "flux_pro_1.1")]
|
||||
FluxPro11,
|
||||
#[serde(rename = "flux_pro_1.1_ultra")]
|
||||
FluxPro11Ultra,
|
||||
#[serde(rename = "gpt_image_1")]
|
||||
GptImage1,
|
||||
#[serde(rename = "recraft_3")]
|
||||
Recraft3,
|
||||
|
||||
// Video models
|
||||
#[serde(rename = "kling_1.6_pro")]
|
||||
Kling16Pro,
|
||||
#[serde(rename = "kling_2.1_pro")]
|
||||
Kling21Pro,
|
||||
#[serde(rename = "kling_2.1_master")]
|
||||
Kling21Master,
|
||||
#[serde(rename = "seedance_1.0_lite")]
|
||||
Seedance10Lite,
|
||||
#[serde(rename = "veo_2")]
|
||||
Veo2,
|
||||
|
||||
// 3D Object generation models
|
||||
#[serde(rename = "hunyuan_3d_2.0")]
|
||||
Hunyuan3d2_0,
|
||||
#[serde(rename = "hunyuan_3d_2.1")]
|
||||
Hunyuan3d2_1,
|
||||
}
|
||||
|
||||
impl_enum_display_and_debug_using_to_str!(TaskModelType);
|
||||
//impl_mysql_enum_coders!(TaskModelType);
|
||||
//impl_mysql_from_row!(TaskModelType);
|
||||
|
||||
// NB: We can derive `sqlx::Type` instead of using `impl_mysql_enum_coders`
|
||||
|
||||
impl TaskModelType {
|
||||
pub fn to_str(&self) -> &'static str {
|
||||
match self {
|
||||
// Image models
|
||||
Self::Flux1Dev => "flux_1_dev",
|
||||
Self::Flux1Schnell => "flux_1_schnell",
|
||||
Self::FluxPro11 => "flux_pro_1.1",
|
||||
Self::FluxPro11Ultra => "flux_pro_1.1_ultra",
|
||||
Self::GptImage1 => "gpt_image_1",
|
||||
Self::Recraft3 => "recraft_3",
|
||||
// Video models
|
||||
Self::Kling16Pro => "kling_1.6_pro",
|
||||
Self::Kling21Pro => "kling_2.1_pro",
|
||||
Self::Kling21Master => "kling_2.1_master",
|
||||
Self::Seedance10Lite => "seedance_1.0_lite",
|
||||
Self::Veo2 => "veo_2",
|
||||
// 3D Object generation models
|
||||
Self::Hunyuan3d2_0 => "hunyuan_3d_2.0",
|
||||
Self::Hunyuan3d2_1 => "hunyuan_3d_2.1",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(job_status: &str) -> Result<Self, String> {
|
||||
match job_status {
|
||||
// Image models
|
||||
"flux_1_dev" => Ok(Self::Flux1Dev),
|
||||
"flux_1_schnell" => Ok(Self::Flux1Schnell),
|
||||
"flux_pro_1.1" => Ok(Self::FluxPro11),
|
||||
"flux_pro_1.1_ultra" => Ok(Self::FluxPro11Ultra),
|
||||
"gpt_image_1" => Ok(Self::GptImage1),
|
||||
"recraft_3" => Ok(Self::Recraft3),
|
||||
// Video models
|
||||
"kling_1.6_pro" => Ok(Self::Kling16Pro),
|
||||
"kling_2.1_pro" => Ok(Self::Kling21Pro),
|
||||
"kling_2.1_master" => Ok(Self::Kling21Master),
|
||||
"seedance_1.0_lite" => Ok(Self::Seedance10Lite),
|
||||
"veo_2" => Ok(Self::Veo2),
|
||||
// 3D Object generation models
|
||||
"hunyuan_3d_2.0" => Ok(Self::Hunyuan3d2_0),
|
||||
"hunyuan_3d_2.1" => Ok(Self::Hunyuan3d2_1),
|
||||
_ => Err(format!("invalid task_state: {:?}", job_status)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn all_variants() -> BTreeSet<Self> {
|
||||
// NB: BTreeSet is sorted
|
||||
// NB: BTreeSet::from() isn't const, but not worth using LazyStatic, etc.
|
||||
BTreeSet::from([
|
||||
// Image models
|
||||
Self::Flux1Dev,
|
||||
Self::Flux1Schnell,
|
||||
Self::FluxPro11,
|
||||
Self::FluxPro11Ultra,
|
||||
Self::GptImage1,
|
||||
Self::Recraft3,
|
||||
// Video models
|
||||
Self::Kling16Pro,
|
||||
Self::Kling21Pro,
|
||||
Self::Kling21Master,
|
||||
Self::Seedance10Lite,
|
||||
Self::Veo2,
|
||||
// 3D Object generation models
|
||||
Self::Hunyuan3d2_0,
|
||||
Self::Hunyuan3d2_1,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::test_helpers::assert_serialization;
|
||||
use crate::tauri::tasks::task_model_type::TaskModelType;
|
||||
|
||||
mod explicit_checks {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_serialization() {
|
||||
// Image models
|
||||
assert_serialization(TaskModelType::Flux1Dev, "flux_1_dev");
|
||||
assert_serialization(TaskModelType::Flux1Schnell, "flux_1_schnell");
|
||||
assert_serialization(TaskModelType::FluxPro11, "flux_pro_1.1");
|
||||
assert_serialization(TaskModelType::FluxPro11Ultra, "flux_pro_1.1_ultra");
|
||||
assert_serialization(TaskModelType::GptImage1, "gpt_image_1");
|
||||
assert_serialization(TaskModelType::Recraft3, "recraft_3");
|
||||
// Video models
|
||||
assert_serialization(TaskModelType::Kling16Pro, "kling_1.6_pro");
|
||||
assert_serialization(TaskModelType::Kling21Pro, "kling_2.1_pro");
|
||||
assert_serialization(TaskModelType::Kling21Master, "kling_2.1_master");
|
||||
assert_serialization(TaskModelType::Seedance10Lite, "seedance_1.0_lite");
|
||||
assert_serialization(TaskModelType::Veo2, "veo_2");
|
||||
// 3D Object generation models
|
||||
assert_serialization(TaskModelType::Hunyuan3d2_0, "hunyuan_3d_2.0");
|
||||
assert_serialization(TaskModelType::Hunyuan3d2_1, "hunyuan_3d_2.1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_str() {
|
||||
// Image models
|
||||
assert_eq!(TaskModelType::Flux1Dev.to_str(), "flux_1_dev");
|
||||
assert_eq!(TaskModelType::Flux1Schnell.to_str(), "flux_1_schnell");
|
||||
assert_eq!(TaskModelType::FluxPro11.to_str(), "flux_pro_1.1");
|
||||
assert_eq!(TaskModelType::FluxPro11Ultra.to_str(), "flux_pro_1.1_ultra");
|
||||
assert_eq!(TaskModelType::GptImage1.to_str(), "gpt_image_1");
|
||||
assert_eq!(TaskModelType::Recraft3.to_str(), "recraft_3");
|
||||
// Video models
|
||||
assert_eq!(TaskModelType::Kling16Pro.to_str(), "kling_1.6_pro");
|
||||
assert_eq!(TaskModelType::Kling21Pro.to_str(), "kling_2.1_pro");
|
||||
assert_eq!(TaskModelType::Kling21Master.to_str(), "kling_2.1_master");
|
||||
assert_eq!(TaskModelType::Seedance10Lite.to_str(), "seedance_1.0_lite");
|
||||
assert_eq!(TaskModelType::Veo2.to_str(), "veo_2");
|
||||
// 3D Object generation models
|
||||
assert_eq!(TaskModelType::Hunyuan3d2_0.to_str(), "hunyuan_3d_2.0");
|
||||
assert_eq!(TaskModelType::Hunyuan3d2_1.to_str(), "hunyuan_3d_2.1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str() {
|
||||
// Image models
|
||||
assert_eq!(TaskModelType::from_str("flux_1_dev").unwrap(), TaskModelType::Flux1Dev);
|
||||
assert_eq!(TaskModelType::from_str("flux_1_schnell").unwrap(), TaskModelType::Flux1Schnell);
|
||||
assert_eq!(TaskModelType::from_str("flux_pro_1.1").unwrap(), TaskModelType::FluxPro11);
|
||||
assert_eq!(TaskModelType::from_str("flux_pro_1.1_ultra").unwrap(), TaskModelType::FluxPro11Ultra);
|
||||
assert_eq!(TaskModelType::from_str("gpt_image_1").unwrap(), TaskModelType::GptImage1);
|
||||
assert_eq!(TaskModelType::from_str("recraft_3").unwrap(), TaskModelType::Recraft3);
|
||||
// Video models
|
||||
assert_eq!(TaskModelType::from_str("kling_1.6_pro").unwrap(), TaskModelType::Kling16Pro);
|
||||
assert_eq!(TaskModelType::from_str("kling_2.1_pro").unwrap(), TaskModelType::Kling21Pro);
|
||||
assert_eq!(TaskModelType::from_str("kling_2.1_master").unwrap(), TaskModelType::Kling21Master);
|
||||
assert_eq!(TaskModelType::from_str("seedance_1.0_lite").unwrap(), TaskModelType::Seedance10Lite);
|
||||
assert_eq!(TaskModelType::from_str("veo_2").unwrap(), TaskModelType::Veo2);
|
||||
// 3D Object generation models
|
||||
assert_eq!(TaskModelType::from_str("hunyuan_3d_2.0").unwrap(), TaskModelType::Hunyuan3d2_0);
|
||||
assert_eq!(TaskModelType::from_str("hunyuan_3d_2.1").unwrap(), TaskModelType::Hunyuan3d2_1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
let mut variants = TaskModelType::all_variants();
|
||||
assert_eq!(variants.len(), 13);
|
||||
// Image models
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::Flux1Dev));
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::Flux1Schnell));
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::FluxPro11));
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::FluxPro11Ultra));
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::GptImage1));
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::Recraft3));
|
||||
// Video models
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::Kling16Pro));
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::Kling21Pro));
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::Kling21Master));
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::Seedance10Lite));
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::Veo2));
|
||||
// 3D Object generation models
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::Hunyuan3d2_0));
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::Hunyuan3d2_1));
|
||||
assert_eq!(variants.pop_first(), None);
|
||||
}
|
||||
}
|
||||
|
||||
mod mechanical_checks {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn variant_length() {
|
||||
use strum::IntoEnumIterator;
|
||||
assert_eq!(TaskModelType::all_variants().len(), TaskModelType::iter().len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip() {
|
||||
for variant in TaskModelType::all_variants() {
|
||||
// Test to_str(), from_str(), Display, and Debug.
|
||||
assert_eq!(variant, TaskModelType::from_str(variant.to_str()).unwrap());
|
||||
assert_eq!(variant, TaskModelType::from_str(&format!("{}", variant)).unwrap());
|
||||
assert_eq!(variant, TaskModelType::from_str(&format!("{:?}", variant)).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialized_length_ok_for_database() {
|
||||
const MAX_LENGTH : usize = 24;
|
||||
for variant in TaskModelType::all_variants() {
|
||||
let serialized = variant.to_str();
|
||||
assert!(serialized.len() > 0, "variant {:?} is too short", variant);
|
||||
assert!(serialized.len() <= MAX_LENGTH, "variant {:?} is too long", variant);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
use utoipa::OpenApi;
|
||||
|
||||
use artcraft_api_defs::common::responses::job_details::JobDetailsLipsyncRequest;
|
||||
use artcraft_api_defs::common::responses::job_details::JobDetailsLivePortraitRequest;
|
||||
use artcraft_api_defs::common::responses::media_links::*;
|
||||
use artcraft_api_defs::generate::image::edit::gpt_image_1_edit_image::*;
|
||||
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageAspectRatio;
|
||||
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageNumImages;
|
||||
@@ -43,50 +46,15 @@ use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateV
|
||||
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2Duration;
|
||||
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2ImageToVideoRequest;
|
||||
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2ImageToVideoResponse;
|
||||
use artcraft_api_defs::jobs::list_session_jobs::*;
|
||||
use billing_component::stripe::http_endpoints::checkout::create::stripe_create_checkout_session_error::CreateCheckoutSessionError;
|
||||
use billing_component::stripe::http_endpoints::checkout::create::stripe_create_checkout_session_json_handler::*;
|
||||
use billing_component::users::http_endpoints::list_active_user_subscriptions_handler::*;
|
||||
use enums::by_table::beta_keys::beta_key_product::BetaKeyProduct;
|
||||
use enums::by_table::comments::comment_entity_type::CommentEntityType;
|
||||
use enums::by_table::featured_items::featured_item_entity_type::FeaturedItemEntityType;
|
||||
use enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
|
||||
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
|
||||
use enums::by_table::media_files::media_file_animation_type::MediaFileAnimationType;
|
||||
use enums::by_table::media_files::media_file_class::MediaFileClass;
|
||||
use enums::by_table::media_files::media_file_engine_category::MediaFileEngineCategory;
|
||||
use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory;
|
||||
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
|
||||
use enums::by_table::media_files::media_file_subtype::MediaFileSubtype;
|
||||
use enums::by_table::media_files::media_file_type::MediaFileType;
|
||||
use enums::by_table::model_weights::{weights_category::WeightsCategory, weights_types::WeightsType};
|
||||
use enums::by_table::prompts::prompt_type::PromptType;
|
||||
use enums::by_table::user_bookmarks::user_bookmark_entity_type::UserBookmarkEntityType;
|
||||
use enums::by_table::user_ratings::entity_type::UserRatingEntityType;
|
||||
use enums::by_table::user_ratings::rating_value::UserRatingValue;
|
||||
use enums::by_table::users::user_feature_flag::UserFeatureFlag;
|
||||
use enums::common::job_status_plus::JobStatusPlus;
|
||||
use enums::common::visibility::Visibility;
|
||||
use enums::no_table::style_transfer::style_transfer_name::StyleTransferName;
|
||||
use enums_public::by_table::media_files::public_media_file_model_type::PublicMediaFileModelType;
|
||||
use enums_public::by_table::model_weights::public_weights_types::PublicWeightsType;
|
||||
use tokens::tokens::batch_generations::*;
|
||||
use tokens::tokens::beta_keys::*;
|
||||
use tokens::tokens::browser_session_logs::*;
|
||||
use tokens::tokens::comments::*;
|
||||
use tokens::tokens::generic_inference_jobs::*;
|
||||
use tokens::tokens::media_files::*;
|
||||
use tokens::tokens::model_weights::*;
|
||||
use tokens::tokens::prompts::*;
|
||||
use tokens::tokens::user_bookmarks::*;
|
||||
use tokens::tokens::users::*;
|
||||
use tokens::tokens::zs_voice_datasets::*;
|
||||
|
||||
use crate::http_server::common_requests::auto_product_category::AutoProductCategory;
|
||||
use crate::http_server::common_requests::media_file_token_path_info::MediaFileTokenPathInfo;
|
||||
use crate::http_server::common_responses::media::cover_image_links::CoverImageLinks;
|
||||
use crate::http_server::common_responses::media::media_file_cover_image_details::MediaFileCoverImageDetails;
|
||||
use crate::http_server::common_responses::media::media_file_cover_image_details::MediaFileDefaultCover;
|
||||
use crate::http_server::common_responses::media::media_links::*;
|
||||
use crate::http_server::common_responses::media::weights_cover_image_details::*;
|
||||
use crate::http_server::common_responses::media_file_origin_details::*;
|
||||
use crate::http_server::common_responses::media_file_social_meta_lite::MediaFileSocialMetaLight;
|
||||
@@ -128,7 +96,40 @@ use crate::http_server::endpoints::featured_items::delete_featured_item_handler:
|
||||
use crate::http_server::endpoints::featured_items::get_is_featured_item_handler::*;
|
||||
use crate::http_server::endpoints::image_studio::prompt::enqueue_studio_image_generation_handler::*;
|
||||
use crate::http_server::endpoints::image_studio::upload::upload_snapshot_media_file_handler::*;
|
||||
use crate::http_server::endpoints::inference_job::common_responses::live_portrait::JobDetailsLivePortraitRequest;
|
||||
use enums::by_table::beta_keys::beta_key_product::BetaKeyProduct;
|
||||
use enums::by_table::comments::comment_entity_type::CommentEntityType;
|
||||
use enums::by_table::featured_items::featured_item_entity_type::FeaturedItemEntityType;
|
||||
use enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
|
||||
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
|
||||
use enums::by_table::media_files::media_file_animation_type::MediaFileAnimationType;
|
||||
use enums::by_table::media_files::media_file_class::MediaFileClass;
|
||||
use enums::by_table::media_files::media_file_engine_category::MediaFileEngineCategory;
|
||||
use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory;
|
||||
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
|
||||
use enums::by_table::media_files::media_file_subtype::MediaFileSubtype;
|
||||
use enums::by_table::media_files::media_file_type::MediaFileType;
|
||||
use enums::by_table::model_weights::{weights_category::WeightsCategory, weights_types::WeightsType};
|
||||
use enums::by_table::prompts::prompt_type::PromptType;
|
||||
use enums::by_table::users::user_feature_flag::UserFeatureFlag;
|
||||
use enums::by_table::user_bookmarks::user_bookmark_entity_type::UserBookmarkEntityType;
|
||||
use enums::by_table::user_ratings::entity_type::UserRatingEntityType;
|
||||
use enums::by_table::user_ratings::rating_value::UserRatingValue;
|
||||
use enums::common::job_status_plus::JobStatusPlus;
|
||||
use enums::common::visibility::Visibility;
|
||||
use enums::no_table::style_transfer::style_transfer_name::StyleTransferName;
|
||||
use enums_public::by_table::media_files::public_media_file_model_type::PublicMediaFileModelType;
|
||||
use enums_public::by_table::model_weights::public_weights_types::PublicWeightsType;
|
||||
use tokens::tokens::batch_generations::*;
|
||||
use tokens::tokens::beta_keys::*;
|
||||
use tokens::tokens::browser_session_logs::*;
|
||||
use tokens::tokens::comments::*;
|
||||
use tokens::tokens::generic_inference_jobs::*;
|
||||
use tokens::tokens::media_files::*;
|
||||
use tokens::tokens::model_weights::*;
|
||||
use tokens::tokens::prompts::*;
|
||||
use tokens::tokens::users::*;
|
||||
use tokens::tokens::user_bookmarks::*;
|
||||
use tokens::tokens::zs_voice_datasets::*;
|
||||
use crate::http_server::endpoints::inference_job::delete::dismiss_finished_session_jobs_handler::*;
|
||||
use crate::http_server::endpoints::inference_job::delete::terminate_inference_job_handler::*;
|
||||
use crate::http_server::endpoints::inference_job::get::batch_get_inference_job_status_handler::*;
|
||||
@@ -363,6 +364,7 @@ use crate::http_server::web_utils::response_success_helpers::*;
|
||||
|
||||
// Common response structs
|
||||
JobDetailsLivePortraitRequest,
|
||||
JobDetailsLipsyncRequest,
|
||||
MediaFileLivePortraitDetails,
|
||||
MediaFileModelDetails,
|
||||
MediaFileOriginDetails,
|
||||
|
||||
+30
-55
@@ -1,48 +1,21 @@
|
||||
use url::Url;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::http_server::common_responses::media::cdn_link::{get_cdn_host, new_cdn_url};
|
||||
use crate::http_server::common_responses::media::media_domain::MediaDomain;
|
||||
use artcraft_api_defs::common::responses::media_links::{MediaLinks, VideoPreviews};
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use server_environment::ServerEnvironment;
|
||||
use url::Url;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
// TODO(bt,2024-09-05): Worth reducing the quality at all?
|
||||
const QUALITY : u8 = 95;
|
||||
|
||||
|
||||
/// Links to media file locations (bucket, CDN, etc.)
|
||||
#[derive(Serialize, ToSchema, Debug, Clone, Eq, PartialEq)]
|
||||
pub struct MediaLinks {
|
||||
/// Primary link to the asset via the CDN.
|
||||
pub cdn_url: Url,
|
||||
///
|
||||
/// This migrates the constructors to the `artcraft_api_defs` crate
|
||||
pub struct MediaLinksBuilder {}
|
||||
|
||||
/// Template to construct thumbnail URLs.
|
||||
/// Replace the string `{WIDTH}` with the desired width.
|
||||
/// Only relevant for image media files. (Video media files instead have
|
||||
/// video previews, which, in turn, have their own thumbnail templates.)
|
||||
pub maybe_thumbnail_template: Option<String>,
|
||||
|
||||
/// Video preview images (still and animated gif) for mp4 video files.
|
||||
/// These are only set for video media files.
|
||||
/// These are not set for "cover images" that are gif or webms, but rather video files.
|
||||
pub maybe_video_previews: Option<VideoPreviews>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema, Debug, Clone, Eq, PartialEq)]
|
||||
pub struct VideoPreviews {
|
||||
/// A static single frame preview image of the video.
|
||||
pub still: Url,
|
||||
/// An animated gif preview of the video.
|
||||
pub animated: Url,
|
||||
/// A template used to construct the still thumbnail URL.
|
||||
/// Replace the string `{WIDTH}` with the desired width.
|
||||
pub still_thumbnail_template: String,
|
||||
/// A template used to construct the animated thumbnail URL.
|
||||
/// Replace the string `{WIDTH}` with the desired width.
|
||||
pub animated_thumbnail_template: String,
|
||||
}
|
||||
|
||||
impl MediaLinks {
|
||||
impl MediaLinksBuilder {
|
||||
#[deprecated(note = "we need to start passing the domain and the server environment to generate the CDN")]
|
||||
pub fn from_media_path(
|
||||
domain: MediaDomain,
|
||||
@@ -78,20 +51,22 @@ impl MediaLinks {
|
||||
MediaLinks {
|
||||
cdn_url,
|
||||
maybe_thumbnail_template: thumbnail_template(domain, server_environment, rooted_path),
|
||||
maybe_video_previews: VideoPreviews::from_rooted_path(domain, rooted_path),
|
||||
maybe_video_previews: VideoPreviewsBuilder::from_rooted_path(domain, rooted_path),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VideoPreviews {
|
||||
pub struct VideoPreviewsBuilder {}
|
||||
|
||||
impl VideoPreviewsBuilder {
|
||||
fn from_rooted_path(
|
||||
domain: MediaDomain,
|
||||
rooted_path: &str,
|
||||
) -> Option<Self> {
|
||||
) -> Option<VideoPreviews> {
|
||||
if !rooted_path.ends_with(".mp4") {
|
||||
return None;
|
||||
}
|
||||
Some(Self {
|
||||
Some(VideoPreviews {
|
||||
still: video_preview(domain, rooted_path, PreviewType::Jpg),
|
||||
animated: video_preview(domain, rooted_path, PreviewType::Gif),
|
||||
still_thumbnail_template: video_preview_thumbnail_template(domain, rooted_path, PreviewType::Jpg),
|
||||
@@ -150,7 +125,7 @@ mod tests {
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
|
||||
use crate::http_server::common_responses::media::media_domain::MediaDomain;
|
||||
use crate::http_server::common_responses::media::media_links::MediaLinks;
|
||||
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
|
||||
|
||||
mod fakeyou {
|
||||
use super::*;
|
||||
@@ -161,10 +136,10 @@ mod tests {
|
||||
// TODO(bt,2025-01-31): Write robust tests for this.
|
||||
#[test]
|
||||
fn spot_check_new_case() {
|
||||
let links = MediaLinks::from_rooted_path_and_env(MediaDomain::FakeYou, ServerEnvironment::Production, "/foo/bar.wav");
|
||||
let links = MediaLinksBuilder::from_rooted_path_and_env(MediaDomain::FakeYou, ServerEnvironment::Production, "/foo/bar.wav");
|
||||
assert_eq!(links.cdn_url.as_str(), "https://cdn-2.fakeyou.com/foo/bar.wav");
|
||||
|
||||
let links = MediaLinks::from_rooted_path_and_env(MediaDomain::Storyteller, ServerEnvironment::Development, "/foo/bar.wav");
|
||||
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;let links = MediaLinksBuilder::from_rooted_path_and_env(MediaDomain::Storyteller, ServerEnvironment::Development, "/foo/bar.wav");
|
||||
assert_eq!(links.cdn_url.as_str(), "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/foo/bar.wav");
|
||||
}
|
||||
|
||||
@@ -173,7 +148,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn wav_file() {
|
||||
let links = MediaLinks::from_rooted_path(DOMAIN, "/foo/bar.wav");
|
||||
let links = MediaLinksBuilder::from_rooted_path(DOMAIN, "/foo/bar.wav");
|
||||
assert_eq!(links.cdn_url.as_str(), "https://cdn-2.fakeyou.com/foo/bar.wav");
|
||||
assert_eq!(links.maybe_thumbnail_template, None);
|
||||
assert_eq!(links.maybe_video_previews, None);
|
||||
@@ -181,7 +156,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn glb_file() {
|
||||
let links = MediaLinks::from_rooted_path(DOMAIN, "/foo/bar.glb");
|
||||
let links = MediaLinksBuilder::from_rooted_path(DOMAIN, "/foo/bar.glb");
|
||||
assert_eq!(links.cdn_url.as_str(), "https://cdn-2.fakeyou.com/foo/bar.glb");
|
||||
assert_eq!(links.maybe_thumbnail_template, None);
|
||||
assert_eq!(links.maybe_video_previews, None);
|
||||
@@ -189,7 +164,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn jpg_image() {
|
||||
let links = MediaLinks::from_rooted_path(DOMAIN, "/foo/bar.jpg");
|
||||
let links = MediaLinksBuilder::from_rooted_path(DOMAIN, "/foo/bar.jpg");
|
||||
assert_eq!(links.cdn_url.as_str(), "https://cdn-2.fakeyou.com/foo/bar.jpg");
|
||||
assert_eq!(links.maybe_thumbnail_template, Some("https://cdn-2.fakeyou.com/cdn-cgi/image/width={WIDTH},quality=95/foo/bar.jpg".to_string()));
|
||||
assert_eq!(links.maybe_video_previews, None);
|
||||
@@ -197,7 +172,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn mp4_video() {
|
||||
let links = MediaLinks::from_rooted_path(DOMAIN, "/foo/bar.mp4");
|
||||
let links = MediaLinksBuilder::from_rooted_path(DOMAIN, "/foo/bar.mp4");
|
||||
assert_eq!(links.cdn_url.as_str(), "https://cdn-2.fakeyou.com/foo/bar.mp4");
|
||||
assert_eq!(links.maybe_thumbnail_template, None);
|
||||
let video_previews = links.maybe_video_previews.expect("should have previews");
|
||||
@@ -215,7 +190,7 @@ mod tests {
|
||||
fn wav_file() {
|
||||
// https://storage.googleapis.com/vocodes-public/media/9/4/a/2/7/94a27nmbd0bqmd10tg0pp3hz45zytf67/fakeyou_94a27nmbd0bqmd10tg0pp3hz45zytf67.wav
|
||||
let media_path = MediaFileBucketPath::from_object_hash("94a27nmbd0bqmd10tg0pp3hz45zytf67", Some("fakeyou_"), Some(".wav"));
|
||||
let links = MediaLinks::from_media_path(DOMAIN, &media_path);
|
||||
let links = MediaLinksBuilder::from_media_path(DOMAIN, &media_path);
|
||||
assert_eq!(links.cdn_url.as_str(), "https://cdn-2.fakeyou.com/media/9/4/a/2/7/94a27nmbd0bqmd10tg0pp3hz45zytf67/fakeyou_94a27nmbd0bqmd10tg0pp3hz45zytf67.wav");
|
||||
assert_eq!(links.maybe_thumbnail_template, None);
|
||||
assert_eq!(links.maybe_video_previews, None);
|
||||
@@ -225,7 +200,7 @@ mod tests {
|
||||
fn png_image() {
|
||||
/// https://storage.googleapis.com/vocodes-public/media/3/7/m/b/3/37mb3gh8fmj85y21thvbv08bzv24atjt/upload_37mb3gh8fmj85y21thvbv08bzv24atjt.png
|
||||
let media_path = MediaFileBucketPath::from_object_hash("37mb3gh8fmj85y21thvbv08bzv24atjt", Some("upload_"), Some(".png"));
|
||||
let links = MediaLinks::from_media_path(DOMAIN, &media_path);
|
||||
let links = MediaLinksBuilder::from_media_path(DOMAIN, &media_path);
|
||||
assert_eq!(links.cdn_url.as_str(), "https://cdn-2.fakeyou.com/media/3/7/m/b/3/37mb3gh8fmj85y21thvbv08bzv24atjt/upload_37mb3gh8fmj85y21thvbv08bzv24atjt.png");
|
||||
assert_eq!(links.maybe_thumbnail_template, Some("https://cdn-2.fakeyou.com/cdn-cgi/image/width={WIDTH},quality=95/media/3/7/m/b/3/37mb3gh8fmj85y21thvbv08bzv24atjt/upload_37mb3gh8fmj85y21thvbv08bzv24atjt.png".to_string()));
|
||||
assert_eq!(links.maybe_video_previews, None);
|
||||
@@ -235,7 +210,7 @@ mod tests {
|
||||
fn mp4_video() {
|
||||
// https://storage.googleapis.com/vocodes-public/media/t/6/c/n/y/t6cnyw4g3e8k7carkk2bvrt6nd3fycjv/storyteller_t6cnyw4g3e8k7carkk2bvrt6nd3fycjv.mp4
|
||||
let media_path = MediaFileBucketPath::from_object_hash("t6cnyw4g3e8k7carkk2bvrt6nd3fycjv", Some("storyteller_"), Some(".mp4"));
|
||||
let links = MediaLinks::from_media_path(DOMAIN, &media_path);
|
||||
let links = MediaLinksBuilder::from_media_path(DOMAIN, &media_path);
|
||||
assert_eq!(links.cdn_url.as_str(), "https://cdn-2.fakeyou.com/media/t/6/c/n/y/t6cnyw4g3e8k7carkk2bvrt6nd3fycjv/storyteller_t6cnyw4g3e8k7carkk2bvrt6nd3fycjv.mp4");
|
||||
assert_eq!(links.maybe_thumbnail_template, None);
|
||||
let video_previews = links.maybe_video_previews.expect("should have previews");
|
||||
@@ -257,7 +232,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn wav_file() {
|
||||
let links = MediaLinks::from_rooted_path(DOMAIN, "/foo/bar.wav");
|
||||
let links = MediaLinksBuilder::from_rooted_path(DOMAIN, "/foo/bar.wav");
|
||||
assert_eq!(links.cdn_url.as_str(), "https://cdn.storyteller.ai/foo/bar.wav");
|
||||
assert_eq!(links.maybe_thumbnail_template, None);
|
||||
assert_eq!(links.maybe_video_previews, None);
|
||||
@@ -265,7 +240,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn glb_file() {
|
||||
let links = MediaLinks::from_rooted_path(DOMAIN, "/foo/bar.glb");
|
||||
let links = MediaLinksBuilder::from_rooted_path(DOMAIN, "/foo/bar.glb");
|
||||
assert_eq!(links.cdn_url.as_str(), "https://cdn.storyteller.ai/foo/bar.glb");
|
||||
assert_eq!(links.maybe_thumbnail_template, None);
|
||||
assert_eq!(links.maybe_video_previews, None);
|
||||
@@ -273,7 +248,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn jpg_image() {
|
||||
let links = MediaLinks::from_rooted_path(DOMAIN, "/foo/bar.jpg");
|
||||
let links = MediaLinksBuilder::from_rooted_path(DOMAIN, "/foo/bar.jpg");
|
||||
assert_eq!(links.cdn_url.as_str(), "https://cdn.storyteller.ai/foo/bar.jpg");
|
||||
assert_eq!(links.maybe_thumbnail_template, Some("https://cdn.storyteller.ai/cdn-cgi/image/width={WIDTH},quality=95/foo/bar.jpg".to_string()));
|
||||
assert_eq!(links.maybe_video_previews, None);
|
||||
@@ -281,7 +256,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn mp4_video() {
|
||||
let links = MediaLinks::from_rooted_path(DOMAIN, "/foo/bar.mp4");
|
||||
let links = MediaLinksBuilder::from_rooted_path(DOMAIN, "/foo/bar.mp4");
|
||||
assert_eq!(links.cdn_url.as_str(), "https://cdn.storyteller.ai/foo/bar.mp4");
|
||||
assert_eq!(links.maybe_thumbnail_template, None);
|
||||
let video_previews = links.maybe_video_previews.expect("should have previews");
|
||||
@@ -299,7 +274,7 @@ mod tests {
|
||||
fn wav_file() {
|
||||
// https://storage.googleapis.com/vocodes-public/media/9/4/a/2/7/94a27nmbd0bqmd10tg0pp3hz45zytf67/fakeyou_94a27nmbd0bqmd10tg0pp3hz45zytf67.wav
|
||||
let media_path = MediaFileBucketPath::from_object_hash("94a27nmbd0bqmd10tg0pp3hz45zytf67", Some("fakeyou_"), Some(".wav"));
|
||||
let links = MediaLinks::from_media_path(DOMAIN, &media_path);
|
||||
let links = MediaLinksBuilder::from_media_path(DOMAIN, &media_path);
|
||||
assert_eq!(links.cdn_url.as_str(), "https://cdn.storyteller.ai/media/9/4/a/2/7/94a27nmbd0bqmd10tg0pp3hz45zytf67/fakeyou_94a27nmbd0bqmd10tg0pp3hz45zytf67.wav");
|
||||
assert_eq!(links.maybe_thumbnail_template, None);
|
||||
assert_eq!(links.maybe_video_previews, None);
|
||||
@@ -309,7 +284,7 @@ mod tests {
|
||||
fn png_image() {
|
||||
/// https://storage.googleapis.com/vocodes-public/media/3/7/m/b/3/37mb3gh8fmj85y21thvbv08bzv24atjt/upload_37mb3gh8fmj85y21thvbv08bzv24atjt.png
|
||||
let media_path = MediaFileBucketPath::from_object_hash("37mb3gh8fmj85y21thvbv08bzv24atjt", Some("upload_"), Some(".png"));
|
||||
let links = MediaLinks::from_media_path(DOMAIN, &media_path);
|
||||
let links = MediaLinksBuilder::from_media_path(DOMAIN, &media_path);
|
||||
assert_eq!(links.cdn_url.as_str(), "https://cdn.storyteller.ai/media/3/7/m/b/3/37mb3gh8fmj85y21thvbv08bzv24atjt/upload_37mb3gh8fmj85y21thvbv08bzv24atjt.png");
|
||||
assert_eq!(links.maybe_thumbnail_template, Some("https://cdn.storyteller.ai/cdn-cgi/image/width={WIDTH},quality=95/media/3/7/m/b/3/37mb3gh8fmj85y21thvbv08bzv24atjt/upload_37mb3gh8fmj85y21thvbv08bzv24atjt.png".to_string()));
|
||||
assert_eq!(links.maybe_video_previews, None);
|
||||
@@ -319,7 +294,7 @@ mod tests {
|
||||
fn mp4_video() {
|
||||
// https://storage.googleapis.com/vocodes-public/media/t/6/c/n/y/t6cnyw4g3e8k7carkk2bvrt6nd3fycjv/storyteller_t6cnyw4g3e8k7carkk2bvrt6nd3fycjv.mp4
|
||||
let media_path = MediaFileBucketPath::from_object_hash("t6cnyw4g3e8k7carkk2bvrt6nd3fycjv", Some("storyteller_"), Some(".mp4"));
|
||||
let links = MediaLinks::from_media_path(DOMAIN, &media_path);
|
||||
let links = MediaLinksBuilder::from_media_path(DOMAIN, &media_path);
|
||||
assert_eq!(links.cdn_url.as_str(), "https://cdn.storyteller.ai/media/t/6/c/n/y/t6cnyw4g3e8k7carkk2bvrt6nd3fycjv/storyteller_t6cnyw4g3e8k7carkk2bvrt6nd3fycjv.mp4");
|
||||
assert_eq!(links.maybe_thumbnail_template, None);
|
||||
let video_previews = links.maybe_video_previews.expect("should have previews");
|
||||
@@ -2,5 +2,5 @@ pub mod cdn_link;
|
||||
pub mod cover_image_links;
|
||||
pub mod media_domain;
|
||||
pub mod media_file_cover_image_details;
|
||||
pub mod media_links;
|
||||
pub mod media_links_builder;
|
||||
pub mod weights_cover_image_details;
|
||||
+5
-5
@@ -1,11 +1,14 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
|
||||
use crate::state::server_state::ServerState;
|
||||
use actix_web::web::Json;
|
||||
use actix_web::{web, HttpRequest};
|
||||
use artcraft_api_defs::generate::image::edit::gpt_image_1_edit_image::{GptImage1EditImageImageQuality, GptImage1EditImageImageSize, GptImage1EditImageNumImages, GptImage1EditImageRequest, GptImage1EditImageResponse};
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use enums::common::visibility::Visibility;
|
||||
use fal_client::creds::open_ai_api_key::OpenAiApiKey;
|
||||
use fal_client::requests::webhook::image::enqueue_gpt_image_1_edit_image_webhook::enqueue_gpt_image_1_edit_image_webhook;
|
||||
@@ -17,11 +20,8 @@ use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job
|
||||
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
|
||||
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs;
|
||||
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
|
||||
use utoipa::ToSchema;
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use mysql_queries::queries::media_files::get::batch_get_media_files_by_tokens::batch_get_media_files_by_tokens;
|
||||
use crate::http_server::common_responses::media::media_links::MediaLinks;
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Gpt Image 1 Image Editing
|
||||
#[utoipa::path(
|
||||
@@ -107,7 +107,7 @@ pub async fn gpt_image_1_edit_image_handler(
|
||||
file.maybe_public_bucket_prefix.as_deref(),
|
||||
file.maybe_public_bucket_extension.as_deref());
|
||||
|
||||
let media_links = MediaLinks::from_media_path_and_env(
|
||||
let media_links = MediaLinksBuilder::from_media_path_and_env(
|
||||
media_domain,
|
||||
server_state.server_environment,
|
||||
&public_bucket_path);
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::common_responses::media::media_links::MediaLinks;
|
||||
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
|
||||
use crate::state::server_state::ServerState;
|
||||
@@ -68,7 +68,7 @@ pub async fn remove_image_background_handler(
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage("No media file token provided".to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
|
||||
}
|
||||
@@ -110,7 +110,7 @@ pub async fn remove_image_background_handler(
|
||||
media_file.maybe_public_bucket_prefix.as_deref(),
|
||||
media_file.maybe_public_bucket_extension.as_deref());
|
||||
|
||||
let media_links = MediaLinks::from_media_path_and_env(
|
||||
let media_links = MediaLinksBuilder::from_media_path_and_env(
|
||||
media_domain,
|
||||
server_state.server_environment,
|
||||
&bucket_path);
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::common_responses::media::media_links::MediaLinks;
|
||||
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
|
||||
use crate::state::server_state::ServerState;
|
||||
@@ -111,7 +111,7 @@ pub async fn generate_hunyuan_2_0_image_to_3d_handler(
|
||||
media_file.maybe_public_bucket_prefix.as_deref(),
|
||||
media_file.maybe_public_bucket_extension.as_deref());
|
||||
|
||||
let media_links = MediaLinks::from_media_path_and_env(
|
||||
let media_links = MediaLinksBuilder::from_media_path_and_env(
|
||||
media_domain,
|
||||
server_state.server_environment,
|
||||
&bucket_path);
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::common_responses::media::media_links::MediaLinks;
|
||||
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
|
||||
use crate::state::server_state::ServerState;
|
||||
@@ -111,7 +111,7 @@ pub async fn generate_hunyuan_2_1_image_to_3d_handler(
|
||||
media_file.maybe_public_bucket_prefix.as_deref(),
|
||||
media_file.maybe_public_bucket_extension.as_deref());
|
||||
|
||||
let media_links = MediaLinks::from_media_path_and_env(
|
||||
let media_links = MediaLinksBuilder::from_media_path_and_env(
|
||||
media_domain,
|
||||
server_state.server_environment,
|
||||
&bucket_path);
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::common_responses::media::media_links::MediaLinks;
|
||||
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
|
||||
use crate::state::server_state::ServerState;
|
||||
@@ -135,7 +135,7 @@ pub async fn generate_kling_1_6_pro_video_handler(
|
||||
media_file.maybe_public_bucket_prefix.as_deref(),
|
||||
media_file.maybe_public_bucket_extension.as_deref());
|
||||
|
||||
let media_links = MediaLinks::from_media_path_and_env(
|
||||
let media_links = MediaLinksBuilder::from_media_path_and_env(
|
||||
media_domain,
|
||||
server_state.server_environment,
|
||||
&bucket_path);
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::common_responses::media::media_links::MediaLinks;
|
||||
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
|
||||
use crate::state::server_state::ServerState;
|
||||
@@ -136,7 +136,7 @@ pub async fn generate_kling_2_1_master_video_handler(
|
||||
media_file.maybe_public_bucket_prefix.as_deref(),
|
||||
media_file.maybe_public_bucket_extension.as_deref());
|
||||
|
||||
let media_links = MediaLinks::from_media_path_and_env(
|
||||
let media_links = MediaLinksBuilder::from_media_path_and_env(
|
||||
media_domain,
|
||||
server_state.server_environment,
|
||||
&bucket_path);
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::common_responses::media::media_links::MediaLinks;
|
||||
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
|
||||
use crate::state::server_state::ServerState;
|
||||
@@ -136,7 +136,7 @@ pub async fn generate_kling_2_1_pro_video_handler(
|
||||
media_file.maybe_public_bucket_prefix.as_deref(),
|
||||
media_file.maybe_public_bucket_extension.as_deref());
|
||||
|
||||
let media_links = MediaLinks::from_media_path_and_env(
|
||||
let media_links = MediaLinksBuilder::from_media_path_and_env(
|
||||
media_domain,
|
||||
server_state.server_environment,
|
||||
&bucket_path);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user