diff --git a/Cargo.lock b/Cargo.lock index 549c68404e..9970c46e23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", ] diff --git a/crates/schema/public/api/artcraft_api_defs/Cargo.toml b/crates/schema/public/api/artcraft_api_defs/Cargo.toml index d764640d37..7ae6985d77 100644 --- a/crates/schema/public/api/artcraft_api_defs/Cargo.toml +++ b/crates/schema/public/api/artcraft_api_defs/Cargo.toml @@ -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"] } diff --git a/crates/schema/public/api/artcraft_api_defs/src/common/mod.rs b/crates/schema/public/api/artcraft_api_defs/src/common/mod.rs new file mode 100644 index 0000000000..6885733dcc --- /dev/null +++ b/crates/schema/public/api/artcraft_api_defs/src/common/mod.rs @@ -0,0 +1 @@ +pub mod responses; \ No newline at end of file diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/inference_job/common_responses/live_portrait.rs b/crates/schema/public/api/artcraft_api_defs/src/common/responses/job_details.rs similarity index 62% rename from crates/service/web/storyteller_web/src/http_server/endpoints/inference_job/common_responses/live_portrait.rs rename to crates/schema/public/api/artcraft_api_defs/src/common/responses/job_details.rs index c25f92d20b..d6d572043b 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/inference_job/common_responses/live_portrait.rs +++ b/crates/schema/public/api/artcraft_api_defs/src/common/responses/job_details.rs @@ -1,6 +1,6 @@ -use utoipa::ToSchema; - +use serde::Serialize; use tokens::tokens::media_files::MediaFileToken; +use utoipa::ToSchema; /// Details about submitted live portrait jobs (request arguments only) #[derive(Serialize, ToSchema)] @@ -18,3 +18,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, 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, +} diff --git a/crates/schema/public/api/artcraft_api_defs/src/common/responses/media_links.rs b/crates/schema/public/api/artcraft_api_defs/src/common/responses/media_links.rs new file mode 100644 index 0000000000..a8868a65e1 --- /dev/null +++ b/crates/schema/public/api/artcraft_api_defs/src/common/responses/media_links.rs @@ -0,0 +1,38 @@ +use serde::Serialize; +use utoipa::ToSchema; +use url::Url; + +/// 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, + + /// 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, + + /// 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, +} + +#[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, +} diff --git a/crates/schema/public/api/artcraft_api_defs/src/common/responses/mod.rs b/crates/schema/public/api/artcraft_api_defs/src/common/responses/mod.rs new file mode 100644 index 0000000000..2f255d45f6 --- /dev/null +++ b/crates/schema/public/api/artcraft_api_defs/src/common/responses/mod.rs @@ -0,0 +1,2 @@ +pub mod job_details; +pub mod media_links; \ No newline at end of file diff --git a/crates/schema/public/api/artcraft_api_defs/src/jobs/list_session_jobs.rs b/crates/schema/public/api/artcraft_api_defs/src/jobs/list_session_jobs.rs new file mode 100644 index 0000000000..f4b85fcc38 --- /dev/null +++ b/crates/schema/public/api/artcraft_api_defs/src/jobs/list_session_jobs.rs @@ -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, + //pub page_size: Option, // TODO(bt,2024-04-23): One page for now. + //pub page_index: Option, // 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, + + /// The opposite of include_states, this filters out states from view. + pub exclude_states: Option, + + // TODO(bt,2024-04-23): Add the ability for users to dismiss completed/dead jobs from view. +} +#[derive(Serialize, ToSchema)] +pub struct ListSessionJobsSuccessResponse { + pub success: bool, + + /// This is not paginated and is limited to showing 100 jobs. + pub jobs: Vec, +} + +#[derive(Serialize, ToSchema)] +pub struct ListSessionJobsItem { + pub job_token: InferenceJobToken, + + pub request: ListSessionRequestDetailsResponse, + pub status: ListSessionStatusDetailsResponse, + pub maybe_result: Option, + + pub created_at: DateTime, + pub updated_at: DateTime, +} + +/// Details about what the user requested for generation +#[derive(Serialize, ToSchema)] +pub struct ListSessionRequestDetailsResponse { + pub inference_category: InferenceCategory, + pub maybe_model_type: Option, + pub maybe_model_token: Option, + + /// OPTIONAL. Title of the model, if it has one + pub maybe_model_title: Option, + + /// OPTIONAL. If the result was TTS, this is the raw inference text. + pub maybe_raw_inference_text: Option, + + /// OPTIONAL. For Comfy / Video Style Transfer jobs, this might include + /// the name of the selected style. + pub maybe_style_name: Option, + + /// OPTIONAL. For Live Portrait jobs, this is additional information on the request. + pub maybe_live_portrait_details: Option, + + /// OPTIONAL. For lipsync jobs (face fusion and sad talker), this is additional + /// information on the request. + pub maybe_lipsync_details: Option, +} + +/// Details about the ongoing job status +#[derive(Serialize, 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, + + pub maybe_assigned_worker: Option, + pub maybe_assigned_cluster: Option, + + pub maybe_first_started_at: Option>, + + /// 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, + + 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, + + /// 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, 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, + + /// Rich CDN links to the media, including thumbnails, previews, and more. + pub media_links: MediaLinks, + + pub maybe_successfully_completed_at: Option>, +} + diff --git a/crates/schema/public/api/artcraft_api_defs/src/jobs/mod.rs b/crates/schema/public/api/artcraft_api_defs/src/jobs/mod.rs new file mode 100644 index 0000000000..164271aad7 --- /dev/null +++ b/crates/schema/public/api/artcraft_api_defs/src/jobs/mod.rs @@ -0,0 +1 @@ +pub mod list_session_jobs; \ No newline at end of file diff --git a/crates/schema/public/api/artcraft_api_defs/src/lib.rs b/crates/schema/public/api/artcraft_api_defs/src/lib.rs index 118c66d72d..72d4e23890 100644 --- a/crates/schema/public/api/artcraft_api_defs/src/lib.rs +++ b/crates/schema/public/api/artcraft_api_defs/src/lib.rs @@ -1 +1,3 @@ -pub mod generate; \ No newline at end of file +pub mod common; +pub mod generate; +pub mod jobs; \ No newline at end of file diff --git a/crates/service/web/storyteller_web/src/docs/api_doc.rs b/crates/service/web/storyteller_web/src/docs/api_doc.rs index d5e5363123..f33a66368d 100644 --- a/crates/service/web/storyteller_web/src/docs/api_doc.rs +++ b/crates/service/web/storyteller_web/src/docs/api_doc.rs @@ -1,5 +1,7 @@ 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::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,44 +45,10 @@ 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; @@ -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, diff --git a/crates/service/web/storyteller_web/src/http_server/common_responses/media/media_links.rs b/crates/service/web/storyteller_web/src/http_server/common_responses/media/media_links.rs index 5d35b28228..e8adbbb2fa 100644 --- a/crates/service/web/storyteller_web/src/http_server/common_responses/media/media_links.rs +++ b/crates/service/web/storyteller_web/src/http_server/common_responses/media/media_links.rs @@ -9,6 +9,8 @@ use server_environment::ServerEnvironment; // TODO(bt,2024-09-05): Worth reducing the quality at all? const QUALITY : u8 = 95; +/// TODO(bt, 2025-07-14): Migrate to `artcraft_api_deps` crate's `MediaLinks`. +/// Use `MediaLinksBuilder` (in this crate) to assist migration. /// Links to media file locations (bucket, CDN, etc.) #[derive(Serialize, ToSchema, Debug, Clone, Eq, PartialEq)] diff --git a/crates/service/web/storyteller_web/src/http_server/common_responses/media/media_links_builder.rs b/crates/service/web/storyteller_web/src/http_server/common_responses/media/media_links_builder.rs new file mode 100644 index 0000000000..211dffa92d --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/common_responses/media/media_links_builder.rs @@ -0,0 +1,308 @@ +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.) +/// +/// This migrates the constructors to the `artcraft_api_defs` crate +pub struct MediaLinksBuilder {} + +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, + bucket_path: &MediaFileBucketPath, + ) -> MediaLinks { + Self::from_media_path_and_env(domain, ServerEnvironment::Production, bucket_path) + } + + #[deprecated(note = "we need to start passing the domain and the server environment to generate the CDN")] + pub fn from_rooted_path( + domain: MediaDomain, + rooted_path: &str, + ) -> MediaLinks { + Self::from_rooted_path_and_env(domain, ServerEnvironment::Production, rooted_path) + } + + pub fn from_media_path_and_env( + domain: MediaDomain, + server_environment: ServerEnvironment, + bucket_path: &MediaFileBucketPath, + ) -> MediaLinks { + let rooted_path = bucket_path.get_full_object_path_str(); + Self::from_rooted_path_and_env(domain, server_environment, rooted_path) + } + + pub fn from_rooted_path_and_env( + domain: MediaDomain, + server_environment: ServerEnvironment, + rooted_path: &str, + ) -> MediaLinks { + let mut cdn_url = new_cdn_url(domain, server_environment); + cdn_url.set_path(rooted_path); + MediaLinks { + cdn_url, + maybe_thumbnail_template: thumbnail_template(domain, server_environment, rooted_path), + maybe_video_previews: VideoPreviewsBuilder::from_rooted_path(domain, rooted_path), + } + } +} + +pub struct VideoPreviewsBuilder {} + +impl VideoPreviewsBuilder { + fn from_rooted_path( + domain: MediaDomain, + rooted_path: &str, + ) -> Option { + if !rooted_path.ends_with(".mp4") { + return None; + } + 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), + animated_thumbnail_template: video_preview_thumbnail_template(domain, rooted_path, PreviewType::Gif), + }) + } +} + +enum PreviewType { + Gif, + Jpg +} + +/// Returns a jpeg or gif preview of the video. +fn video_preview(media_domain: MediaDomain, rooted_path: &str, thumbnail_type: PreviewType) -> Url { + let host = media_domain.cdn_url_str(); + let rooted_path = match thumbnail_type { + PreviewType::Gif => format!("{rooted_path}-thumb.gif"), + PreviewType::Jpg => format!("{rooted_path}-thumb.jpg"), + }; + let mut url = media_domain.new_cdn_url(); + url.set_path(&rooted_path); + url +} + +/// Returns a thumbnail template for image +fn thumbnail_template(media_domain: MediaDomain, server_environment: ServerEnvironment, rooted_path: &str) -> Option { + if !rooted_path.ends_with(".jpg") + && !rooted_path.ends_with(".png") + && !rooted_path.ends_with(".gif") { + return None; + } + + let host = get_cdn_host(media_domain, server_environment); + + // NB(bt,2025-02-01): Development doesn't currently support thumbnails, so serve the full image. + match server_environment { + ServerEnvironment::Development => Some(format!("{host}{rooted_path}")), // NB(bt,2025-02-01): No thumbnails in development. + ServerEnvironment::Production => Some(format!("{host}/cdn-cgi/image/width={{WIDTH}},quality={QUALITY}{rooted_path}")) + } +} + +/// Returns a thumbnail template for video +fn video_preview_thumbnail_template(media_domain: MediaDomain, rooted_path: &str, thumbnail_type: PreviewType) -> String { + let host = media_domain.cdn_url_str(); + let rooted_path = match thumbnail_type { + PreviewType::Gif => format!("{rooted_path}-thumb.gif"), + PreviewType::Jpg => format!("{rooted_path}-thumb.jpg"), + }; + format!("{host}/cdn-cgi/image/width={{WIDTH}},quality={QUALITY}{rooted_path}") +} + + +#[cfg(test)] +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_builder::MediaLinksBuilder; + + mod fakeyou { + use super::*; + use server_environment::ServerEnvironment; + + const DOMAIN : MediaDomain = MediaDomain::FakeYou; + + // TODO(bt,2025-01-31): Write robust tests for this. + #[test] + fn spot_check_new_case() { + 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"); + +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"); + } + + mod rooted_path { + use super::*; + + #[test] + fn wav_file() { + 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); + } + + #[test] + fn glb_file() { + 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); + } + + #[test] + fn jpg_image() { + 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); + } + + #[test] + fn mp4_video() { + 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"); + assert_eq!(video_previews.animated.as_str(), "https://cdn-2.fakeyou.com/foo/bar.mp4-thumb.gif"); + assert_eq!(video_previews.still.as_str(), "https://cdn-2.fakeyou.com/foo/bar.mp4-thumb.jpg"); + assert_eq!(video_previews.animated_thumbnail_template, "https://cdn-2.fakeyou.com/cdn-cgi/image/width={WIDTH},quality=95/foo/bar.mp4-thumb.gif"); + assert_eq!(video_previews.still_thumbnail_template, "https://cdn-2.fakeyou.com/cdn-cgi/image/width={WIDTH},quality=95/foo/bar.mp4-thumb.jpg"); + } + } + + mod media_path { + use super::*; + + #[test] + 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 = 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); + } + + #[test] + 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 = 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); + } + + #[test] + 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 = 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"); + assert_eq!(video_previews.animated.as_str(), "https://cdn-2.fakeyou.com/media/t/6/c/n/y/t6cnyw4g3e8k7carkk2bvrt6nd3fycjv/storyteller_t6cnyw4g3e8k7carkk2bvrt6nd3fycjv.mp4-thumb.gif"); + assert_eq!(video_previews.still.as_str(), "https://cdn-2.fakeyou.com/media/t/6/c/n/y/t6cnyw4g3e8k7carkk2bvrt6nd3fycjv/storyteller_t6cnyw4g3e8k7carkk2bvrt6nd3fycjv.mp4-thumb.jpg"); + assert_eq!(video_previews.animated_thumbnail_template, "https://cdn-2.fakeyou.com/cdn-cgi/image/width={WIDTH},quality=95/media/t/6/c/n/y/t6cnyw4g3e8k7carkk2bvrt6nd3fycjv/storyteller_t6cnyw4g3e8k7carkk2bvrt6nd3fycjv.mp4-thumb.gif"); + assert_eq!(video_previews.still_thumbnail_template, "https://cdn-2.fakeyou.com/cdn-cgi/image/width={WIDTH},quality=95/media/t/6/c/n/y/t6cnyw4g3e8k7carkk2bvrt6nd3fycjv/storyteller_t6cnyw4g3e8k7carkk2bvrt6nd3fycjv.mp4-thumb.jpg"); + } + } + } + + mod storyteller { + use super::*; + + const DOMAIN : MediaDomain = MediaDomain::Storyteller; + + mod rooted_path { + use super::*; + + #[test] + fn wav_file() { + 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); + } + + #[test] + fn glb_file() { + 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); + } + + #[test] + fn jpg_image() { + 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); + } + + #[test] + fn mp4_video() { + 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"); + assert_eq!(video_previews.animated.as_str(), "https://cdn.storyteller.ai/foo/bar.mp4-thumb.gif"); + assert_eq!(video_previews.still.as_str(), "https://cdn.storyteller.ai/foo/bar.mp4-thumb.jpg"); + assert_eq!(video_previews.animated_thumbnail_template, "https://cdn.storyteller.ai/cdn-cgi/image/width={WIDTH},quality=95/foo/bar.mp4-thumb.gif"); + assert_eq!(video_previews.still_thumbnail_template, "https://cdn.storyteller.ai/cdn-cgi/image/width={WIDTH},quality=95/foo/bar.mp4-thumb.jpg"); + } + } + + mod media_path { + use super::*; + + #[test] + 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 = 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); + } + + #[test] + 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 = 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); + } + + #[test] + 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 = 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"); + assert_eq!(video_previews.animated.as_str(), "https://cdn.storyteller.ai/media/t/6/c/n/y/t6cnyw4g3e8k7carkk2bvrt6nd3fycjv/storyteller_t6cnyw4g3e8k7carkk2bvrt6nd3fycjv.mp4-thumb.gif"); + assert_eq!(video_previews.still.as_str(), "https://cdn.storyteller.ai/media/t/6/c/n/y/t6cnyw4g3e8k7carkk2bvrt6nd3fycjv/storyteller_t6cnyw4g3e8k7carkk2bvrt6nd3fycjv.mp4-thumb.jpg"); + assert_eq!(video_previews.animated_thumbnail_template, "https://cdn.storyteller.ai/cdn-cgi/image/width={WIDTH},quality=95/media/t/6/c/n/y/t6cnyw4g3e8k7carkk2bvrt6nd3fycjv/storyteller_t6cnyw4g3e8k7carkk2bvrt6nd3fycjv.mp4-thumb.gif"); + assert_eq!(video_previews.still_thumbnail_template, "https://cdn.storyteller.ai/cdn-cgi/image/width={WIDTH},quality=95/media/t/6/c/n/y/t6cnyw4g3e8k7carkk2bvrt6nd3fycjv/storyteller_t6cnyw4g3e8k7carkk2bvrt6nd3fycjv.mp4-thumb.jpg"); + } + } + } +} diff --git a/crates/service/web/storyteller_web/src/http_server/common_responses/media/mod.rs b/crates/service/web/storyteller_web/src/http_server/common_responses/media/mod.rs index e44d0a7919..87328e244f 100644 --- a/crates/service/web/storyteller_web/src/http_server/common_responses/media/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/common_responses/media/mod.rs @@ -3,4 +3,5 @@ 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; \ No newline at end of file diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/inference_job/common_responses/lipsync.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/inference_job/common_responses/lipsync.rs deleted file mode 100644 index 8dbabd9da3..0000000000 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/inference_job/common_responses/lipsync.rs +++ /dev/null @@ -1,14 +0,0 @@ -use tokens::tokens::media_files::MediaFileToken; -use utoipa::ToSchema; - -/// Details about submitted lipsync jobs (request arguments only) -#[derive(Serialize, 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, -} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/inference_job/common_responses/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/inference_job/common_responses/mod.rs deleted file mode 100644 index 8514adecdf..0000000000 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/inference_job/common_responses/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod lipsync; -pub mod live_portrait; \ No newline at end of file diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/inference_job/get/batch_get_inference_job_status_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/inference_job/get/batch_get_inference_job_status_handler.rs index bc3dae57fc..b3fa0cd2ea 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/inference_job/get/batch_get_inference_job_status_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/inference_job/get/batch_get_inference_job_status_handler.rs @@ -2,20 +2,8 @@ use std::collections::HashSet; use std::fmt; use std::sync::Arc; -use actix_web::error::ResponseError; -use actix_web::http::StatusCode; -use actix_web::web::Json; -use actix_web::{web, HttpRequest, HttpResponse}; -use actix_web_lab::extract::Query; -use chrono::{DateTime, Utc}; -use log::error; -use r2d2_redis::redis::Commands; -use utoipa::{IntoParams, ToSchema}; - use crate::http_server::common_responses::media::media_domain::MediaDomain; use crate::http_server::common_responses::media::media_links::MediaLinks; -use crate::http_server::endpoints::inference_job::common_responses::lipsync::JobDetailsLipsyncRequest; -use crate::http_server::endpoints::inference_job::common_responses::live_portrait::JobDetailsLivePortraitRequest; use crate::http_server::endpoints::inference_job::utils::estimates::estimate_job_progress::estimate_job_progress; use crate::http_server::endpoints::inference_job::utils::extractors::extract_lipsync_details::extract_lipsync_details; use crate::http_server::endpoints::inference_job::utils::extractors::extract_live_portrait_details::extract_live_portrait_details; @@ -24,16 +12,26 @@ use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_m use crate::http_server::web_utils::filter_model_name::maybe_filter_model_name; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::state::server_state::ServerState; +use actix_web::error::ResponseError; +use actix_web::http::StatusCode; +use actix_web::web::Json; +use actix_web::{web, HttpRequest, HttpResponse}; +use actix_web_lab::extract::Query; +use artcraft_api_defs::common::responses::job_details::{JobDetailsLipsyncRequest, JobDetailsLivePortraitRequest}; use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath; use bucket_paths::legacy::typified_paths::public::voice_conversion_results::bucket_file_path::VoiceConversionResultOriginalFilePath; +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 log::error; use mysql_queries::queries::generic_inference::web::batch_get_inference_job_status::batch_get_inference_job_status; use mysql_queries::queries::generic_inference::web::job_status::GenericInferenceJobStatus; +use r2d2_redis::redis::Commands; use redis_common::redis_keys::RedisKeys; use tokens::tokens::generic_inference_jobs::InferenceJobToken; +use utoipa::{IntoParams, ToSchema}; /// For certain jobs or job classes (eg. non-premium), we kill the jobs if the user hasn't /// maintained a keepalive. This prevents wasted work when users who are unlikely to return @@ -41,7 +39,6 @@ use tokens::tokens::generic_inference_jobs::InferenceJobToken; /// typically do not require keepalive. const JOB_KEEPALIVE_TTL_SECONDS : usize = 60 * 3; - #[derive(Deserialize, ToSchema, IntoParams)] pub struct BatchGetInferenceJobStatusQueryParams { /// A grab bag of tokens of various types. diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/inference_job/get/get_inference_job_status_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/inference_job/get/get_inference_job_status_handler.rs index 8bb5ff4a1a..6d58d87b8d 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/inference_job/get/get_inference_job_status_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/inference_job/get/get_inference_job_status_handler.rs @@ -1,19 +1,8 @@ use std::fmt; use std::sync::Arc; -use actix_web::error::ResponseError; -use actix_web::http::StatusCode; -use actix_web::web::{Json, Path}; -use actix_web::{web, HttpRequest, HttpResponse}; -use chrono::{DateTime, Utc}; -use log::error; -use r2d2_redis::redis::{Commands, RedisResult}; -use utoipa::ToSchema; - use crate::http_server::common_responses::media::media_domain::MediaDomain; use crate::http_server::common_responses::media::media_links::MediaLinks; -use crate::http_server::endpoints::inference_job::common_responses::lipsync::JobDetailsLipsyncRequest; -use crate::http_server::endpoints::inference_job::common_responses::live_portrait::JobDetailsLivePortraitRequest; use crate::http_server::endpoints::inference_job::utils::estimates::estimate_job_progress::estimate_job_progress; use crate::http_server::endpoints::inference_job::utils::extractors::extract_lipsync_details::extract_lipsync_details; use crate::http_server::endpoints::inference_job::utils::extractors::extract_live_portrait_details::extract_live_portrait_details; @@ -22,16 +11,25 @@ use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_m use crate::http_server::web_utils::filter_model_name::maybe_filter_model_name; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::state::server_state::ServerState; +use actix_web::error::ResponseError; +use actix_web::http::StatusCode; +use actix_web::web::{Json, Path}; +use actix_web::{web, HttpRequest, HttpResponse}; +use artcraft_api_defs::common::responses::job_details::{JobDetailsLipsyncRequest, JobDetailsLivePortraitRequest}; use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath; use bucket_paths::legacy::typified_paths::public::voice_conversion_results::bucket_file_path::VoiceConversionResultOriginalFilePath; +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 log::error; use mysql_queries::queries::generic_inference::web::get_inference_job_status::get_inference_job_status; use mysql_queries::queries::generic_inference::web::job_status::GenericInferenceJobStatus; +use r2d2_redis::redis::{Commands, RedisResult}; use redis_common::redis_keys::RedisKeys; use tokens::tokens::generic_inference_jobs::InferenceJobToken; +use utoipa::ToSchema; /// For certain jobs or job classes (eg. non-premium), we kill the jobs if the user hasn't /// maintained a keepalive. This prevents wasted work when users who are unlikely to return diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/inference_job/list/list_session_jobs_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/inference_job/list/list_session_jobs_handler.rs index a7ebae365c..029e3d6710 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/inference_job/list/list_session_jobs_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/inference_job/list/list_session_jobs_handler.rs @@ -2,20 +2,9 @@ use std::collections::HashSet; use std::fmt; use std::sync::Arc; -use actix_web::error::ResponseError; -use actix_web::http::StatusCode; -use actix_web::web::Json; -use actix_web::{web, HttpRequest, HttpResponse}; -use actix_web_lab::extract::Query; -use chrono::{DateTime, Utc}; -use log::{error, warn}; -use r2d2_redis::redis::Commands; -use utoipa::{IntoParams, ToSchema}; - use crate::http_server::common_responses::media::media_domain::MediaDomain; use crate::http_server::common_responses::media::media_links::MediaLinks; -use crate::http_server::endpoints::inference_job::common_responses::lipsync::JobDetailsLipsyncRequest; -use crate::http_server::endpoints::inference_job::common_responses::live_portrait::JobDetailsLivePortraitRequest; +use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder; use crate::http_server::endpoints::inference_job::utils::estimates::estimate_job_progress::estimate_job_progress; use crate::http_server::endpoints::inference_job::utils::extractors::extract_lipsync_details::extract_lipsync_details; use crate::http_server::endpoints::inference_job::utils::extractors::extract_live_portrait_details::extract_live_portrait_details; @@ -24,18 +13,28 @@ use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_m use crate::http_server::web_utils::filter_model_name::maybe_filter_model_name; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::state::server_state::ServerState; +use actix_web::error::ResponseError; +use actix_web::http::StatusCode; +use actix_web::web::Json; +use actix_web::{web, HttpRequest, HttpResponse}; +use actix_web_lab::extract::Query; +use artcraft_api_defs::jobs::list_session_jobs::{ListSessionJobsItem, ListSessionJobsQueryParams, ListSessionJobsSuccessResponse, ListSessionRequestDetailsResponse, ListSessionResultDetailsResponse, ListSessionStatusDetailsResponse}; use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath; use bucket_paths::legacy::typified_paths::public::voice_conversion_results::bucket_file_path::VoiceConversionResultOriginalFilePath; +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 log::{error, warn}; use mysql_queries::queries::generic_inference::web::job_status::GenericInferenceJobStatus; use mysql_queries::queries::generic_inference::web::list_session_jobs::{list_session_jobs_from_connection, ListSessionJobsForUserArgs, SessionUser}; use primitives::numerics::i64_to_u64_zero_clamped::i64_to_u64_zero_clamped; +use r2d2_redis::redis::Commands; use redis_common::redis_keys::RedisKeys; use tokens::tokens::generic_inference_jobs::InferenceJobToken; use tokens::tokens::media_files::MediaFileToken; +use utoipa::{IntoParams, ToSchema}; /// For certain jobs or job classes (eg. non-premium), we kill the jobs if the user hasn't /// maintained a keepalive. This prevents wasted work when users who are unlikely to return @@ -43,118 +42,6 @@ use tokens::tokens::media_files::MediaFileToken; /// typically do not require keepalive. const JOB_KEEPALIVE_TTL_SECONDS : usize = 60 * 3; -#[derive(Deserialize, ToSchema, IntoParams)] -pub struct ListSessionJobsQueryParams { - //pub sort_ascending: Option, - //pub page_size: Option, // TODO(bt,2024-04-23): One page for now. - //pub page_index: Option, // 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, - - /// The opposite of include_states, this filters out states from view. - pub exclude_states: Option, - - // TODO(bt,2024-04-23): Add the ability for users to dismiss completed/dead jobs from view. -} - -#[derive(Serialize, ToSchema)] -pub struct ListSessionJobsSuccessResponse { - pub success: bool, - - /// This is not paginated and is limited to showing 100 jobs. - pub jobs: Vec, -} - -#[derive(Serialize, ToSchema)] -pub struct ListSessionJobsItem { - pub job_token: InferenceJobToken, - - pub request: ListSessionRequestDetailsResponse, - pub status: ListSessionStatusDetailsResponse, - pub maybe_result: Option, - - pub created_at: DateTime, - pub updated_at: DateTime, -} - -/// Details about what the user requested for generation -#[derive(Serialize, ToSchema)] -pub struct ListSessionRequestDetailsResponse { - pub inference_category: InferenceCategory, - pub maybe_model_type: Option, - pub maybe_model_token: Option, - - /// OPTIONAL. Title of the model, if it has one - pub maybe_model_title: Option, - - /// OPTIONAL. If the result was TTS, this is the raw inference text. - pub maybe_raw_inference_text: Option, - - /// OPTIONAL. For Comfy / Video Style Transfer jobs, this might include - /// the name of the selected style. - pub maybe_style_name: Option, - - /// OPTIONAL. For Live Portrait jobs, this is additional information on the request. - pub maybe_live_portrait_details: Option, - - /// OPTIONAL. For lipsync jobs (face fusion and sad talker), this is additional - /// information on the request. - pub maybe_lipsync_details: Option, -} - -/// Details about the ongoing job status -#[derive(Serialize, 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, - - pub maybe_assigned_worker: Option, - pub maybe_assigned_cluster: Option, - - pub maybe_first_started_at: Option>, - - /// 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, - - 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, - - /// 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, 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, - - /// Rich CDN links to the media, including thumbnails, previews, and more. - pub media_links: MediaLinks, - - pub maybe_successfully_completed_at: Option>, -} - - #[derive(Debug, ToSchema)] pub enum ListSessionJobsError { ServerError, @@ -472,7 +359,7 @@ fn db_record_to_response_payload( ListSessionResultDetailsResponse { entity_type: result_details.entity_type, entity_token: result_details.entity_token, - media_links: MediaLinks::from_rooted_path(media_domain, &public_bucket_media_path), + media_links: MediaLinksBuilder::from_rooted_path(media_domain, &public_bucket_media_path), maybe_public_bucket_media_path: Some(public_bucket_media_path), maybe_successfully_completed_at: result_details.maybe_successfully_completed_at, } @@ -481,4 +368,3 @@ fn db_record_to_response_payload( updated_at: record.updated_at, } } - diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/inference_job/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/inference_job/mod.rs index 5050010a5b..348f862aa9 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/inference_job/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/inference_job/mod.rs @@ -1,5 +1,4 @@ pub mod admin; -pub mod common_responses; pub mod delete; pub mod get; pub mod list; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/inference_job/utils/extractors/extract_lipsync_details.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/inference_job/utils/extractors/extract_lipsync_details.rs index bd481add66..0a61cf2835 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/inference_job/utils/extractors/extract_lipsync_details.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/inference_job/utils/extractors/extract_lipsync_details.rs @@ -1,4 +1,4 @@ -use crate::http_server::endpoints::inference_job::common_responses::lipsync::JobDetailsLipsyncRequest; +use artcraft_api_defs::common::responses::job_details::JobDetailsLipsyncRequest; use mysql_queries::payloads::generic_inference_args::generic_inference_args::PolymorphicInferenceArgs; use mysql_queries::payloads::generic_inference_args::inner_payloads::face_fusion_payload::FaceFusionPayload; use mysql_queries::payloads::generic_inference_args::inner_payloads::lipsync_payload::{LipsyncAnimationAudioSource, LipsyncAnimationImageSource, LipsyncArgs}; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/inference_job/utils/extractors/extract_live_portrait_details.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/inference_job/utils/extractors/extract_live_portrait_details.rs index 3201151817..b22c90f7e6 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/inference_job/utils/extractors/extract_live_portrait_details.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/inference_job/utils/extractors/extract_live_portrait_details.rs @@ -1,7 +1,6 @@ +use artcraft_api_defs::common::responses::job_details::JobDetailsLivePortraitRequest; use mysql_queries::payloads::generic_inference_args::generic_inference_args::PolymorphicInferenceArgs; -use crate::http_server::endpoints::inference_job::common_responses::live_portrait::JobDetailsLivePortraitRequest; - pub fn extract_live_portrait_details(args: &PolymorphicInferenceArgs) -> Option { let lp = if let PolymorphicInferenceArgs::Lp(live_portrait_args) = args { @@ -47,4 +46,4 @@ mod tests { assert_eq!(details.source_media_file_token.as_str(), "portrait"); assert_eq!(details.face_driver_media_file_token.as_str(), "driver"); } -} \ No newline at end of file +}