Compare commits

...
Author SHA1 Message Date
Brandon Thomas 436f368c35 rename enum 2026-05-12 16:24:16 -04:00
Brandon Thomas b9e54702fd fixup 2026-05-12 16:18:37 -04:00
Brandon Thomas 460f475f7c codegen db 2026-05-12 16:13:25 -04:00
Brandon Thomas 98a617569b add in more places 2026-05-12 16:12:49 -04:00
Brandon Thomas a75080b650 generation provider 2026-05-12 13:37:39 -04:00
Brandon Thomas 5224f90b57 set provider on uploads via api 2026-05-12 11:00:33 -04:00
29 changed files with 296 additions and 36 deletions
@@ -1,12 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO media_files\nSET\n token = ?,\n\n media_class = ?,\n media_type = ?,\n\n origin_category = ?,\n origin_product_category = ?,\n\n maybe_engine_category = ?,\n maybe_animation_type = ?,\n\n maybe_mime_type = ?,\n file_size_bytes = ?,\n\n checksum_sha2 = ?,\n\n maybe_prompt_token = ?,\n maybe_batch_token = ?,\n\n maybe_title = ?,\n maybe_duration_millis = ?,\n\n public_bucket_directory_hash = ?,\n maybe_public_bucket_prefix = ?,\n maybe_public_bucket_extension = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n\n creator_set_visibility = ?,\n\n maybe_scene_source_media_file_token = ?,\n is_intermediate_system_file = ?,\n\n maybe_creator_file_synthetic_id = ?,\n maybe_creator_category_synthetic_id = ?,\n\n is_user_upload = TRUE,\n\n maybe_origin_model_type = NULL,\n maybe_origin_model_token = NULL\n ",
"query": "\nINSERT INTO media_files\nSET\n token = ?,\n\n media_class = ?,\n media_type = ?,\n\n origin_category = ?,\n origin_product_category = ?,\n\n maybe_engine_category = ?,\n maybe_animation_type = ?,\n\n maybe_mime_type = ?,\n file_size_bytes = ?,\n\n checksum_sha2 = ?,\n\n maybe_prompt_token = ?,\n maybe_batch_token = ?,\n\n maybe_title = ?,\n maybe_duration_millis = ?,\n\n public_bucket_directory_hash = ?,\n maybe_public_bucket_prefix = ?,\n maybe_public_bucket_extension = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n\n creator_set_visibility = ?,\n\n maybe_scene_source_media_file_token = ?,\n\n is_intermediate_system_file = ?,\n is_user_upload = ?,\n\n maybe_generation_provider = ?,\n\n maybe_creator_file_synthetic_id = ?,\n maybe_creator_category_synthetic_id = ?,\n\n maybe_origin_model_type = NULL,\n maybe_origin_model_token = NULL\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 25
"Right": 27
},
"nullable": []
},
"hash": "8d6c1f608ec25a74dddf375d525c3e30467c5eba5389094401b74b7dc74552d7"
"hash": "7369b2f3fd9e7c1e968dc5501ef9bea7e1610fd7a044a0f9ad8b908dfada2b6a"
}
@@ -1,12 +1,12 @@
{
"db_name": "MySQL",
"query": "\n INSERT INTO media_files\n SET\n token = ?,\n\n media_class = ?,\n media_type = ?,\n\n is_user_upload = ?,\n is_intermediate_system_file = ?,\n\n origin_category = ?, \n origin_product_category = ?, \n maybe_origin_model_type = ?, \n maybe_origin_model_token = ?, \n maybe_origin_filename = ?,\n\n is_batch_generated = ?,\n maybe_batch_token = ?,\n\n maybe_mime_type = ?,\n file_size_bytes = ?,\n maybe_duration_millis = ?,\n maybe_audio_encoding = ?, \n maybe_video_encoding = ?, \n maybe_frame_width = ?, \n maybe_frame_height = ?,\n maybe_prompt_token = ?,\n checksum_sha2 = ?,\n \n maybe_engine_category = ?,\n\n maybe_title = ?,\n maybe_text_transcript = ?,\n\n maybe_scene_source_media_file_token = ?,\n\n public_bucket_directory_hash = ?, \n maybe_public_bucket_prefix = ?, \n maybe_public_bucket_extension = ?, \n\n maybe_creator_user_token = ?, \n maybe_creator_anonymous_visitor_token = ?, \n\n creator_ip_address = ?, \n creator_set_visibility = ?, \n\n maybe_creator_file_synthetic_id = ?, \n maybe_creator_category_synthetic_id = ?,\n\n extra_file_modification_info = ?,\n\n maybe_cover_image_media_file_token = ?,\n\n maybe_mod_user_token = ?,\n is_generated_on_prem = ?,\n generated_by_worker = ?,\n generated_by_cluster = ?\n ",
"query": "\n INSERT INTO media_files\n SET\n token = ?,\n\n media_class = ?,\n media_type = ?,\n\n is_user_upload = ?,\n is_intermediate_system_file = ?,\n\n origin_category = ?, \n origin_product_category = ?, \n maybe_origin_model_type = ?, \n maybe_origin_model_token = ?, \n maybe_origin_filename = ?,\n\n is_batch_generated = ?,\n maybe_batch_token = ?,\n\n maybe_mime_type = ?,\n file_size_bytes = ?,\n maybe_duration_millis = ?,\n maybe_audio_encoding = ?, \n maybe_video_encoding = ?, \n maybe_frame_width = ?, \n maybe_frame_height = ?,\n maybe_prompt_token = ?,\n checksum_sha2 = ?,\n \n maybe_engine_category = ?,\n\n maybe_title = ?,\n maybe_text_transcript = ?,\n\n maybe_scene_source_media_file_token = ?,\n\n public_bucket_directory_hash = ?, \n maybe_public_bucket_prefix = ?, \n maybe_public_bucket_extension = ?, \n\n maybe_creator_user_token = ?, \n maybe_creator_anonymous_visitor_token = ?, \n\n creator_ip_address = ?, \n creator_set_visibility = ?, \n\n maybe_creator_file_synthetic_id = ?, \n maybe_creator_category_synthetic_id = ?,\n\n extra_file_modification_info = ?,\n\n maybe_generation_provider = ?,\n\n maybe_cover_image_media_file_token = ?,\n\n maybe_mod_user_token = ?,\n is_generated_on_prem = ?,\n generated_by_worker = ?,\n generated_by_cluster = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 40
"Right": 41
},
"nullable": []
},
"hash": "87d5511700985daaabfc9f2972f018d56b6672c07be082daa7c1a782400cb18d"
"hash": "a4901f904aa1c00d9afc23faa7008816c6e1b653bd6ad9f0b6772e32000be544"
}
@@ -28,6 +28,7 @@ CREATE TABLE media_files (
-- There is an index on this column.
-- Broad "category" for where the file came from:
-- * 'inference' for inference output
-- * 'inference_third' for inference output
-- * 'processed' for processed file (eg. mp3 encoding, stem splitting, etc.)
-- * 'upload' for direct user upload (from the filesystem)
-- * 'device_api' for direct user uploads recorded using Browser/Device APIs.
@@ -9,6 +9,7 @@ use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCat
use enums::by_table::media_files::media_file_origin_model_type::MediaFileOriginModelType;
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
use enums::by_table::media_files::media_file_type::MediaFileType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::visibility::Visibility;
use errors::AnyhowResult;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
@@ -88,6 +89,9 @@ pub struct InsertArgs<'a> {
pub generated_by_worker: Option<&'a str>,
pub generated_by_cluster: Option<&'a str>,
/// If provided, the third-party provider that generated this file.
pub maybe_generation_provider: Option<GenerationProvider>,
// Cover image (e.g. thumbnail for 3D splats)
pub maybe_cover_image_media_file_token: Option<&'a MediaFileToken>,
@@ -110,6 +114,22 @@ pub async fn insert_media_file_generic(
let is_batch_generated = args.maybe_batch_token.is_some();
let mut maybe_generation_provider_str = None;
let mut is_intermediate_system_file = args.is_intermediate_system_file;
let mut is_user_upload = args.is_user_upload;
let mut origin_category = args.origin_category;
if let Some(generation_provider) = args.maybe_generation_provider {
// Overrides if we're using a generation provider
maybe_generation_provider_str = Some(generation_provider.to_str());
is_intermediate_system_file = false;
is_user_upload = false;
if generation_provider != GenerationProvider::Artcraft {
origin_category = MediaFileOriginCategory::ThirdPartyInference;
}
}
let mut transaction = args.pool.begin().await?;
if let Some(user_token) = args.maybe_creator_user_token.as_deref() {
@@ -182,6 +202,8 @@ pub async fn insert_media_file_generic(
extra_file_modification_info = ?,
maybe_generation_provider = ?,
maybe_cover_image_media_file_token = ?,
maybe_mod_user_token = ?,
@@ -194,10 +216,10 @@ pub async fn insert_media_file_generic(
args.media_class.to_str(),
args.media_type.to_str(),
args.is_user_upload,
args.is_intermediate_system_file,
is_user_upload,
is_intermediate_system_file,
args.origin_category.to_str(),
origin_category.to_str(),
args.origin_product_category.to_str(),
args.maybe_origin_model_type.map(|e| e.to_str()),
args.maybe_origin_model_token.map(|t| t.to_string()),
@@ -238,6 +260,8 @@ pub async fn insert_media_file_generic(
extra_file_modification_info,
maybe_generation_provider_str,
args.maybe_cover_image_media_file_token.map(|t| t.as_str()),
args.maybe_mod_user_token,
@@ -120,6 +120,7 @@ pub async fn insert_media_file_generic_from_job(
maybe_text_transcript: args.maybe_text_transcript,
maybe_title: args.maybe_title,
maybe_video_encoding: args.maybe_video_encoding,
maybe_generation_provider: None,
maybe_cover_image_media_file_token: None,
media_class: args.media_class,
media_type: args.media_type,
@@ -9,6 +9,7 @@ use enums::by_table::media_files::media_file_origin_product_category::MediaFileO
use enums::by_table::media_files::media_file_type::MediaFileType;
use enums::common::visibility::Visibility;
use sqlx::MySqlPool;
use enums::common::generation_provider::GenerationProvider;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::batch_generations::BatchGenerationToken;
use tokens::tokens::media_files::MediaFileToken;
@@ -79,7 +80,8 @@ pub struct MediaFileInsertBuilder {
// Storage details
public_bucket_directory_hash: Option<MediaFileBucketPath>, // NB: Non-nullable field(s)
// Remaining fields...
// Generation details
maybe_generation_provider: Option<GenerationProvider>,
}
impl MediaFileInsertBuilder {
@@ -113,6 +115,7 @@ impl MediaFileInsertBuilder {
maybe_cover_image_media_file_token: None,
public_bucket_directory_hash: None,
maybe_prompt_token: None,
maybe_generation_provider: None,
}
}
@@ -264,16 +267,17 @@ impl MediaFileInsertBuilder {
self
}
pub fn maybe_generation_provider(mut self, maybe_generation_provider: Option<GenerationProvider>) -> Self {
self.maybe_generation_provider = maybe_generation_provider;
self
}
// TODO(bt,2025-04-26): Other connector options.
pub async fn insert_pool(self, mysql_pool: &MySqlPool) -> Result<MediaFileToken, MediaFileInsertBuilderError> {
let media_file_type = self.media_file_type
.ok_or_else(|| MediaFileInsertBuilderError::MissingRequiredField(
"Media file type is required".to_string()))?;
let origin_category = self.origin_category
.ok_or_else(|| MediaFileInsertBuilderError::MissingRequiredField(
"Origin category is required".to_string()))?;
let checksum_sha2 = self.checksum_sha2
.ok_or_else(|| MediaFileInsertBuilderError::MissingRequiredField(
"Checksum SHA2 is required".to_string()))?;
@@ -281,6 +285,25 @@ impl MediaFileInsertBuilder {
let bucket_path = self.public_bucket_directory_hash
.ok_or_else(|| MediaFileInsertBuilderError::MissingRequiredField(
"Public bucket directory hash is required".to_string()))?;
let mut origin_category = self.origin_category
.ok_or_else(|| MediaFileInsertBuilderError::MissingRequiredField(
"Origin category is required".to_string()))?;
let mut maybe_generation_provider_str = None;
let mut is_intermediate_system_file = self.is_intermediate_system_file;
let mut is_user_upload = self.is_user_upload;
if let Some(generation_provider) = self.maybe_generation_provider {
// Overrides if we're using a generation provider
maybe_generation_provider_str = Some(generation_provider.to_str());
is_intermediate_system_file = false;
is_user_upload = false;
if generation_provider != GenerationProvider::Artcraft {
origin_category = MediaFileOriginCategory::ThirdPartyInference;
}
}
let result = insert_media_file_generic(InsertArgs {
pool: mysql_pool,
@@ -290,8 +313,8 @@ impl MediaFileInsertBuilder {
creator_set_visibility: self.creator_set_visibility,
media_class: self.media_file_class,
media_type: media_file_type,
is_user_upload: self.is_user_upload,
is_intermediate_system_file: self.is_intermediate_system_file,
is_user_upload,
is_intermediate_system_file,
origin_category,
origin_product_category: self.origin_product_category,
maybe_origin_model_type: None, // TODO
@@ -317,6 +340,7 @@ impl MediaFileInsertBuilder {
maybe_creator_file_synthetic_id_category: IdCategory::MediaFile, // TODO: Remove this
maybe_creator_category_synthetic_id_category: IdCategory::FileUpload, // TODO: Remove this
maybe_extra_media_info: None, // TODO
maybe_generation_provider: self.maybe_generation_provider,
maybe_cover_image_media_file_token: self.maybe_cover_image_media_file_token.as_ref(),
is_generated_on_prem: false, // TODO
generated_by_worker: None, // TODO
@@ -111,6 +111,7 @@ pub async fn insert_media_file_from_studio_scene_render(
maybe_prompt_token: None,
maybe_mod_user_token: None,
maybe_engine_category: None,
maybe_generation_provider: None,
maybe_cover_image_media_file_token: None,
}).await?;
@@ -9,6 +9,7 @@ use enums::by_table::media_files::media_file_engine_category::MediaFileEngineCat
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_type::MediaFileType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::visibility::Visibility;
use errors::AnyhowResult;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
@@ -20,6 +21,7 @@ use tokens::tokens::users::UserToken;
use crate::queries::generic_synthetic_ids::transactional_increment_generic_synthetic_id::transactional_increment_generic_synthetic_id;
pub enum UploadType {
ThirdPartyInference,
Filesystem,
DeviceCaptureApi,
StorytellerEngine,
@@ -54,6 +56,10 @@ pub struct InsertMediaFileFromUploadArgs<'a> {
pub maybe_scene_source_media_file_token: Option<&'a MediaFileToken>,
pub is_intermediate_system_file: bool,
/// If provided, the third-party provider that generated this file.
/// When set, `is_user_upload` and `is_intermediate_system_file` will be set to false.
pub maybe_generation_provider: Option<GenerationProvider>,
pub public_bucket_directory_hash: &'a str,
pub maybe_public_bucket_prefix: Option<&'a str>,
pub maybe_public_bucket_extension: Option<&'a str>,
@@ -65,12 +71,27 @@ pub async fn insert_media_file_from_file_upload(
{
let token = MediaFileToken::generate();
let origin_category = match args.upload_type {
let mut origin_category = match args.upload_type {
UploadType::ThirdPartyInference => MediaFileOriginCategory::ThirdPartyInference,
UploadType::Filesystem => MediaFileOriginCategory::Upload,
UploadType::DeviceCaptureApi => MediaFileOriginCategory::DeviceApi,
UploadType::StorytellerEngine => MediaFileOriginCategory::StorytellerStudio,
};
let mut maybe_generation_provider_str = None;
let mut is_intermediate_system_file = args.is_intermediate_system_file;
let mut is_user_upload = true;
if let Some(generation_provider) = args.maybe_generation_provider {
// Overrides if we're using a generation provider
maybe_generation_provider_str = Some(generation_provider.to_str());
is_intermediate_system_file = false;
is_user_upload = false;
if generation_provider != GenerationProvider::Artcraft {
origin_category = MediaFileOriginCategory::ThirdPartyInference;
}
}
let mut maybe_creator_file_synthetic_id : Option<u64> = None;
let mut maybe_creator_category_synthetic_id : Option<u64> = None;
@@ -134,13 +155,15 @@ SET
creator_set_visibility = ?,
maybe_scene_source_media_file_token = ?,
is_intermediate_system_file = ?,
is_user_upload = ?,
maybe_generation_provider = ?,
maybe_creator_file_synthetic_id = ?,
maybe_creator_category_synthetic_id = ?,
is_user_upload = TRUE,
maybe_origin_model_type = NULL,
maybe_origin_model_token = NULL
"#,
@@ -180,7 +203,11 @@ SET
args.creator_set_visibility.to_str(),
args.maybe_scene_source_media_file_token.map(|t| t.as_str()),
args.is_intermediate_system_file,
is_intermediate_system_file,
is_user_upload,
maybe_generation_provider_str,
maybe_creator_file_synthetic_id,
maybe_creator_category_synthetic_id,
@@ -18,6 +18,11 @@ pub enum MediaFileOriginCategory {
/// ML model inference output - uploaded models or zero shot.
Inference,
/// Third-party inference output (e.g. FAL, Replicate).
/// The file was generated by a third-party AI service and uploaded by the client.
#[serde(rename = "inference3rd")]
ThirdPartyInference,
/// Processed results - (we don't have these systems yet, but eg. trim, transcode, etc).
Processed,
@@ -47,6 +52,7 @@ impl MediaFileOriginCategory {
pub fn to_str(&self) -> &'static str {
match self {
Self::Inference => "inference",
Self::ThirdPartyInference => "inference3rd",
Self::Processed => "processed",
Self::Upload => "upload",
Self::DeviceApi => "device_api",
@@ -58,6 +64,7 @@ impl MediaFileOriginCategory {
pub fn from_str(value: &str) -> Result<Self, String> {
match value {
"inference" => Ok(Self::Inference),
"inference3rd" => Ok(Self::ThirdPartyInference),
"processed" => Ok(Self::Processed),
"upload" => Ok(Self::Upload),
"device_api" => Ok(Self::DeviceApi),
@@ -68,10 +75,9 @@ impl MediaFileOriginCategory {
}
pub fn all_variants() -> BTreeSet<Self> {
// NB: BTreeSet is sorted
// NB: BTreeSet::from() isn't const, but not worth using LazyStatic, etc.
BTreeSet::from([
Self::Inference,
Self::ThirdPartyInference,
Self::Processed,
Self::Upload,
Self::DeviceApi,
@@ -92,6 +98,7 @@ mod tests {
#[test]
fn test_serialization() {
assert_serialization(MediaFileOriginCategory::Inference, "inference");
assert_serialization(MediaFileOriginCategory::ThirdPartyInference, "inference3rd");
assert_serialization(MediaFileOriginCategory::Processed, "processed");
assert_serialization(MediaFileOriginCategory::Upload, "upload");
assert_serialization(MediaFileOriginCategory::DeviceApi, "device_api");
@@ -102,6 +109,7 @@ mod tests {
#[test]
fn test_to_str() {
assert_eq!(MediaFileOriginCategory::Inference.to_str(), "inference");
assert_eq!(MediaFileOriginCategory::ThirdPartyInference.to_str(), "inference3rd");
assert_eq!(MediaFileOriginCategory::Processed.to_str(), "processed");
assert_eq!(MediaFileOriginCategory::Upload.to_str(), "upload");
assert_eq!(MediaFileOriginCategory::DeviceApi.to_str(), "device_api");
@@ -112,6 +120,7 @@ mod tests {
#[test]
fn test_from_str() {
assert_eq!(MediaFileOriginCategory::from_str("inference").unwrap(), MediaFileOriginCategory::Inference);
assert_eq!(MediaFileOriginCategory::from_str("inference3rd").unwrap(), MediaFileOriginCategory::ThirdPartyInference);
assert_eq!(MediaFileOriginCategory::from_str("processed").unwrap(), MediaFileOriginCategory::Processed);
assert_eq!(MediaFileOriginCategory::from_str("upload").unwrap(), MediaFileOriginCategory::Upload);
assert_eq!(MediaFileOriginCategory::from_str("device_api").unwrap(), MediaFileOriginCategory::DeviceApi);
@@ -123,8 +132,9 @@ mod tests {
#[test]
fn all_variants() {
let mut variants = MediaFileOriginCategory::all_variants();
assert_eq!(variants.len(), 6);
assert_eq!(variants.len(), 7);
assert_eq!(variants.pop_first(), Some(MediaFileOriginCategory::Inference));
assert_eq!(variants.pop_first(), Some(MediaFileOriginCategory::ThirdPartyInference));
assert_eq!(variants.pop_first(), Some(MediaFileOriginCategory::Processed));
assert_eq!(variants.pop_first(), Some(MediaFileOriginCategory::Upload));
assert_eq!(variants.pop_first(), Some(MediaFileOriginCategory::DeviceApi));
@@ -213,6 +213,7 @@ pub async fn create_scene_handler(
maybe_title: upload_media_request.title.as_deref(),
maybe_scene_source_media_file_token: None,
is_intermediate_system_file: false,
maybe_generation_provider: None,
public_bucket_directory_hash: public_upload_path.get_object_hash(),
maybe_public_bucket_prefix: PREFIX,
maybe_public_bucket_extension: SUFFIX,
@@ -341,6 +341,7 @@ pub async fn process_upload_media_file(
maybe_title: upload_media_request.title.as_deref(),
maybe_scene_source_media_file_token: None,
is_intermediate_system_file: false,
maybe_generation_provider: None,
public_bucket_directory_hash: public_upload_path.get_object_hash(),
maybe_public_bucket_prefix: PREFIX,
maybe_public_bucket_extension: extension.as_deref(),
@@ -255,6 +255,7 @@ pub async fn upload_snapshot_media_file_handler(
maybe_title: Some("Snapshot"),
maybe_scene_source_media_file_token: None,
is_intermediate_system_file: true,
maybe_generation_provider: None,
public_bucket_directory_hash: public_upload_path.get_object_hash(),
maybe_public_bucket_prefix: PREFIX,
maybe_public_bucket_extension: Some(&extension),
@@ -0,0 +1 @@
pub mod try_parse_generation_provider;
@@ -0,0 +1,51 @@
use enums::common::generation_provider::GenerationProvider;
use log::warn;
/// Try to parse a generation provider from a string value.
/// Fails open: returns None if parsing fails, but logs a warning.
pub fn try_parse_generation_provider(value: &str) -> Option<GenerationProvider> {
let trimmed = value.trim();
if trimmed.is_empty() {
return None;
}
match GenerationProvider::from_str(trimmed) {
Ok(provider) => Some(provider),
Err(_) => {
warn!("Could not parse generation provider: {:?}", trimmed);
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_known_providers() {
assert_eq!(try_parse_generation_provider("fal"), Some(GenerationProvider::Fal));
assert_eq!(try_parse_generation_provider("artcraft"), Some(GenerationProvider::Artcraft));
assert_eq!(try_parse_generation_provider("grok"), Some(GenerationProvider::Grok));
assert_eq!(try_parse_generation_provider("midjourney"), Some(GenerationProvider::Midjourney));
assert_eq!(try_parse_generation_provider("sora"), Some(GenerationProvider::Sora));
assert_eq!(try_parse_generation_provider("world_labs"), Some(GenerationProvider::WorldLabs));
}
#[test]
fn trims_whitespace() {
assert_eq!(try_parse_generation_provider(" fal "), Some(GenerationProvider::Fal));
}
#[test]
fn returns_none_for_empty() {
assert_eq!(try_parse_generation_provider(""), None);
assert_eq!(try_parse_generation_provider(" "), None);
}
#[test]
fn returns_none_for_unknown() {
assert_eq!(try_parse_generation_provider("unknown_provider"), None);
assert_eq!(try_parse_generation_provider("DALL-E"), None);
}
}
@@ -1,3 +1,4 @@
pub mod common_utils;
pub mod upload_audio_media_file_handler;
pub mod upload_engine_asset;
pub mod upload_error;
@@ -26,6 +26,7 @@ use mysql_queries::queries::media_files::create::specialized_insert::insert_medi
use tokens::tokens::media_files::MediaFileToken;
use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError;
use crate::http_server::endpoints::media_files::upload::common_utils::try_parse_generation_provider::try_parse_generation_provider;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::state::server_state::ServerState;
@@ -55,6 +56,12 @@ pub struct UploadAudioMediaFileForm {
#[multipart(limit = "2 KiB")]
#[schema(value_type = Option<Visibility>, format = Binary)]
maybe_visibility: Option<Text<Visibility>>,
/// Optional: The third-party generation provider (e.g. "fal", "replicate").
/// If set, `is_user_upload` will be false and `is_intermediate_system_file` will be forced false.
#[multipart(limit = "2 KiB")]
#[schema(value_type = Option<String>, format = Binary)]
maybe_generation_provider: Option<Text<String>>,
}
// Unlike the "upload" endpoints, which are pure inserts, these endpoints are *upserts*.
@@ -314,6 +321,16 @@ pub async fn upload_audio_media_file_handler(
MediaFileUploadError::ServerError
})?;
let maybe_generation_provider = form.maybe_generation_provider
.as_ref()
.and_then(|text| try_parse_generation_provider(text.as_ref()));
let upload_type = if maybe_generation_provider.is_some() {
UploadType::ThirdPartyInference
} else {
UploadType::Filesystem
};
let (token, record_id) = insert_media_file_from_file_upload(InsertMediaFileFromUploadArgs {
maybe_media_class: Some(MediaFileClass::Audio),
media_file_type: MediaFileType::Audio,
@@ -321,7 +338,7 @@ pub async fn upload_audio_media_file_handler(
maybe_creator_anonymous_visitor_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility,
upload_type: UploadType::Filesystem, // TODO(bt,2024-05-02): This should be a parameter and a well-known enum.
upload_type,
maybe_engine_category: None,
maybe_animation_type: None,
maybe_prompt_token: None,
@@ -333,6 +350,7 @@ pub async fn upload_audio_media_file_handler(
maybe_title: maybe_title.as_deref(),
maybe_scene_source_media_file_token: None,
is_intermediate_system_file: false, // NB: is_user_upload = true
maybe_generation_provider,
public_bucket_directory_hash: public_upload_path.get_object_hash(),
maybe_public_bucket_prefix: PREFIX,
maybe_public_bucket_extension: Some(&extension),
@@ -6,8 +6,9 @@ use log::warn;
use enums::by_table::media_files::media_file_animation_type::MediaFileAnimationType;
use enums::by_table::media_files::media_file_engine_category::MediaFileEngineCategory;
use enums::common::generation_provider::GenerationProvider;
use errors::AnyhowResult;
use crate::http_server::endpoints::media_files::upload::common_utils::try_parse_generation_provider::try_parse_generation_provider;
use crate::http_server::web_utils::read_multipart_field_bytes::{checked_read_multipart_bytes, read_multipart_field_as_text};
pub struct MediaFileUploadData {
@@ -26,6 +27,9 @@ pub struct MediaFileUploadData {
// Optional: visibility
pub maybe_animation_type: Option<MediaFileAnimationType>,
// Optional: The third-party generation provider (e.g. "fal", "replicate").
pub maybe_generation_provider: Option<GenerationProvider>,
}
/// Pull common parts out of multipart media HTTP requests, typically for handling file uploads.
@@ -37,6 +41,7 @@ pub async fn drain_multipart_request(mut multipart_payload: Multipart) -> Anyhow
let mut visibility = None;
let mut maybe_engine_category = None;
let mut maybe_animation_type = None;
let mut generation_provider = None;
while let Ok(Some(mut field)) = multipart_payload.try_next().await {
let mut field_name = None;
@@ -105,10 +110,21 @@ pub async fn drain_multipart_request(mut multipart_payload: Multipart) -> Anyhow
e
})?;
},
Some("maybe_generation_provider") => {
generation_provider = read_multipart_field_as_text(&mut field).await
.map_err(|e| {
warn!("Error reading maybe_generation_provider: {:}", &e);
e
})?;
},
_ => continue,
}
}
let maybe_generation_provider = generation_provider.as_deref()
.map(|s| try_parse_generation_provider(s))
.flatten();
Ok(MediaFileUploadData {
uuid_idempotency_token,
file_name,
@@ -117,5 +133,6 @@ pub async fn drain_multipart_request(mut multipart_payload: Multipart) -> Anyhow
maybe_visibility: visibility,
maybe_engine_category,
maybe_animation_type,
maybe_generation_provider,
})
}
@@ -221,6 +221,7 @@ pub async fn upload_engine_asset_media_file_handler(
maybe_title: upload_media_request.maybe_title.as_deref(),
maybe_scene_source_media_file_token: None,
is_intermediate_system_file: false, // NB: is_user_upload = TRUE
maybe_generation_provider: upload_media_request.maybe_generation_provider,
public_bucket_directory_hash: public_upload_path.get_object_hash(),
maybe_public_bucket_prefix: PREFIX,
maybe_public_bucket_extension: Some(suffix),
@@ -1,12 +1,12 @@
use crate::http_server::endpoints::media_files::upload::common_utils::try_parse_generation_provider::try_parse_generation_provider;
use crate::http_server::web_utils::read_multipart_field_bytes::{checked_read_multipart_bytes, read_multipart_field_as_text};
use actix_multipart::Multipart;
use actix_web::web::BytesMut;
use enums::common::generation_provider::GenerationProvider;
use errors::AnyhowResult;
use futures::TryStreamExt;
use log::warn;
use errors::AnyhowResult;
use crate::http_server::web_utils::read_multipart_field_bytes::{checked_read_multipart_bytes, read_multipart_field_as_text};
pub struct MediaFileUploadData {
pub uuid_idempotency_token: Option<String>,
pub file_name: Option<String>,
@@ -18,6 +18,9 @@ pub struct MediaFileUploadData {
// Optional: visibility
pub visibility: Option<String>,
// Optional: The third-party generation provider (e.g. "fal", "replicate").
pub maybe_generation_provider: Option<GenerationProvider>,
}
/// Where the frontend tells us the file came from.
@@ -38,6 +41,7 @@ pub async fn drain_multipart_request(mut multipart_payload: Multipart) -> Anyhow
let mut media_source = None;
let mut title = None;
let mut visibility = None;
let mut generation_provider = None;
while let Ok(Some(mut field)) = multipart_payload.try_next().await {
let mut field_name = None;
@@ -54,7 +58,7 @@ pub async fn drain_multipart_request(mut multipart_payload: Multipart) -> Anyhow
Some("uuid_idempotency_token") => {
uuid_idempotency_token = read_multipart_field_as_text(&mut field).await
.map_err(|e| {
warn!("Error reading idempotency token: {:}", &e);
warn!("Error reading uuid_idempotency_token: {:}", &e);
e
})?;
},
@@ -62,7 +66,7 @@ pub async fn drain_multipart_request(mut multipart_payload: Multipart) -> Anyhow
file_name = field_filename.clone();
file_bytes = checked_read_multipart_bytes(&mut field).await
.map_err(|e| {
warn!("Error reading audio upload: {:}", &e);
warn!("Error reading file: {:}", &e);
e
})?;
},
@@ -83,7 +87,14 @@ pub async fn drain_multipart_request(mut multipart_payload: Multipart) -> Anyhow
Some("visibility") => {
visibility = read_multipart_field_as_text(&mut field).await
.map_err(|e| {
warn!("Error reading title: {:}", &e);
warn!("Error reading visibility: {:}", &e);
e
})?;
},
Some("maybe_generation_provider") => {
generation_provider = read_multipart_field_as_text(&mut field).await
.map_err(|e| {
warn!("Error reading maybe_generation_provider: {:}", &e);
e
})?;
},
@@ -103,6 +114,10 @@ pub async fn drain_multipart_request(mut multipart_payload: Multipart) -> Anyhow
},
};
let maybe_generation_provider = generation_provider.as_deref()
.map(|s| try_parse_generation_provider(s))
.flatten();
Ok(MediaFileUploadData {
uuid_idempotency_token,
file_name,
@@ -110,5 +125,6 @@ pub async fn drain_multipart_request(mut multipart_payload: Multipart) -> Anyhow
media_source,
title,
visibility,
maybe_generation_provider,
})
}
@@ -405,6 +405,7 @@ pub async fn process_upload_media_file(
.checksum_sha2(&hash)
.maybe_title(upload_media_request.title.as_deref())
.maybe_origin_filename(upload_media_request.file_name.as_deref())
.maybe_generation_provider(upload_media_request.maybe_generation_provider)
.is_intermediate_system_file(false)
.insert_pool(&server_state.mysql_pool)
.await
@@ -43,6 +43,7 @@ static ALLOWED_MIME_TYPES : Lazy<HashSet<&'static str>> = Lazy::new(|| {
});
/// DEPRECATED: Use one of the various specialized upload endpoints instead.
/// TODO: Does not support `maybe_generation_provider`. Use the specialized upload endpoints instead.
#[deprecated]
#[utoipa::path(
post,
@@ -13,6 +13,7 @@ use once_cell::sync::Lazy;
use utoipa::ToSchema;
use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError;
use crate::http_server::endpoints::media_files::upload::common_utils::try_parse_generation_provider::try_parse_generation_provider;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::state::server_state::ServerState;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
@@ -72,6 +73,12 @@ pub struct UploadImageMediaFileForm {
#[multipart(limit = "2 KiB")]
#[schema(value_type = Option<bool>, format = Binary)]
is_intermediate_system_file: Option<Text<bool>>,
/// Optional: The third-party generation provider (e.g. "fal", "replicate").
/// If set, `is_user_upload` will be false and `is_intermediate_system_file` will be forced false.
#[multipart(limit = "2 KiB")]
#[schema(value_type = Option<String>, format = Binary)]
maybe_generation_provider: Option<Text<String>>,
}
// Unlike the "upload" endpoints, which are pure inserts, these endpoints are *upserts*.
@@ -282,6 +289,16 @@ pub async fn upload_image_media_file_handler(
.as_ref()
.map(|token| token.0.clone());
let maybe_generation_provider = form.maybe_generation_provider
.as_ref()
.and_then(|text| try_parse_generation_provider(text.as_ref()));
let upload_type = if maybe_generation_provider.is_some() {
UploadType::ThirdPartyInference
} else {
UploadType::Filesystem
};
let (token, record_id) = insert_media_file_from_file_upload(InsertMediaFileFromUploadArgs {
maybe_media_class: Some(MediaFileClass::Image),
media_file_type: MediaFileType::Image,
@@ -289,7 +306,7 @@ pub async fn upload_image_media_file_handler(
maybe_creator_anonymous_visitor_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility,
upload_type: UploadType::Filesystem, // TODO(bt,2024-05-02): This should be a parameter and a well-known enum.
upload_type,
maybe_engine_category: None,
maybe_animation_type: None,
maybe_mime_type: Some(&mimetype),
@@ -300,7 +317,8 @@ pub async fn upload_image_media_file_handler(
sha256_checksum: &hash,
maybe_title: maybe_title.as_deref(),
maybe_scene_source_media_file_token: None,
is_intermediate_system_file, // NB: is_user_upload = TRUE
is_intermediate_system_file,
maybe_generation_provider,
public_bucket_directory_hash: public_upload_path.get_object_hash(),
maybe_public_bucket_prefix: PREFIX,
maybe_public_bucket_extension: Some(&extension),
@@ -22,7 +22,7 @@ use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_
use mysql_queries::queries::media_files::create::specialized_insert::insert_media_file_from_file_upload::{insert_media_file_from_file_upload, InsertMediaFileFromUploadArgs, UploadType};
use mysql_queries::queries::users::user_sessions::get_user_session_by_token::SessionUserRecord;
use tokens::tokens::media_files::MediaFileToken;
use crate::http_server::endpoints::media_files::upload::common_utils::try_parse_generation_provider::try_parse_generation_provider;
use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::state::server_state::ServerState;
@@ -70,6 +70,12 @@ pub struct UploadNewEngineAssetFileForm {
#[multipart(limit = "2 KiB")]
#[schema(value_type = Option<u64>, format = Binary)]
maybe_duration_millis: Option<Text<u64>>,
/// Optional: The third-party generation provider (e.g. "fal", "replicate").
/// If set, `is_user_upload` will be false and `is_intermediate_system_file` will be forced false.
#[multipart(limit = "2 KiB")]
#[schema(value_type = Option<String>, format = Binary)]
maybe_generation_provider: Option<Text<String>>,
}
#[derive(Serialize, ToSchema)]
@@ -177,6 +183,10 @@ pub async fn upload_new_engine_asset_media_file_handler(
// ==================== FILE DATA ==================== //
let maybe_generation_provider = form.maybe_generation_provider
.as_ref()
.and_then(|text| try_parse_generation_provider(text.as_ref()));
let file_info = validate_and_process_form(maybe_user_session.as_ref(), form)?;
// ==================== UPLOAD AND SAVE ==================== //
@@ -216,6 +226,7 @@ pub async fn upload_new_engine_asset_media_file_handler(
sha256_checksum: &file_info.sha256_checksum,
maybe_scene_source_media_file_token: None,
is_intermediate_system_file: false, // NB: is_user_upload = TRUE
maybe_generation_provider,
maybe_title: file_info.maybe_title.as_deref(),
public_bucket_directory_hash: public_upload_path.get_object_hash(),
maybe_public_bucket_prefix: PREFIX,
@@ -242,6 +242,7 @@ pub async fn upload_new_scene_media_file_handler(
maybe_title: maybe_title.as_deref(),
maybe_scene_source_media_file_token: None,
is_intermediate_system_file: false, // NB: is_user_upload = TRUE
maybe_generation_provider: None,
public_bucket_directory_hash: public_upload_path.get_object_hash(),
maybe_public_bucket_prefix: PREFIX,
maybe_public_bucket_extension: Some(SUFFIX),
@@ -272,6 +272,7 @@ pub async fn upload_pmx_media_file_handler(
sha256_checksum: &pmx_details.sha256_checksum,
maybe_scene_source_media_file_token: None,
is_intermediate_system_file: false, // NB: is_user_upload = TRUE
maybe_generation_provider: None,
maybe_title: maybe_title.as_deref(),
public_bucket_directory_hash: pmx_details.pmx_public_upload_path.get_object_hash(),
maybe_public_bucket_prefix: PREFIX,
@@ -236,6 +236,7 @@ pub async fn upload_scene_snapshot_media_file_handler(
maybe_scene_source_media_file_token: maybe_scene_source_media_file_token.as_ref(),
// NB: is_user_upload = TRUE
is_intermediate_system_file: true, // NB: This is a snapshot, so we shouldn't show it off in lists.
maybe_generation_provider: None,
public_bucket_directory_hash: public_upload_path.get_object_hash(),
maybe_public_bucket_prefix: PREFIX,
maybe_public_bucket_extension: Some(SUFFIX),
@@ -24,6 +24,7 @@ use mysql_queries::queries::media_files::create::specialized_insert::insert_medi
use tokens::tokens::media_files::MediaFileToken;
use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError;
use crate::http_server::endpoints::media_files::upload::common_utils::try_parse_generation_provider::try_parse_generation_provider;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::state::server_state::ServerState;
@@ -56,6 +57,11 @@ pub struct UploadSpzMediaFileForm {
#[multipart(limit = "2 KiB")]
#[schema(value_type = Option<String>, format = Binary)]
maybe_visibility: Option<Text<Visibility>>,
/// Optional: The third-party generation provider (e.g. "fal", "replicate").
#[multipart(limit = "2 KiB")]
#[schema(value_type = Option<String>, format = Binary)]
maybe_generation_provider: Option<Text<String>>,
}
#[derive(Serialize, ToSchema)]
@@ -242,6 +248,10 @@ pub async fn upload_spz_media_file_handler(
MediaFileUploadError::ServerError
})?;
let maybe_generation_provider = form.maybe_generation_provider
.as_ref()
.and_then(|text| try_parse_generation_provider(text.as_ref()));
let media_token = MediaFileInsertBuilder::new()
.media_file_class(MediaFileClass::Dimensional)
.media_file_type(MediaFileType::Spz)
@@ -257,6 +267,7 @@ pub async fn upload_spz_media_file_handler(
.is_intermediate_system_file(false)
.public_bucket_directory_hash(&public_upload_path)
.maybe_origin_filename(form.file.file_name.as_deref())
.maybe_generation_provider(maybe_generation_provider)
.insert_pool(&server_state.mysql_pool)
.await
.map_err(|err| {
@@ -33,6 +33,7 @@ use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::prompts::PromptToken;
use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError;
use crate::http_server::endpoints::media_files::upload::common_utils::try_parse_generation_provider::try_parse_generation_provider;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::state::server_state::ServerState;
@@ -99,6 +100,12 @@ pub struct UploadNewVideoMediaFileForm {
#[multipart(limit = "2 KiB")]
#[schema(value_type = Option<u64>, format = Binary)]
maybe_resample_fps: Option<Text<u8>>,
/// Optional: The third-party generation provider (e.g. "fal", "replicate").
/// If set, `is_user_upload` will be false and `is_intermediate_system_file` will be forced false.
#[multipart(limit = "2 KiB")]
#[schema(value_type = Option<String>, format = Binary)]
maybe_generation_provider: Option<Text<String>>,
}
// Unlike the "upload" endpoints, which are pure inserts, these endpoints are *upserts*.
@@ -487,6 +494,16 @@ pub async fn upload_new_video_media_file_handler(
maybe_scene_source_media_file_token.is_some() ||
form.is_intermediate_system_file.map(|b| b.0).unwrap_or(false);
let maybe_generation_provider = form.maybe_generation_provider
.as_ref()
.and_then(|text| try_parse_generation_provider(text.as_ref()));
let upload_type = if maybe_generation_provider.is_some() {
UploadType::ThirdPartyInference
} else {
UploadType::Filesystem
};
let (token, record_id) = insert_media_file_from_file_upload(InsertMediaFileFromUploadArgs {
maybe_media_class: Some(MediaFileClass::Video),
media_file_type: MediaFileType::Video,
@@ -494,7 +511,7 @@ pub async fn upload_new_video_media_file_handler(
maybe_creator_anonymous_visitor_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility,
upload_type: UploadType::Filesystem, // TODO(bt,2024-05-02): This should be a parameter and a well-known enum.
upload_type,
maybe_engine_category: None,
maybe_animation_type: None,
maybe_prompt_token: maybe_prompt_token.as_ref(),
@@ -506,6 +523,7 @@ pub async fn upload_new_video_media_file_handler(
maybe_title: maybe_title.as_deref(),
maybe_scene_source_media_file_token,
is_intermediate_system_file, // NB: is_user_upload = TRUE
maybe_generation_provider,
public_bucket_directory_hash: public_upload_path.get_object_hash(),
maybe_public_bucket_prefix: PREFIX,
maybe_public_bucket_extension: Some(&extension),
@@ -237,6 +237,7 @@ pub async fn upload_video_media_file_handler(
maybe_title: upload_media_request.title.as_deref(),
maybe_scene_source_media_file_token: None,
is_intermediate_system_file: false, // NB: is_user_upload = TRUE
maybe_generation_provider: None,
public_bucket_directory_hash: public_upload_path.get_object_hash(),
maybe_public_bucket_prefix: PREFIX,
maybe_public_bucket_extension: Some(&extension),