Compare commits

..
Author SHA1 Message Date
Brandon Thomas acc16c15fb remove fields from table 2026-07-08 22:06:53 -04:00
170 changed files with 4923 additions and 4368 deletions
@@ -1,223 +0,0 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n m.id as `id: u64`,\n m.token as `token: tokens::tokens::media_files::MediaFileToken`,\n\n m.media_class as `media_class: enums::by_table::media_files::media_file_class::MediaFileClass`,\n m.maybe_project_type as `project_type!: enums::by_table::media_files::media_file_project_type::MediaFileProjectType`,\n m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\n\n media_file_cover_image.public_bucket_directory_hash as maybe_file_cover_image_public_bucket_hash,\n media_file_cover_image.maybe_public_bucket_prefix as maybe_file_cover_image_public_bucket_prefix,\n media_file_cover_image.maybe_public_bucket_extension as maybe_file_cover_image_public_bucket_extension,\n\n m.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.maybe_title,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m FORCE INDEX (fk_maybe_creator_user_token)\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\nLEFT OUTER JOIN media_files as media_file_cover_image\n ON media_file_cover_image.token = m.maybe_cover_image_media_file_token\nWHERE\n m.maybe_creator_user_token = ?\n AND m.media_class = 'project'\n AND m.maybe_project_type IS NOT NULL\n AND m.maybe_project_type = ?\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n AND NOT m.is_intermediate_system_file\n AND m.id > ?\nORDER BY m.id ASC\nLIMIT ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id: u64",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 1,
"name": "token: tokens::tokens::media_files::MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "media_class: enums::by_table::media_files::media_file_class::MediaFileClass",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 3,
"name": "project_type!: enums::by_table::media_files::media_file_project_type::MediaFileProjectType",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 4,
"name": "media_type: enums::by_table::media_files::media_file_type::MediaFileType",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 5,
"name": "public_bucket_directory_hash",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 6,
"name": "maybe_public_bucket_prefix",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 7,
"name": "maybe_public_bucket_extension",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 8,
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 9,
"name": "maybe_creator_username",
"type_info": {
"type": "VarString",
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 10,
"name": "maybe_creator_display_name",
"type_info": {
"type": "VarString",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 11,
"name": "maybe_creator_gravatar_hash",
"type_info": {
"type": "String",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 12,
"name": "maybe_file_cover_image_public_bucket_hash",
"type_info": {
"type": "VarString",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 13,
"name": "maybe_file_cover_image_public_bucket_prefix",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 14,
"name": "maybe_file_cover_image_public_bucket_extension",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 15,
"name": "creator_set_visibility: enums::common::visibility::Visibility",
"type_info": {
"type": "String",
"flags": "NOT_NULL | BINARY | ENUM",
"char_set": 224,
"max_size": 28
}
},
{
"ordinal": 16,
"name": "maybe_title",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 17,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 18,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 4
},
"nullable": [
false,
false,
false,
true,
false,
false,
true,
true,
true,
true,
true,
true,
true,
true,
true,
false,
true,
false,
false
]
},
"hash": "11cc7b91730784e9e8c8f073aeaf51a1f1660584e379b9a5be3aea370d84f692"
}
@@ -0,0 +1,36 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n t.token as `token: tokens::tokens::tags::TagToken`,\n t.tag_value\n\nFROM\n tags AS t\nJOIN tag_uses AS u\n ON t.token = u.tag_token\nWHERE\n u.entity_type = ?\n AND u.entity_token = ?\n AND u.maybe_deleted_at IS NULL\nORDER BY t.id DESC\nLIMIT 500\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token: tokens::tokens::tags::TagToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "tag_value",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false
]
},
"hash": "3929e9fe74e75fad7252681880c6c3bd7e76e96b0bf603f77f6a9b2d4fcda18d"
}
@@ -0,0 +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 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_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\nON DUPLICATE KEY UPDATE\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 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_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 ",
"describe": {
"columns": [],
"parameters": {
"Right": 37
},
"nullable": []
},
"hash": "3af0a10ab0ed65daba9363605db4ddae9a60a2177369c58046abdee9ad1e91ee"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nDELETE FROM tag_uses\nWHERE entity_type = ?\nAND entity_token = ?\nLIMIT 1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "41b16ccf04ef1ffb8b48bc59edf9389f61f54b871f6361f35529706b98d419ec"
}
@@ -1,223 +0,0 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n m.id as `id: u64`,\n m.token as `token: tokens::tokens::media_files::MediaFileToken`,\n\n m.media_class as `media_class: enums::by_table::media_files::media_file_class::MediaFileClass`,\n m.maybe_project_type as `project_type!: enums::by_table::media_files::media_file_project_type::MediaFileProjectType`,\n m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\n\n media_file_cover_image.public_bucket_directory_hash as maybe_file_cover_image_public_bucket_hash,\n media_file_cover_image.maybe_public_bucket_prefix as maybe_file_cover_image_public_bucket_prefix,\n media_file_cover_image.maybe_public_bucket_extension as maybe_file_cover_image_public_bucket_extension,\n\n m.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.maybe_title,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m FORCE INDEX (fk_maybe_creator_user_token)\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\nLEFT OUTER JOIN media_files as media_file_cover_image\n ON media_file_cover_image.token = m.maybe_cover_image_media_file_token\nWHERE\n m.maybe_creator_user_token = ?\n AND m.media_class = 'project'\n AND m.maybe_project_type IS NOT NULL\n AND m.maybe_project_type = ?\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n AND NOT m.is_intermediate_system_file\n AND m.id < ?\nORDER BY m.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id: u64",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 1,
"name": "token: tokens::tokens::media_files::MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "media_class: enums::by_table::media_files::media_file_class::MediaFileClass",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 3,
"name": "project_type!: enums::by_table::media_files::media_file_project_type::MediaFileProjectType",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 4,
"name": "media_type: enums::by_table::media_files::media_file_type::MediaFileType",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 5,
"name": "public_bucket_directory_hash",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 6,
"name": "maybe_public_bucket_prefix",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 7,
"name": "maybe_public_bucket_extension",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 8,
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 9,
"name": "maybe_creator_username",
"type_info": {
"type": "VarString",
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 10,
"name": "maybe_creator_display_name",
"type_info": {
"type": "VarString",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 11,
"name": "maybe_creator_gravatar_hash",
"type_info": {
"type": "String",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 12,
"name": "maybe_file_cover_image_public_bucket_hash",
"type_info": {
"type": "VarString",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 13,
"name": "maybe_file_cover_image_public_bucket_prefix",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 14,
"name": "maybe_file_cover_image_public_bucket_extension",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 15,
"name": "creator_set_visibility: enums::common::visibility::Visibility",
"type_info": {
"type": "String",
"flags": "NOT_NULL | BINARY | ENUM",
"char_set": 224,
"max_size": 28
}
},
{
"ordinal": 16,
"name": "maybe_title",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 17,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 18,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 4
},
"nullable": [
false,
false,
false,
true,
false,
false,
true,
true,
true,
true,
true,
true,
true,
true,
true,
false,
true,
false,
false
]
},
"hash": "6c7adbc8171bc3fdfb6109193ffb0a55413fa0942746d65037c2b1d39b361bd0"
}
@@ -0,0 +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\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": 27
},
"nullable": []
},
"hash": "7369b2f3fd9e7c1e968dc5501ef9bea7e1610fd7a044a0f9ad8b908dfada2b6a"
}
@@ -1,58 +0,0 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n maybe_creator_user_token as `maybe_creator_user_token: UserToken`,\n maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: AnonymousVisitorTrackingToken`,\n media_type as `media_type: MediaFileType`,\n maybe_project_type as `maybe_project_type: MediaFileProjectType`\nFROM media_files\nWHERE token = ?\n AND user_deleted_at IS NULL\n AND mod_deleted_at IS NULL\nLIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "maybe_creator_user_token: UserToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "maybe_creator_anonymous_visitor_token: AnonymousVisitorTrackingToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "media_type: MediaFileType",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 3,
"name": "maybe_project_type: MediaFileProjectType",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
true,
true,
false,
true
]
},
"hash": "8dcd4bb064881f245beb39dd246e74ec1a2c47b69d7ce92a0d175f58dc4b9e63"
}
@@ -1,223 +0,0 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n m.id as `id: u64`,\n m.token as `token: tokens::tokens::media_files::MediaFileToken`,\n\n m.media_class as `media_class: enums::by_table::media_files::media_file_class::MediaFileClass`,\n m.maybe_project_type as `project_type!: enums::by_table::media_files::media_file_project_type::MediaFileProjectType`,\n m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\n\n media_file_cover_image.public_bucket_directory_hash as maybe_file_cover_image_public_bucket_hash,\n media_file_cover_image.maybe_public_bucket_prefix as maybe_file_cover_image_public_bucket_prefix,\n media_file_cover_image.maybe_public_bucket_extension as maybe_file_cover_image_public_bucket_extension,\n\n m.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.maybe_title,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m FORCE INDEX (fk_maybe_creator_user_token)\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\nLEFT OUTER JOIN media_files as media_file_cover_image\n ON media_file_cover_image.token = m.maybe_cover_image_media_file_token\nWHERE\n m.maybe_creator_user_token = ?\n AND m.media_class = 'project'\n AND m.maybe_project_type IS NOT NULL\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n AND NOT m.is_intermediate_system_file\n AND m.id < ?\nORDER BY m.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id: u64",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 1,
"name": "token: tokens::tokens::media_files::MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "media_class: enums::by_table::media_files::media_file_class::MediaFileClass",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 3,
"name": "project_type!: enums::by_table::media_files::media_file_project_type::MediaFileProjectType",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 4,
"name": "media_type: enums::by_table::media_files::media_file_type::MediaFileType",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 5,
"name": "public_bucket_directory_hash",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 6,
"name": "maybe_public_bucket_prefix",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 7,
"name": "maybe_public_bucket_extension",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 8,
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 9,
"name": "maybe_creator_username",
"type_info": {
"type": "VarString",
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 10,
"name": "maybe_creator_display_name",
"type_info": {
"type": "VarString",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 11,
"name": "maybe_creator_gravatar_hash",
"type_info": {
"type": "String",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 12,
"name": "maybe_file_cover_image_public_bucket_hash",
"type_info": {
"type": "VarString",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 13,
"name": "maybe_file_cover_image_public_bucket_prefix",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 14,
"name": "maybe_file_cover_image_public_bucket_extension",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 15,
"name": "creator_set_visibility: enums::common::visibility::Visibility",
"type_info": {
"type": "String",
"flags": "NOT_NULL | BINARY | ENUM",
"char_set": 224,
"max_size": 28
}
},
{
"ordinal": 16,
"name": "maybe_title",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 17,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 18,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 3
},
"nullable": [
false,
false,
false,
true,
false,
false,
true,
true,
true,
true,
true,
true,
true,
true,
true,
false,
true,
false,
false
]
},
"hash": "97e4bc3ab037126e3f4e213cdb7535c2ad233809fbeb6607c34fb02ec45cfe47"
}
@@ -1,12 +0,0 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO media_files\nSET\n token = ?,\n\n media_class = ?,\n media_type = ?,\n maybe_project_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": 28
},
"nullable": []
},
"hash": "a25fe977f51329ed078e2441446ef7ff16683dcad8b81c8d0a0a4540b5e985bc"
}
@@ -1,12 +0,0 @@
{
"db_name": "MySQL",
"query": "\nUPDATE media_files\nSET\n media_class = ?,\n media_type = ?,\n maybe_project_type = ?,\n\n public_bucket_directory_hash = ?,\n maybe_public_bucket_prefix = ?,\n maybe_public_bucket_extension = ?,\n\n maybe_mime_type = ?,\n file_size_bytes = ?,\n checksum_sha2 = ?,\n\n creator_ip_address = ?\nWHERE token = ?\nLIMIT 1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 11
},
"nullable": []
},
"hash": "a3a86db1b77af3973bb37751d797a7dc5ed34b0f0fa156845e3b4b805e397f9d"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\n UPDATE media_files\n SET\n public_bucket_directory_hash = ?,\n maybe_public_bucket_prefix = ?,\n maybe_public_bucket_extension = ?,\n\n maybe_mime_type = ?,\n file_size_bytes = ?,\n\n creator_ip_address = ?\n WHERE token = ?\n LIMIT 1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 7
},
"nullable": []
},
"hash": "ad311b4aa95a71b1dc37aa09c27850a0e963380c6e7780f47a9fc5a95ab70583"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO tags\nSET\n token = ?,\n tag_value = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "c7e848e4c906a34ac5a4ad8cc73354baa8d5cd81170dd06d66d7f414505d538b"
}
@@ -1,223 +0,0 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n m.id as `id: u64`,\n m.token as `token: tokens::tokens::media_files::MediaFileToken`,\n\n m.media_class as `media_class: enums::by_table::media_files::media_file_class::MediaFileClass`,\n m.maybe_project_type as `project_type!: enums::by_table::media_files::media_file_project_type::MediaFileProjectType`,\n m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\n\n media_file_cover_image.public_bucket_directory_hash as maybe_file_cover_image_public_bucket_hash,\n media_file_cover_image.maybe_public_bucket_prefix as maybe_file_cover_image_public_bucket_prefix,\n media_file_cover_image.maybe_public_bucket_extension as maybe_file_cover_image_public_bucket_extension,\n\n m.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.maybe_title,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m FORCE INDEX (fk_maybe_creator_user_token)\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\nLEFT OUTER JOIN media_files as media_file_cover_image\n ON media_file_cover_image.token = m.maybe_cover_image_media_file_token\nWHERE\n m.maybe_creator_user_token = ?\n AND m.media_class = 'project'\n AND m.maybe_project_type IS NOT NULL\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n AND NOT m.is_intermediate_system_file\n AND m.id > ?\nORDER BY m.id ASC\nLIMIT ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id: u64",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 1,
"name": "token: tokens::tokens::media_files::MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "media_class: enums::by_table::media_files::media_file_class::MediaFileClass",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 3,
"name": "project_type!: enums::by_table::media_files::media_file_project_type::MediaFileProjectType",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 4,
"name": "media_type: enums::by_table::media_files::media_file_type::MediaFileType",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 5,
"name": "public_bucket_directory_hash",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 6,
"name": "maybe_public_bucket_prefix",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 7,
"name": "maybe_public_bucket_extension",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 8,
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 9,
"name": "maybe_creator_username",
"type_info": {
"type": "VarString",
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 10,
"name": "maybe_creator_display_name",
"type_info": {
"type": "VarString",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 11,
"name": "maybe_creator_gravatar_hash",
"type_info": {
"type": "String",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 12,
"name": "maybe_file_cover_image_public_bucket_hash",
"type_info": {
"type": "VarString",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 13,
"name": "maybe_file_cover_image_public_bucket_prefix",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 14,
"name": "maybe_file_cover_image_public_bucket_extension",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 15,
"name": "creator_set_visibility: enums::common::visibility::Visibility",
"type_info": {
"type": "String",
"flags": "NOT_NULL | BINARY | ENUM",
"char_set": 224,
"max_size": 28
}
},
{
"ordinal": 16,
"name": "maybe_title",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 17,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 18,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 3
},
"nullable": [
false,
false,
false,
true,
false,
false,
true,
true,
true,
true,
true,
true,
true,
true,
true,
false,
true,
false,
false
]
},
"hash": "d8f4ce4e1026c71a7be8fd87f00f6f8858c401701da1ca3316eed1ed971d4bb2"
}
@@ -1,12 +0,0 @@
{
"db_name": "MySQL",
"query": "\n UPDATE media_files\n SET\n media_class = ?,\n maybe_project_type = ?,\n\n public_bucket_directory_hash = ?,\n maybe_public_bucket_prefix = ?,\n maybe_public_bucket_extension = ?,\n\n maybe_mime_type = ?,\n file_size_bytes = ?,\n\n creator_ip_address = ?\n WHERE token = ?\n LIMIT 1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 9
},
"nullable": []
},
"hash": "e33fa6280a39fff22b2e8c2731b7b6058b4705b5b557f34dba52a6440c22529b"
}
@@ -1,7 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE media_files
DROP INDEX index_maybe_project_type,
DROP COLUMN maybe_project_type;
@@ -1,16 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- Which kind of internal Artcraft project document this media file is
-- ('scene_3d', 'mood_board', 'workflow', 'video_timeline', ...).
-- Only set when media_class = 'project'. The file format lives in media_type;
-- the JSON payload carries its own schema version.
ALTER TABLE media_files
ADD COLUMN maybe_project_type VARCHAR(32) DEFAULT NULL
AFTER media_class;
-- For "list my mood boards"-style filtered queries.
-- The column is all-NULL at creation time, so the index build is cheap.
ALTER TABLE media_files
ADD INDEX index_maybe_project_type (maybe_project_type);
@@ -157,20 +157,11 @@ CREATE TABLE media_files (
-- This is especially helpful with engine types that map to specific semantics.
-- * 'unknown' (TODO: This is the default until all records are backfilled.)
-- * 'audio' for wav, mp3, etc.
-- * 'image' for a variety of image types.
-- * 'image' for a variety of video types.
-- * 'video' for a variety of video types.
-- * 'dimensional' (DEPRECATED) split into 'mesh', 'splat', and 'project'; read-only until backfilled
-- * 'mesh' for 3d mesh assets (glb, gltf, fbx, obj, pmx, pmd, etc.)
-- * 'splat' for Gaussian splats (spz, etc.)
-- * 'project' for internal Artcraft project documents (see maybe_project_type)
-- * 'dimensional' for a variety of 3d types
media_class VARCHAR(16) NOT NULL DEFAULT "unknown",
-- Which kind of internal Artcraft project document this media file is
-- ('scene_3d', 'mood_board', 'workflow', 'video_timeline', ...).
-- Only set when media_class = 'project'. The file format lives in media_type;
-- the JSON payload carries its own schema version.
maybe_project_type VARCHAR(32) DEFAULT NULL,
-- The file's mime type.
maybe_mime_type VARCHAR(32) DEFAULT NULL,
@@ -332,7 +323,6 @@ CREATE TABLE media_files (
KEY index_maybe_scene_source_media_file_token (maybe_scene_source_media_file_token),
KEY index_media_type (media_type),
KEY index_media_class (media_class),
KEY index_maybe_project_type (maybe_project_type),
KEY index_maybe_engine_category (maybe_engine_category),
KEY fk_maybe_prompt_token (maybe_prompt_token),
KEY index_checksum_sha2 (checksum_sha2),
@@ -0,0 +1,267 @@
{
"title": "[GET] /v1/engine/scene/{token}",
"description": "Auto-generated per-endpoint dashboard for `GET /v1/engine/scene/{token}`.\n\nFilters all metrics on `service:storyteller-web,route:/v1/engine/scene/_token,method:get`.\n\nSource: `script/metrics/generate_endpoint_dashboards.py`. Do not hand-edit; changes will be overwritten on the next regeneration.",
"layout_type": "ordered",
"notify_list": [],
"reflow_type": "fixed",
"widgets": [
{
"definition": {
"type": "group",
"layout_type": "ordered",
"title": "Top-line health",
"show_title": true,
"widgets": [
{
"definition": {
"type": "query_value",
"title": "Requests/sec",
"title_size": "16",
"title_align": "left",
"precision": 2,
"autoscale": true,
"requests": [
{
"q": "sum:http.server.request.count{service:storyteller-web,route:/v1/engine/scene/_token,method:get}.as_rate()",
"aggregator": "avg"
}
]
},
"layout": {
"x": 0,
"y": 0,
"width": 3,
"height": 2
}
},
{
"definition": {
"type": "query_value",
"title": "5xx error rate (%)",
"title_size": "16",
"title_align": "left",
"precision": 2,
"autoscale": false,
"custom_unit": "%",
"requests": [
{
"q": "(sum:http.server.request.count{service:storyteller-web,route:/v1/engine/scene/_token,method:get,status_class:5xx}.as_count() / sum:http.server.request.count{service:storyteller-web,route:/v1/engine/scene/_token,method:get}.as_count()) * 100",
"aggregator": "avg",
"conditional_formats": [
{
"comparator": ">",
"value": 5.0,
"palette": "white_on_red"
},
{
"comparator": ">",
"value": 1.0,
"palette": "white_on_yellow"
},
{
"comparator": "<=",
"value": 1.0,
"palette": "white_on_green"
}
]
}
]
},
"layout": {
"x": 3,
"y": 0,
"width": 3,
"height": 2
}
},
{
"definition": {
"type": "query_value",
"title": "Latency p95 (ms)",
"title_size": "16",
"title_align": "left",
"precision": 0,
"autoscale": true,
"custom_unit": "ms",
"requests": [
{
"q": "p95:http.server.request.duration_ms{service:storyteller-web,route:/v1/engine/scene/_token,method:get}",
"aggregator": "avg",
"conditional_formats": [
{
"comparator": ">",
"value": 1000,
"palette": "white_on_red"
},
{
"comparator": ">",
"value": 250,
"palette": "white_on_yellow"
},
{
"comparator": "<=",
"value": 250,
"palette": "white_on_green"
}
]
}
]
},
"layout": {
"x": 6,
"y": 0,
"width": 3,
"height": 2
}
},
{
"definition": {
"type": "query_value",
"title": "Latency p99 (ms)",
"title_size": "16",
"title_align": "left",
"precision": 0,
"autoscale": true,
"custom_unit": "ms",
"requests": [
{
"q": "p99:http.server.request.duration_ms{service:storyteller-web,route:/v1/engine/scene/_token,method:get}",
"aggregator": "avg"
}
]
},
"layout": {
"x": 9,
"y": 0,
"width": 3,
"height": 2
}
}
]
},
"layout": {
"x": 0,
"y": 0,
"width": 12,
"height": 3
}
},
{
"definition": {
"type": "group",
"layout_type": "ordered",
"title": "Trends",
"show_title": true,
"widgets": [
{
"definition": {
"type": "timeseries",
"title": "Requests/sec by status class",
"title_size": "16",
"title_align": "left",
"show_legend": true,
"legend_layout": "horizontal",
"requests": [
{
"q": "sum:http.server.request.count{service:storyteller-web,route:/v1/engine/scene/_token,method:get} by {status_class}.as_rate()",
"display_type": "bars",
"style": {
"palette": "semantic",
"line_type": "solid",
"line_width": "normal"
}
}
],
"yaxis": {
"scale": "linear",
"include_zero": true
}
},
"layout": {
"x": 0,
"y": 0,
"width": 6,
"height": 4
}
},
{
"definition": {
"type": "timeseries",
"title": "Latency percentiles",
"title_size": "16",
"title_align": "left",
"show_legend": true,
"legend_layout": "horizontal",
"requests": [
{
"q": "p50:http.server.request.duration_ms{service:storyteller-web,route:/v1/engine/scene/_token,method:get}",
"display_type": "line",
"style": {
"palette": "cool",
"line_type": "solid",
"line_width": "normal"
}
},
{
"q": "p95:http.server.request.duration_ms{service:storyteller-web,route:/v1/engine/scene/_token,method:get}",
"display_type": "line",
"style": {
"palette": "warm",
"line_type": "solid",
"line_width": "normal"
}
},
{
"q": "p99:http.server.request.duration_ms{service:storyteller-web,route:/v1/engine/scene/_token,method:get}",
"display_type": "line",
"style": {
"palette": "warm",
"line_type": "dashed",
"line_width": "normal"
}
}
],
"yaxis": {
"scale": "linear",
"include_zero": true,
"label": "ms"
}
},
"layout": {
"x": 6,
"y": 0,
"width": 6,
"height": 4
}
}
]
},
"layout": {
"x": 0,
"y": 3,
"width": 12,
"height": 5
}
},
{
"definition": {
"type": "toplist",
"title": "Status code distribution",
"title_size": "16",
"title_align": "left",
"requests": [
{
"q": "top(sum:http.server.request.count{service:storyteller-web,route:/v1/engine/scene/_token,method:get} by {status_code}.as_count(), 25, 'sum', 'desc')"
}
]
},
"layout": {
"x": 0,
"y": 8,
"width": 12,
"height": 4
}
}
],
"id": "24m-c4g-btm"
}
@@ -0,0 +1,267 @@
{
"title": "[POST] /v1/engine/create_scene",
"description": "Auto-generated per-endpoint dashboard for `POST /v1/engine/create_scene`.\n\nFilters all metrics on `service:storyteller-web,route:/v1/engine/create_scene,method:post`.\n\nSource: `script/metrics/generate_endpoint_dashboards.py`. Do not hand-edit; changes will be overwritten on the next regeneration.",
"layout_type": "ordered",
"notify_list": [],
"reflow_type": "fixed",
"widgets": [
{
"definition": {
"type": "group",
"layout_type": "ordered",
"title": "Top-line health",
"show_title": true,
"widgets": [
{
"definition": {
"type": "query_value",
"title": "Requests/sec",
"title_size": "16",
"title_align": "left",
"precision": 2,
"autoscale": true,
"requests": [
{
"q": "sum:http.server.request.count{service:storyteller-web,route:/v1/engine/create_scene,method:post}.as_rate()",
"aggregator": "avg"
}
]
},
"layout": {
"x": 0,
"y": 0,
"width": 3,
"height": 2
}
},
{
"definition": {
"type": "query_value",
"title": "5xx error rate (%)",
"title_size": "16",
"title_align": "left",
"precision": 2,
"autoscale": false,
"custom_unit": "%",
"requests": [
{
"q": "(sum:http.server.request.count{service:storyteller-web,route:/v1/engine/create_scene,method:post,status_class:5xx}.as_count() / sum:http.server.request.count{service:storyteller-web,route:/v1/engine/create_scene,method:post}.as_count()) * 100",
"aggregator": "avg",
"conditional_formats": [
{
"comparator": ">",
"value": 5.0,
"palette": "white_on_red"
},
{
"comparator": ">",
"value": 1.0,
"palette": "white_on_yellow"
},
{
"comparator": "<=",
"value": 1.0,
"palette": "white_on_green"
}
]
}
]
},
"layout": {
"x": 3,
"y": 0,
"width": 3,
"height": 2
}
},
{
"definition": {
"type": "query_value",
"title": "Latency p95 (ms)",
"title_size": "16",
"title_align": "left",
"precision": 0,
"autoscale": true,
"custom_unit": "ms",
"requests": [
{
"q": "p95:http.server.request.duration_ms{service:storyteller-web,route:/v1/engine/create_scene,method:post}",
"aggregator": "avg",
"conditional_formats": [
{
"comparator": ">",
"value": 1000,
"palette": "white_on_red"
},
{
"comparator": ">",
"value": 250,
"palette": "white_on_yellow"
},
{
"comparator": "<=",
"value": 250,
"palette": "white_on_green"
}
]
}
]
},
"layout": {
"x": 6,
"y": 0,
"width": 3,
"height": 2
}
},
{
"definition": {
"type": "query_value",
"title": "Latency p99 (ms)",
"title_size": "16",
"title_align": "left",
"precision": 0,
"autoscale": true,
"custom_unit": "ms",
"requests": [
{
"q": "p99:http.server.request.duration_ms{service:storyteller-web,route:/v1/engine/create_scene,method:post}",
"aggregator": "avg"
}
]
},
"layout": {
"x": 9,
"y": 0,
"width": 3,
"height": 2
}
}
]
},
"layout": {
"x": 0,
"y": 0,
"width": 12,
"height": 3
}
},
{
"definition": {
"type": "group",
"layout_type": "ordered",
"title": "Trends",
"show_title": true,
"widgets": [
{
"definition": {
"type": "timeseries",
"title": "Requests/sec by status class",
"title_size": "16",
"title_align": "left",
"show_legend": true,
"legend_layout": "horizontal",
"requests": [
{
"q": "sum:http.server.request.count{service:storyteller-web,route:/v1/engine/create_scene,method:post} by {status_class}.as_rate()",
"display_type": "bars",
"style": {
"palette": "semantic",
"line_type": "solid",
"line_width": "normal"
}
}
],
"yaxis": {
"scale": "linear",
"include_zero": true
}
},
"layout": {
"x": 0,
"y": 0,
"width": 6,
"height": 4
}
},
{
"definition": {
"type": "timeseries",
"title": "Latency percentiles",
"title_size": "16",
"title_align": "left",
"show_legend": true,
"legend_layout": "horizontal",
"requests": [
{
"q": "p50:http.server.request.duration_ms{service:storyteller-web,route:/v1/engine/create_scene,method:post}",
"display_type": "line",
"style": {
"palette": "cool",
"line_type": "solid",
"line_width": "normal"
}
},
{
"q": "p95:http.server.request.duration_ms{service:storyteller-web,route:/v1/engine/create_scene,method:post}",
"display_type": "line",
"style": {
"palette": "warm",
"line_type": "solid",
"line_width": "normal"
}
},
{
"q": "p99:http.server.request.duration_ms{service:storyteller-web,route:/v1/engine/create_scene,method:post}",
"display_type": "line",
"style": {
"palette": "warm",
"line_type": "dashed",
"line_width": "normal"
}
}
],
"yaxis": {
"scale": "linear",
"include_zero": true,
"label": "ms"
}
},
"layout": {
"x": 6,
"y": 0,
"width": 6,
"height": 4
}
}
]
},
"layout": {
"x": 0,
"y": 3,
"width": 12,
"height": 5
}
},
{
"definition": {
"type": "toplist",
"title": "Status code distribution",
"title_size": "16",
"title_align": "left",
"requests": [
{
"q": "top(sum:http.server.request.count{service:storyteller-web,route:/v1/engine/create_scene,method:post} by {status_code}.as_count(), 25, 'sum', 'desc')"
}
]
},
"layout": {
"x": 0,
"y": 8,
"width": 12,
"height": 4
}
}
],
"id": "z9c-89r-p57"
}
@@ -0,0 +1,267 @@
{
"title": "[POST] /v1/engine/scene/{token}",
"description": "Auto-generated per-endpoint dashboard for `POST /v1/engine/scene/{token}`.\n\nFilters all metrics on `service:storyteller-web,route:/v1/engine/scene/_token,method:post`.\n\nSource: `script/metrics/generate_endpoint_dashboards.py`. Do not hand-edit; changes will be overwritten on the next regeneration.",
"layout_type": "ordered",
"notify_list": [],
"reflow_type": "fixed",
"widgets": [
{
"definition": {
"type": "group",
"layout_type": "ordered",
"title": "Top-line health",
"show_title": true,
"widgets": [
{
"definition": {
"type": "query_value",
"title": "Requests/sec",
"title_size": "16",
"title_align": "left",
"precision": 2,
"autoscale": true,
"requests": [
{
"q": "sum:http.server.request.count{service:storyteller-web,route:/v1/engine/scene/_token,method:post}.as_rate()",
"aggregator": "avg"
}
]
},
"layout": {
"x": 0,
"y": 0,
"width": 3,
"height": 2
}
},
{
"definition": {
"type": "query_value",
"title": "5xx error rate (%)",
"title_size": "16",
"title_align": "left",
"precision": 2,
"autoscale": false,
"custom_unit": "%",
"requests": [
{
"q": "(sum:http.server.request.count{service:storyteller-web,route:/v1/engine/scene/_token,method:post,status_class:5xx}.as_count() / sum:http.server.request.count{service:storyteller-web,route:/v1/engine/scene/_token,method:post}.as_count()) * 100",
"aggregator": "avg",
"conditional_formats": [
{
"comparator": ">",
"value": 5.0,
"palette": "white_on_red"
},
{
"comparator": ">",
"value": 1.0,
"palette": "white_on_yellow"
},
{
"comparator": "<=",
"value": 1.0,
"palette": "white_on_green"
}
]
}
]
},
"layout": {
"x": 3,
"y": 0,
"width": 3,
"height": 2
}
},
{
"definition": {
"type": "query_value",
"title": "Latency p95 (ms)",
"title_size": "16",
"title_align": "left",
"precision": 0,
"autoscale": true,
"custom_unit": "ms",
"requests": [
{
"q": "p95:http.server.request.duration_ms{service:storyteller-web,route:/v1/engine/scene/_token,method:post}",
"aggregator": "avg",
"conditional_formats": [
{
"comparator": ">",
"value": 1000,
"palette": "white_on_red"
},
{
"comparator": ">",
"value": 250,
"palette": "white_on_yellow"
},
{
"comparator": "<=",
"value": 250,
"palette": "white_on_green"
}
]
}
]
},
"layout": {
"x": 6,
"y": 0,
"width": 3,
"height": 2
}
},
{
"definition": {
"type": "query_value",
"title": "Latency p99 (ms)",
"title_size": "16",
"title_align": "left",
"precision": 0,
"autoscale": true,
"custom_unit": "ms",
"requests": [
{
"q": "p99:http.server.request.duration_ms{service:storyteller-web,route:/v1/engine/scene/_token,method:post}",
"aggregator": "avg"
}
]
},
"layout": {
"x": 9,
"y": 0,
"width": 3,
"height": 2
}
}
]
},
"layout": {
"x": 0,
"y": 0,
"width": 12,
"height": 3
}
},
{
"definition": {
"type": "group",
"layout_type": "ordered",
"title": "Trends",
"show_title": true,
"widgets": [
{
"definition": {
"type": "timeseries",
"title": "Requests/sec by status class",
"title_size": "16",
"title_align": "left",
"show_legend": true,
"legend_layout": "horizontal",
"requests": [
{
"q": "sum:http.server.request.count{service:storyteller-web,route:/v1/engine/scene/_token,method:post} by {status_class}.as_rate()",
"display_type": "bars",
"style": {
"palette": "semantic",
"line_type": "solid",
"line_width": "normal"
}
}
],
"yaxis": {
"scale": "linear",
"include_zero": true
}
},
"layout": {
"x": 0,
"y": 0,
"width": 6,
"height": 4
}
},
{
"definition": {
"type": "timeseries",
"title": "Latency percentiles",
"title_size": "16",
"title_align": "left",
"show_legend": true,
"legend_layout": "horizontal",
"requests": [
{
"q": "p50:http.server.request.duration_ms{service:storyteller-web,route:/v1/engine/scene/_token,method:post}",
"display_type": "line",
"style": {
"palette": "cool",
"line_type": "solid",
"line_width": "normal"
}
},
{
"q": "p95:http.server.request.duration_ms{service:storyteller-web,route:/v1/engine/scene/_token,method:post}",
"display_type": "line",
"style": {
"palette": "warm",
"line_type": "solid",
"line_width": "normal"
}
},
{
"q": "p99:http.server.request.duration_ms{service:storyteller-web,route:/v1/engine/scene/_token,method:post}",
"display_type": "line",
"style": {
"palette": "warm",
"line_type": "dashed",
"line_width": "normal"
}
}
],
"yaxis": {
"scale": "linear",
"include_zero": true,
"label": "ms"
}
},
"layout": {
"x": 6,
"y": 0,
"width": 6,
"height": 4
}
}
]
},
"layout": {
"x": 0,
"y": 3,
"width": 12,
"height": 5
}
},
{
"definition": {
"type": "toplist",
"title": "Status code distribution",
"title_size": "16",
"title_align": "left",
"requests": [
{
"q": "top(sum:http.server.request.count{service:storyteller-web,route:/v1/engine/scene/_token,method:post} by {status_code}.as_count(), 25, 'sum', 'desc')"
}
]
},
"layout": {
"x": 0,
"y": 8,
"width": 12,
"height": 4
}
}
],
"id": "bxf-r4f-xfs"
}
@@ -0,0 +1,267 @@
{
"title": "[POST] /v1/media_files/upload/engine_asset",
"description": "Auto-generated per-endpoint dashboard for `POST /v1/media_files/upload/engine_asset`.\n\nFilters all metrics on `service:storyteller-web,route:/v1/media_files/upload/engine_asset,method:post`.\n\nSource: `script/metrics/generate_endpoint_dashboards.py`. Do not hand-edit; changes will be overwritten on the next regeneration.",
"layout_type": "ordered",
"notify_list": [],
"reflow_type": "fixed",
"widgets": [
{
"definition": {
"type": "group",
"layout_type": "ordered",
"title": "Top-line health",
"show_title": true,
"widgets": [
{
"definition": {
"type": "query_value",
"title": "Requests/sec",
"title_size": "16",
"title_align": "left",
"precision": 2,
"autoscale": true,
"requests": [
{
"q": "sum:http.server.request.count{service:storyteller-web,route:/v1/media_files/upload/engine_asset,method:post}.as_rate()",
"aggregator": "avg"
}
]
},
"layout": {
"x": 0,
"y": 0,
"width": 3,
"height": 2
}
},
{
"definition": {
"type": "query_value",
"title": "5xx error rate (%)",
"title_size": "16",
"title_align": "left",
"precision": 2,
"autoscale": false,
"custom_unit": "%",
"requests": [
{
"q": "(sum:http.server.request.count{service:storyteller-web,route:/v1/media_files/upload/engine_asset,method:post,status_class:5xx}.as_count() / sum:http.server.request.count{service:storyteller-web,route:/v1/media_files/upload/engine_asset,method:post}.as_count()) * 100",
"aggregator": "avg",
"conditional_formats": [
{
"comparator": ">",
"value": 5.0,
"palette": "white_on_red"
},
{
"comparator": ">",
"value": 1.0,
"palette": "white_on_yellow"
},
{
"comparator": "<=",
"value": 1.0,
"palette": "white_on_green"
}
]
}
]
},
"layout": {
"x": 3,
"y": 0,
"width": 3,
"height": 2
}
},
{
"definition": {
"type": "query_value",
"title": "Latency p95 (ms)",
"title_size": "16",
"title_align": "left",
"precision": 0,
"autoscale": true,
"custom_unit": "ms",
"requests": [
{
"q": "p95:http.server.request.duration_ms{service:storyteller-web,route:/v1/media_files/upload/engine_asset,method:post}",
"aggregator": "avg",
"conditional_formats": [
{
"comparator": ">",
"value": 1000,
"palette": "white_on_red"
},
{
"comparator": ">",
"value": 250,
"palette": "white_on_yellow"
},
{
"comparator": "<=",
"value": 250,
"palette": "white_on_green"
}
]
}
]
},
"layout": {
"x": 6,
"y": 0,
"width": 3,
"height": 2
}
},
{
"definition": {
"type": "query_value",
"title": "Latency p99 (ms)",
"title_size": "16",
"title_align": "left",
"precision": 0,
"autoscale": true,
"custom_unit": "ms",
"requests": [
{
"q": "p99:http.server.request.duration_ms{service:storyteller-web,route:/v1/media_files/upload/engine_asset,method:post}",
"aggregator": "avg"
}
]
},
"layout": {
"x": 9,
"y": 0,
"width": 3,
"height": 2
}
}
]
},
"layout": {
"x": 0,
"y": 0,
"width": 12,
"height": 3
}
},
{
"definition": {
"type": "group",
"layout_type": "ordered",
"title": "Trends",
"show_title": true,
"widgets": [
{
"definition": {
"type": "timeseries",
"title": "Requests/sec by status class",
"title_size": "16",
"title_align": "left",
"show_legend": true,
"legend_layout": "horizontal",
"requests": [
{
"q": "sum:http.server.request.count{service:storyteller-web,route:/v1/media_files/upload/engine_asset,method:post} by {status_class}.as_rate()",
"display_type": "bars",
"style": {
"palette": "semantic",
"line_type": "solid",
"line_width": "normal"
}
}
],
"yaxis": {
"scale": "linear",
"include_zero": true
}
},
"layout": {
"x": 0,
"y": 0,
"width": 6,
"height": 4
}
},
{
"definition": {
"type": "timeseries",
"title": "Latency percentiles",
"title_size": "16",
"title_align": "left",
"show_legend": true,
"legend_layout": "horizontal",
"requests": [
{
"q": "p50:http.server.request.duration_ms{service:storyteller-web,route:/v1/media_files/upload/engine_asset,method:post}",
"display_type": "line",
"style": {
"palette": "cool",
"line_type": "solid",
"line_width": "normal"
}
},
{
"q": "p95:http.server.request.duration_ms{service:storyteller-web,route:/v1/media_files/upload/engine_asset,method:post}",
"display_type": "line",
"style": {
"palette": "warm",
"line_type": "solid",
"line_width": "normal"
}
},
{
"q": "p99:http.server.request.duration_ms{service:storyteller-web,route:/v1/media_files/upload/engine_asset,method:post}",
"display_type": "line",
"style": {
"palette": "warm",
"line_type": "dashed",
"line_width": "normal"
}
}
],
"yaxis": {
"scale": "linear",
"include_zero": true,
"label": "ms"
}
},
"layout": {
"x": 6,
"y": 0,
"width": 6,
"height": 4
}
}
]
},
"layout": {
"x": 0,
"y": 3,
"width": 12,
"height": 5
}
},
{
"definition": {
"type": "toplist",
"title": "Status code distribution",
"title_size": "16",
"title_align": "left",
"requests": [
{
"q": "top(sum:http.server.request.count{service:storyteller-web,route:/v1/media_files/upload/engine_asset,method:post} by {status_code}.as_count(), 25, 'sum', 'desc')"
}
]
},
"layout": {
"x": 0,
"y": 8,
"width": 12,
"height": 4
}
}
],
"id": "7nm-j6w-9tz"
}
@@ -0,0 +1,267 @@
{
"title": "[POST] /v1/media_files/write/engine_asset",
"description": "Auto-generated per-endpoint dashboard for `POST /v1/media_files/write/engine_asset`.\n\nFilters all metrics on `service:storyteller-web,route:/v1/media_files/write/engine_asset,method:post`.\n\nSource: `script/metrics/generate_endpoint_dashboards.py`. Do not hand-edit; changes will be overwritten on the next regeneration.",
"layout_type": "ordered",
"notify_list": [],
"reflow_type": "fixed",
"widgets": [
{
"definition": {
"type": "group",
"layout_type": "ordered",
"title": "Top-line health",
"show_title": true,
"widgets": [
{
"definition": {
"type": "query_value",
"title": "Requests/sec",
"title_size": "16",
"title_align": "left",
"precision": 2,
"autoscale": true,
"requests": [
{
"q": "sum:http.server.request.count{service:storyteller-web,route:/v1/media_files/write/engine_asset,method:post}.as_rate()",
"aggregator": "avg"
}
]
},
"layout": {
"x": 0,
"y": 0,
"width": 3,
"height": 2
}
},
{
"definition": {
"type": "query_value",
"title": "5xx error rate (%)",
"title_size": "16",
"title_align": "left",
"precision": 2,
"autoscale": false,
"custom_unit": "%",
"requests": [
{
"q": "(sum:http.server.request.count{service:storyteller-web,route:/v1/media_files/write/engine_asset,method:post,status_class:5xx}.as_count() / sum:http.server.request.count{service:storyteller-web,route:/v1/media_files/write/engine_asset,method:post}.as_count()) * 100",
"aggregator": "avg",
"conditional_formats": [
{
"comparator": ">",
"value": 5.0,
"palette": "white_on_red"
},
{
"comparator": ">",
"value": 1.0,
"palette": "white_on_yellow"
},
{
"comparator": "<=",
"value": 1.0,
"palette": "white_on_green"
}
]
}
]
},
"layout": {
"x": 3,
"y": 0,
"width": 3,
"height": 2
}
},
{
"definition": {
"type": "query_value",
"title": "Latency p95 (ms)",
"title_size": "16",
"title_align": "left",
"precision": 0,
"autoscale": true,
"custom_unit": "ms",
"requests": [
{
"q": "p95:http.server.request.duration_ms{service:storyteller-web,route:/v1/media_files/write/engine_asset,method:post}",
"aggregator": "avg",
"conditional_formats": [
{
"comparator": ">",
"value": 1000,
"palette": "white_on_red"
},
{
"comparator": ">",
"value": 250,
"palette": "white_on_yellow"
},
{
"comparator": "<=",
"value": 250,
"palette": "white_on_green"
}
]
}
]
},
"layout": {
"x": 6,
"y": 0,
"width": 3,
"height": 2
}
},
{
"definition": {
"type": "query_value",
"title": "Latency p99 (ms)",
"title_size": "16",
"title_align": "left",
"precision": 0,
"autoscale": true,
"custom_unit": "ms",
"requests": [
{
"q": "p99:http.server.request.duration_ms{service:storyteller-web,route:/v1/media_files/write/engine_asset,method:post}",
"aggregator": "avg"
}
]
},
"layout": {
"x": 9,
"y": 0,
"width": 3,
"height": 2
}
}
]
},
"layout": {
"x": 0,
"y": 0,
"width": 12,
"height": 3
}
},
{
"definition": {
"type": "group",
"layout_type": "ordered",
"title": "Trends",
"show_title": true,
"widgets": [
{
"definition": {
"type": "timeseries",
"title": "Requests/sec by status class",
"title_size": "16",
"title_align": "left",
"show_legend": true,
"legend_layout": "horizontal",
"requests": [
{
"q": "sum:http.server.request.count{service:storyteller-web,route:/v1/media_files/write/engine_asset,method:post} by {status_class}.as_rate()",
"display_type": "bars",
"style": {
"palette": "semantic",
"line_type": "solid",
"line_width": "normal"
}
}
],
"yaxis": {
"scale": "linear",
"include_zero": true
}
},
"layout": {
"x": 0,
"y": 0,
"width": 6,
"height": 4
}
},
{
"definition": {
"type": "timeseries",
"title": "Latency percentiles",
"title_size": "16",
"title_align": "left",
"show_legend": true,
"legend_layout": "horizontal",
"requests": [
{
"q": "p50:http.server.request.duration_ms{service:storyteller-web,route:/v1/media_files/write/engine_asset,method:post}",
"display_type": "line",
"style": {
"palette": "cool",
"line_type": "solid",
"line_width": "normal"
}
},
{
"q": "p95:http.server.request.duration_ms{service:storyteller-web,route:/v1/media_files/write/engine_asset,method:post}",
"display_type": "line",
"style": {
"palette": "warm",
"line_type": "solid",
"line_width": "normal"
}
},
{
"q": "p99:http.server.request.duration_ms{service:storyteller-web,route:/v1/media_files/write/engine_asset,method:post}",
"display_type": "line",
"style": {
"palette": "warm",
"line_type": "dashed",
"line_width": "normal"
}
}
],
"yaxis": {
"scale": "linear",
"include_zero": true,
"label": "ms"
}
},
"layout": {
"x": 6,
"y": 0,
"width": 6,
"height": 4
}
}
]
},
"layout": {
"x": 0,
"y": 3,
"width": 12,
"height": 5
}
},
{
"definition": {
"type": "toplist",
"title": "Status code distribution",
"title_size": "16",
"title_align": "left",
"requests": [
{
"q": "top(sum:http.server.request.count{service:storyteller-web,route:/v1/media_files/write/engine_asset,method:post} by {status_code}.as_count(), 25, 'sum', 'desc')"
}
]
},
"layout": {
"x": 0,
"y": 8,
"width": 12,
"height": 4
}
}
],
"id": "xv7-2ca-2c5"
}
@@ -0,0 +1,267 @@
{
"title": "[POST] /v1/media_files/write/scene_file",
"description": "Auto-generated per-endpoint dashboard for `POST /v1/media_files/write/scene_file`.\n\nFilters all metrics on `service:storyteller-web,route:/v1/media_files/write/scene_file,method:post`.\n\nSource: `script/metrics/generate_endpoint_dashboards.py`. Do not hand-edit; changes will be overwritten on the next regeneration.",
"layout_type": "ordered",
"notify_list": [],
"reflow_type": "fixed",
"widgets": [
{
"definition": {
"type": "group",
"layout_type": "ordered",
"title": "Top-line health",
"show_title": true,
"widgets": [
{
"definition": {
"type": "query_value",
"title": "Requests/sec",
"title_size": "16",
"title_align": "left",
"precision": 2,
"autoscale": true,
"requests": [
{
"q": "sum:http.server.request.count{service:storyteller-web,route:/v1/media_files/write/scene_file,method:post}.as_rate()",
"aggregator": "avg"
}
]
},
"layout": {
"x": 0,
"y": 0,
"width": 3,
"height": 2
}
},
{
"definition": {
"type": "query_value",
"title": "5xx error rate (%)",
"title_size": "16",
"title_align": "left",
"precision": 2,
"autoscale": false,
"custom_unit": "%",
"requests": [
{
"q": "(sum:http.server.request.count{service:storyteller-web,route:/v1/media_files/write/scene_file,method:post,status_class:5xx}.as_count() / sum:http.server.request.count{service:storyteller-web,route:/v1/media_files/write/scene_file,method:post}.as_count()) * 100",
"aggregator": "avg",
"conditional_formats": [
{
"comparator": ">",
"value": 5.0,
"palette": "white_on_red"
},
{
"comparator": ">",
"value": 1.0,
"palette": "white_on_yellow"
},
{
"comparator": "<=",
"value": 1.0,
"palette": "white_on_green"
}
]
}
]
},
"layout": {
"x": 3,
"y": 0,
"width": 3,
"height": 2
}
},
{
"definition": {
"type": "query_value",
"title": "Latency p95 (ms)",
"title_size": "16",
"title_align": "left",
"precision": 0,
"autoscale": true,
"custom_unit": "ms",
"requests": [
{
"q": "p95:http.server.request.duration_ms{service:storyteller-web,route:/v1/media_files/write/scene_file,method:post}",
"aggregator": "avg",
"conditional_formats": [
{
"comparator": ">",
"value": 1000,
"palette": "white_on_red"
},
{
"comparator": ">",
"value": 250,
"palette": "white_on_yellow"
},
{
"comparator": "<=",
"value": 250,
"palette": "white_on_green"
}
]
}
]
},
"layout": {
"x": 6,
"y": 0,
"width": 3,
"height": 2
}
},
{
"definition": {
"type": "query_value",
"title": "Latency p99 (ms)",
"title_size": "16",
"title_align": "left",
"precision": 0,
"autoscale": true,
"custom_unit": "ms",
"requests": [
{
"q": "p99:http.server.request.duration_ms{service:storyteller-web,route:/v1/media_files/write/scene_file,method:post}",
"aggregator": "avg"
}
]
},
"layout": {
"x": 9,
"y": 0,
"width": 3,
"height": 2
}
}
]
},
"layout": {
"x": 0,
"y": 0,
"width": 12,
"height": 3
}
},
{
"definition": {
"type": "group",
"layout_type": "ordered",
"title": "Trends",
"show_title": true,
"widgets": [
{
"definition": {
"type": "timeseries",
"title": "Requests/sec by status class",
"title_size": "16",
"title_align": "left",
"show_legend": true,
"legend_layout": "horizontal",
"requests": [
{
"q": "sum:http.server.request.count{service:storyteller-web,route:/v1/media_files/write/scene_file,method:post} by {status_class}.as_rate()",
"display_type": "bars",
"style": {
"palette": "semantic",
"line_type": "solid",
"line_width": "normal"
}
}
],
"yaxis": {
"scale": "linear",
"include_zero": true
}
},
"layout": {
"x": 0,
"y": 0,
"width": 6,
"height": 4
}
},
{
"definition": {
"type": "timeseries",
"title": "Latency percentiles",
"title_size": "16",
"title_align": "left",
"show_legend": true,
"legend_layout": "horizontal",
"requests": [
{
"q": "p50:http.server.request.duration_ms{service:storyteller-web,route:/v1/media_files/write/scene_file,method:post}",
"display_type": "line",
"style": {
"palette": "cool",
"line_type": "solid",
"line_width": "normal"
}
},
{
"q": "p95:http.server.request.duration_ms{service:storyteller-web,route:/v1/media_files/write/scene_file,method:post}",
"display_type": "line",
"style": {
"palette": "warm",
"line_type": "solid",
"line_width": "normal"
}
},
{
"q": "p99:http.server.request.duration_ms{service:storyteller-web,route:/v1/media_files/write/scene_file,method:post}",
"display_type": "line",
"style": {
"palette": "warm",
"line_type": "dashed",
"line_width": "normal"
}
}
],
"yaxis": {
"scale": "linear",
"include_zero": true,
"label": "ms"
}
},
"layout": {
"x": 6,
"y": 0,
"width": 6,
"height": 4
}
}
]
},
"layout": {
"x": 0,
"y": 3,
"width": 12,
"height": 5
}
},
{
"definition": {
"type": "toplist",
"title": "Status code distribution",
"title_size": "16",
"title_align": "left",
"requests": [
{
"q": "top(sum:http.server.request.count{service:storyteller-web,route:/v1/media_files/write/scene_file,method:post} by {status_code}.as_count(), 25, 'sum', 'desc')"
}
]
},
"layout": {
"x": 0,
"y": 8,
"width": 12,
"height": 4
}
}
],
"id": "c8z-t2z-nk4"
}
@@ -0,0 +1,2 @@
$kind: collection
order: 8000
@@ -0,0 +1,15 @@
$kind: http-request
name: Edit Tags
url: "{{api_host}}/v1/tags/edit/model_weight/weight_84t9zeafn72wkje1x59f9rm81"
method: POST
headers:
Accept: application/json
Content-Type: application/json
Cookie: ""
body:
type: json
content: |-
{
"tags": "video games, legend of zelda"
}
order: 1000
@@ -0,0 +1,6 @@
$kind: http-request
url: "{{api_host}}/v1/tags/list/model_weight/weight_84t9zeafn72wkje1x59f9rm81"
method: GET
headers:
Accept: application/json
order: 2000
@@ -2,7 +2,6 @@ pub mod cover_image_links;
pub mod job_details;
pub mod media_file_cover_image_details;
pub mod media_links;
pub mod pagination_cursors;
pub mod simple_entity_stats;
pub mod simple_generic_json_success;
pub mod user_details_light;
@@ -1,22 +0,0 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
// TODO(bt,2026-07-09): Replace the storyteller-web version of this.
/// Pagination by cursors
/// These types of pagination are for "infinite scrolling", which do not reveal a number of pages.
/// This is good so that investors and competitors cannot reveal how many database records we have.
/// This is typically used for "discovery" type pages, not user profiles.
#[derive(Serialize, Deserialize, ToSchema, Debug)]
pub struct PaginationCursors {
/// The "next" cursor.
/// This is an opaque (typically even encrypted) handle.
pub maybe_next: Option<String>,
/// The "previous" cursor.
/// This is an opaque (typically even encrypted) handle.
pub maybe_previous: Option<String>,
/// Details whether we're walking forward or backward.
pub cursor_is_reversed: bool,
}
@@ -1,78 +0,0 @@
use chrono::{DateTime, Utc};
use serde_derive::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
use enums::by_table::media_files::media_file_class::MediaFileClass;
use enums::by_table::media_files::media_file_project_type::MediaFileProjectType;
use enums::by_table::media_files::media_file_type::MediaFileType;
use enums::common::visibility::Visibility;
use tokens::tokens::media_files::MediaFileToken;
use crate::common::responses::media_file_cover_image_details::MediaFileCoverImageDetails;
use crate::common::responses::media_links::MediaLinks;
use crate::common::responses::pagination_cursors::PaginationCursors;
use crate::common::responses::user_details_light::UserDetailsLight;
pub const LIST_SESSION_PROJECT_MEDIA_FILES_URL_PATH: &str = "/v1/media_files/project/list";
/// Query string parameters for listing the session user's project files.
#[derive(Deserialize, IntoParams)]
pub struct ListSessionProjectMediaFilesQueryParams {
pub sort_ascending: Option<bool>,
pub page_size: Option<usize>,
pub cursor: Option<String>,
pub cursor_is_reversed: Option<bool>,
/// Optional filter on the specific project document type.
///
/// Usage:
/// - `?filter_project_type=scene_3d`
/// - `?filter_project_type=mood_board`
/// - `?filter_project_type=workflow`
/// - `?filter_project_type=video_timeline`
pub filter_project_type: Option<MediaFileProjectType>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct ListSessionProjectMediaFilesSuccessResponse {
pub success: bool,
pub results: Vec<ProjectMediaFileInfo>,
pub pagination: PaginationCursors,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct ProjectMediaFileInfo {
/// Primary key identifier
pub token: MediaFileToken,
/// The coarse-grained class of media file.
/// Always `project` for this endpoint.
pub media_class: MediaFileClass,
/// The specific kind of project document: 3D scene, mood board, etc.
pub project_type: MediaFileProjectType,
/// Type of media will dictate which fields are populated and what
/// the frontend should display (eg. video player vs audio player).
/// This is closer in meaning to a "mime type".
pub media_type: MediaFileType,
/// Rich CDN links to the media, including thumbnails, previews, and more.
pub media_links: MediaLinks,
/// Information about the cover image. Many media files do not require a cover image,
/// e.g. image files, video files with thumbnails, audio files, etc.
/// 3D files require them.
pub cover_image: MediaFileCoverImageDetails,
/// User info
pub maybe_creator_user: Option<UserDetailsLight>,
pub creator_set_visibility: Visibility,
/// The name or title of the media file (optional)
pub maybe_title: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
@@ -1,4 +1,2 @@
pub mod delete_media_file;
pub mod list_batch_generated_media_files;
pub mod list_session_project_media_files;
pub mod project;
pub mod list_batch_generated_media_files;
@@ -1,8 +0,0 @@
pub mod upload_new_editor_2d_project;
pub mod upload_new_mood_board_project;
pub mod upload_new_scene_3d_project;
pub mod upload_new_video_timeline_project;
pub mod upload_updated_editor_2d_project;
pub mod upload_updated_mood_board_project;
pub mod upload_updated_scene_3d_project;
pub mod upload_updated_video_timeline_project;
@@ -1,16 +0,0 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
use tokens::tokens::media_files::MediaFileToken;
pub const UPLOAD_NEW_EDITOR_2D_PROJECT_URL_PATH: &str = "/v1/media_files/upload/project/editor_2d/new";
/// Response for saving a new 2D editor document project.
#[derive(Serialize, Deserialize, ToSchema)]
pub struct UploadNewEditor2dProjectSuccessResponse {
pub success: bool,
/// Token for the newly created project media file. Use it to query the
/// project and to save subsequent updates via the update endpoint.
pub media_file_token: MediaFileToken,
}
@@ -1,16 +0,0 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
use tokens::tokens::media_files::MediaFileToken;
pub const UPLOAD_NEW_MOOD_BOARD_PROJECT_URL_PATH: &str = "/v1/media_files/upload/project/mood_board/new";
/// Response for saving a new mood board project.
#[derive(Serialize, Deserialize, ToSchema)]
pub struct UploadNewMoodBoardProjectSuccessResponse {
pub success: bool,
/// Token for the newly created project media file. Use it to query the
/// project and to save subsequent updates via the update endpoint.
pub media_file_token: MediaFileToken,
}
@@ -1,16 +0,0 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
use tokens::tokens::media_files::MediaFileToken;
pub const UPLOAD_NEW_SCENE_3D_PROJECT_URL_PATH: &str = "/v1/media_files/upload/project/scene_3d/new";
/// Response for saving a new 3D scene project.
#[derive(Serialize, Deserialize, ToSchema)]
pub struct UploadNewScene3dProjectSuccessResponse {
pub success: bool,
/// Token for the newly created project media file. Use it to query the
/// project and to save subsequent updates via the update endpoint.
pub media_file_token: MediaFileToken,
}
@@ -1,16 +0,0 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
use tokens::tokens::media_files::MediaFileToken;
pub const UPLOAD_NEW_VIDEO_TIMELINE_PROJECT_URL_PATH: &str = "/v1/media_files/upload/project/video_timeline/new";
/// Response for saving a new video editor timeline project.
#[derive(Serialize, Deserialize, ToSchema)]
pub struct UploadNewVideoTimelineProjectSuccessResponse {
pub success: bool,
/// Token for the newly created project media file. Use it to query the
/// project and to save subsequent updates via the update endpoint.
pub media_file_token: MediaFileToken,
}
@@ -1,21 +0,0 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
use tokens::tokens::media_files::MediaFileToken;
pub const UPLOAD_UPDATED_EDITOR_2D_PROJECT_URL_PATH: &str = "/v1/media_files/upload/project/editor_2d/update/{token}";
/// For the URL PathInfo
#[derive(Serialize, Deserialize, ToSchema)]
pub struct UploadUpdatedEditor2dProjectPathInfo {
pub token: MediaFileToken,
}
/// Response for overwriting an existing 2D editor project.
#[derive(Serialize, Deserialize, ToSchema)]
pub struct UploadUpdatedEditor2dProjectSuccessResponse {
pub success: bool,
/// Token of the updated project media file (unchanged by the update).
pub media_file_token: MediaFileToken,
}
@@ -1,21 +0,0 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
use tokens::tokens::media_files::MediaFileToken;
pub const UPLOAD_UPDATED_MOOD_BOARD_PROJECT_URL_PATH: &str = "/v1/media_files/upload/project/mood_board/update/{token}";
/// For the URL PathInfo
#[derive(Serialize, Deserialize, ToSchema)]
pub struct UploadUpdatedMoodBoardProjectPathInfo {
pub token: MediaFileToken,
}
/// Response for overwriting an existing mood board project.
#[derive(Serialize, Deserialize, ToSchema)]
pub struct UploadUpdatedMoodBoardProjectSuccessResponse {
pub success: bool,
/// Token of the updated project media file (unchanged by the update).
pub media_file_token: MediaFileToken,
}
@@ -1,21 +0,0 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
use tokens::tokens::media_files::MediaFileToken;
pub const UPLOAD_UPDATED_SCENE_3D_PROJECT_URL_PATH: &str = "/v1/media_files/upload/project/scene_3d/update/{token}";
/// For the URL PathInfo
#[derive(Serialize, Deserialize, ToSchema)]
pub struct UploadUpdatedScene3dProjectPathInfo {
pub token: MediaFileToken,
}
/// Response for overwriting an existing 3D scene project.
#[derive(Serialize, Deserialize, ToSchema)]
pub struct UploadUpdatedScene3dProjectSuccessResponse {
pub success: bool,
/// Token of the updated project media file (unchanged by the update).
pub media_file_token: MediaFileToken,
}
@@ -1,21 +0,0 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
use tokens::tokens::media_files::MediaFileToken;
pub const UPLOAD_UPDATED_VIDEO_TIMELINE_PROJECT_URL_PATH: &str = "/v1/media_files/upload/project/video_timeline/update/{token}";
/// For the URL PathInfo
#[derive(Serialize, Deserialize, ToSchema)]
pub struct UploadUpdatedVideoTimelineProjectPathInfo {
pub token: MediaFileToken,
}
/// Response for overwriting an existing video editor timeline project.
#[derive(Serialize, Deserialize, ToSchema)]
pub struct UploadUpdatedVideoTimelineProjectSuccessResponse {
pub success: bool,
/// Token of the updated project media file (unchanged by the update).
pub media_file_token: MediaFileToken,
}
@@ -24,10 +24,6 @@ pub enum RouterSplatModel {
#[serde(rename = "marble_1p1_plus")]
Marble1p1Plus,
/// TripoSplat: single-image to 3D Gaussian splat (via Fal).
#[serde(rename = "triposplat")]
TripoSplat,
}
#[cfg(test)]
@@ -44,7 +40,6 @@ mod tests {
assert_serde_round_trip(RouterSplatModel::Marble1p0Draft, "marble_1p0_draft");
assert_serde_round_trip(RouterSplatModel::Marble1p1, "marble_1p1");
assert_serde_round_trip(RouterSplatModel::Marble1p1Plus, "marble_1p1_plus");
assert_serde_round_trip(RouterSplatModel::TripoSplat, "triposplat");
}
#[test]
@@ -58,7 +53,6 @@ mod tests {
(RouterSplatModel::Marble1p0Draft, CommonSplatModel::Marble1p0Draft),
(RouterSplatModel::Marble1p1, CommonSplatModel::Marble1p1),
(RouterSplatModel::Marble1p1Plus, CommonSplatModel::Marble1p1Plus),
(RouterSplatModel::TripoSplat, CommonSplatModel::TripoSplat),
];
for (router_model, expected_common) in cases {
let json = serde_json::to_string(&router_model).unwrap();
@@ -8,8 +8,6 @@ use crate::generate::generate_splat::providers::artcraft::marble_1p0::build::bui
use crate::generate::generate_splat::providers::artcraft::marble_1p0_draft::build::build_artcraft_marble_1p0_draft;
use crate::generate::generate_splat::providers::artcraft::marble_1p1::build::build_artcraft_marble_1p1;
use crate::generate::generate_splat::providers::artcraft::marble_1p1_plus::build::build_artcraft_marble_1p1_plus;
use crate::generate::generate_splat::providers::artcraft::triposplat::build::build_artcraft_triposplat;
use crate::generate::generate_splat::providers::fal::triposplat::build::build_fal_triposplat;
use crate::generate::generate_splat::providers::worldlabs::marble_1p0::build::build_worldlabs_marble_1p0;
use crate::generate::generate_splat::providers::worldlabs::marble_1p0_draft::build::build_worldlabs_marble_1p0_draft;
use crate::generate::generate_splat::providers::worldlabs::marble_1p1::build::build_worldlabs_marble_1p1;
@@ -81,9 +79,6 @@ impl GenerateSplatRequestBuilder {
(RouterProvider::Artcraft, RouterSplatModel::Marble1p0Draft | RouterSplatModel::Marble0p1Mini) => build_artcraft_marble_1p0_draft(self),
(RouterProvider::Artcraft, RouterSplatModel::Marble1p1) => build_artcraft_marble_1p1(self),
(RouterProvider::Artcraft, RouterSplatModel::Marble1p1Plus) => build_artcraft_marble_1p1_plus(self),
(RouterProvider::Artcraft, RouterSplatModel::TripoSplat) => build_artcraft_triposplat(self),
// Fal
(RouterProvider::Fal, RouterSplatModel::TripoSplat) => build_fal_triposplat(self),
// World Labs
(RouterProvider::WorldLabs, RouterSplatModel::Marble1p0 | RouterSplatModel::Marble0p1Plus) => build_worldlabs_marble_1p0(self),
(RouterProvider::WorldLabs, RouterSplatModel::Marble1p0Draft | RouterSplatModel::Marble0p1Mini) => build_worldlabs_marble_1p0_draft(self),
@@ -1,6 +1,3 @@
use std::fmt::Debug;
use std::sync::Arc;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
#[derive(Clone, Debug)]
@@ -16,30 +13,9 @@ pub struct WorldLabsSplatResponsePayload {
pub done: bool,
}
/// Fal's queue/webhook submission receipt for a splat generation.
#[derive(Clone, Debug)]
pub struct FalSplatResponsePayload {
/// Fal's ID for the queued request. Present on queue submissions and on
/// most webhook submissions.
pub request_id: Option<String>,
/// Gateway request ID (webhook submissions).
pub gateway_request_id: Option<String>,
/// Queue-mode status polling URL.
pub maybe_status_url: Option<String>,
/// Queue-mode response URL.
pub maybe_response_url: Option<String>,
/// The outbound request we sent, for debug logging.
pub maybe_outbound_request: Option<Arc<dyn Debug + Send + Sync>>,
}
#[derive(Clone, Debug)]
pub enum GenerateSplatResponse {
Artcraft(ArtcraftSplatResponsePayload),
Fal(FalSplatResponsePayload),
WorldLabs(WorldLabsSplatResponsePayload),
}
@@ -51,13 +27,6 @@ impl GenerateSplatResponse {
}
}
pub fn get_fal_payload(&self) -> Option<FalSplatResponsePayload> {
match self {
Self::Fal(p) => Some(p.clone()),
_ => None,
}
}
pub fn get_worldlabs_payload(&self) -> Option<WorldLabsSplatResponsePayload> {
match self {
Self::WorldLabs(p) => Some(p.clone()),
@@ -6,4 +6,3 @@ pub mod marble_1p0;
pub mod marble_1p0_draft;
pub mod marble_1p1;
pub mod marble_1p1_plus;
pub mod triposplat;
@@ -1,14 +0,0 @@
use enums::common::generation::common_splat_model::CommonSplatModel as CommonSplatModelEnum;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_splat::generate_splat_request_builder::GenerateSplatRequestBuilder;
use crate::generate::generate_splat::providers::artcraft::build_common::build_artcraft_omni_splat_request;
use crate::generate::generate_splat::providers::artcraft::triposplat::request::ArtcraftTripoSplatRequestState;
use crate::generate::generate_splat::splat_generation_draft_or_request::SplatGenerationDraftOrRequest;
use crate::generate::generate_splat::splat_generation_request::SplatGenerationRequest;
pub fn build_artcraft_triposplat(builder: GenerateSplatRequestBuilder) -> Result<SplatGenerationDraftOrRequest, ArtcraftRouterError> {
let request = build_artcraft_omni_splat_request(builder, CommonSplatModelEnum::TripoSplat)?;
let state = ArtcraftTripoSplatRequestState { request };
Ok(SplatGenerationDraftOrRequest::Request(SplatGenerationRequest::ArtcraftTripoSplat(state)))
}
@@ -1,51 +0,0 @@
use crate::generate::generate_splat::providers::artcraft::cost_common::artcraft_splat_cost_estimate;
use crate::generate::generate_splat::providers::artcraft::triposplat::request::ArtcraftTripoSplatRequestState;
use crate::generate::generate_splat::splat_generation_cost_estimate::SplatGenerationCostEstimate;
const FLAT_COST_IN_USD_CENTS: u64 = 7;
/// TripoSplat via Artcraft is flat priced: it takes a single input image and
/// no option affects the price.
#[derive(Clone, Debug)]
pub struct ArtcraftTripoSplatCostState {
pub cost_in_usd_cents: u64,
}
impl ArtcraftTripoSplatCostState {
pub fn from_request(_request: &ArtcraftTripoSplatRequestState) -> Self {
Self { cost_in_usd_cents: FLAT_COST_IN_USD_CENTS }
}
pub fn estimate_cost(&self) -> SplatGenerationCostEstimate {
artcraft_splat_cost_estimate(self.cost_in_usd_cents)
}
}
#[cfg(test)]
mod tests {
use tokens::tokens::media_files::MediaFileToken;
use crate::api::image_list_ref::ImageListRef;
use crate::api::router_provider::RouterProvider;
use crate::api::router_splat_model::RouterSplatModel;
use crate::generate::generate_splat::generate_splat_request_builder::GenerateSplatRequestBuilder;
#[test]
fn flat_seven_cents() {
let builder = GenerateSplatRequestBuilder {
model: RouterSplatModel::TripoSplat,
provider: RouterProvider::Artcraft,
reference_images: Some(ImageListRef::MediaFileTokens(vec![
MediaFileToken::new("mf_test123".to_string()),
])),
..Default::default()
};
let cost = builder.build2()
.expect("build should succeed")
.estimate_cost()
.expect("estimate should succeed")
.cost_in_usd_cents
.expect("cost should be present");
assert_eq!(cost, 7);
}
}
@@ -1,3 +0,0 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -1,17 +0,0 @@
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_splat_cost_and_generate_request::OmniGenSplatCostAndGenerateRequest;
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_splat::generate_splat_response::GenerateSplatResponse;
use crate::generate::generate_splat::providers::artcraft::request_common::send_artcraft_omni_splat_request;
#[derive(Clone, Debug)]
pub struct ArtcraftTripoSplatRequestState {
pub request: OmniGenSplatCostAndGenerateRequest,
}
impl ArtcraftTripoSplatRequestState {
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateSplatResponse, ArtcraftRouterError> {
send_artcraft_omni_splat_request(&self.request, client).await
}
}
@@ -1,2 +0,0 @@
pub(crate) mod request_common;
pub mod triposplat;
@@ -1,44 +0,0 @@
use std::fmt::Debug;
use std::sync::Arc;
use fal_client::requests::traits::fal_endpoint_trait::FalEndpoint;
use crate::client::router_fal_client::RouterFalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_splat::generate_splat_response::{
FalSplatResponsePayload, GenerateSplatResponse,
};
/// Send a splat request to fal using the client's dispatch mode (webhook or
/// queue). Shared by all Fal splat request states.
pub(crate) async fn send_fal_splat_request<R>(
request: &R,
client: &RouterFalClient,
) -> Result<GenerateSplatResponse, ArtcraftRouterError>
where
R: FalEndpoint + Clone + Debug + Send + Sync + 'static,
{
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let payload = if let Some(webhook_url) = &client.webhook_url {
let response = request.send_webhook_request(&client.api_key, webhook_url).await?;
FalSplatResponsePayload {
request_id: response.request_id,
gateway_request_id: response.gateway_request_id,
maybe_status_url: None,
maybe_response_url: None,
maybe_outbound_request: Some(outbound),
}
} else {
let response = request.send_queue_request(&client.api_key).await?;
FalSplatResponsePayload {
request_id: Some(response.request_id),
gateway_request_id: None,
maybe_status_url: Some(response.status_url),
maybe_response_url: Some(response.response_url),
maybe_outbound_request: Some(outbound),
}
};
Ok(GenerateSplatResponse::Fal(payload))
}
@@ -1,228 +0,0 @@
use fal_client::requests::api::splat::image::triposplat_image_to_splat::api::{
TripoSplatImageToSplatRequest, TripoSplatOutputFormat,
};
use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_mesh::providers::reject_unsupported::reject_unsupported_option;
use crate::generate::generate_splat::generate_splat_request_builder::GenerateSplatRequestBuilder;
use crate::generate::generate_splat::providers::fal::triposplat::request::FalTripoSplatRequestState;
use crate::generate::generate_splat::splat_generation_draft_or_request::SplatGenerationDraftOrRequest;
use crate::generate::generate_splat::splat_generation_request::SplatGenerationRequest;
/// TripoSplat takes exactly one input image and nothing else. Prompts,
/// videos, panorama and recaption flags are unsupported: rejected under
/// `ErrorOut`, dropped otherwise.
pub fn build_fal_triposplat(builder: GenerateSplatRequestBuilder) -> Result<SplatGenerationDraftOrRequest, ArtcraftRouterError> {
let state = build_fal_triposplat_state(builder)?;
Ok(SplatGenerationDraftOrRequest::Request(SplatGenerationRequest::FalTripoSplat(state)))
}
pub(crate) fn build_fal_triposplat_state(
mut builder: GenerateSplatRequestBuilder,
) -> Result<FalTripoSplatRequestState, ArtcraftRouterError> {
let strategy = builder.request_mismatch_mitigation_strategy;
reject_unsupported_option("prompt", builder.prompt.as_ref(), strategy)?;
reject_unsupported_option("reference_video", builder.reference_video.as_ref(), strategy)?;
if builder.is_panoramic == Some(true) {
reject_unsupported_option("is_panoramic", builder.is_panoramic.as_ref(), strategy)?;
}
reject_unsupported_option("disable_recaption", builder.disable_recaption.as_ref(), strategy)?;
let image_url = plan_single_image_url(builder.reference_images.take(), strategy)?
.ok_or_else(|| {
ArtcraftRouterError::InvalidInput(
"TripoSplat requires an input image".to_string(),
)
})?;
let request = TripoSplatImageToSplatRequest {
image_url,
num_gaussians: None,
num_inference_steps: None,
guidance_scale: None,
// Always request PLY output — it's the format our pipeline stores.
output_format: Some(TripoSplatOutputFormat::Ply),
seed: None,
enable_safety_checker: None,
};
Ok(FalTripoSplatRequestState { request })
}
/// Resolve the single input image from `reference_images`. More than one
/// image rejects under `ErrorOut`; the other strategies keep the first.
/// Fal only takes URLs; media file tokens are rejected.
fn plan_single_image_url(
reference_images: Option<ImageListRef>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<String>, ArtcraftRouterError> {
let urls = match reference_images {
None => return Ok(None),
Some(ImageListRef::Urls(urls)) => urls,
Some(ImageListRef::MediaFileTokens(tokens)) if tokens.is_empty() => return Ok(None),
Some(ImageListRef::MediaFileTokens(_)) => {
return Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls));
}
};
if urls.len() > 1 {
if let RequestMismatchMitigationStrategy::ErrorOut = strategy {
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "reference_images",
value: format!("Expected at most 1 image, got {}", urls.len()),
}));
}
}
Ok(urls.into_iter().next())
}
#[cfg(test)]
mod tests {
use crate::api::router_provider::RouterProvider;
use crate::api::router_splat_model::RouterSplatModel;
use crate::api::video_ref::VideoRef;
use super::*;
const IMAGE_URL: &str = "https://example.com/object.png";
mod input_tests {
use super::*;
#[test]
fn single_image_builds() {
let state = build_fal_triposplat_state(image_builder()).expect("build");
assert_eq!(state.request.image_url, IMAGE_URL);
}
#[test]
fn output_format_is_always_ply() {
let state = build_fal_triposplat_state(image_builder()).expect("build");
assert_eq!(state.request.output_format, Some(TripoSplatOutputFormat::Ply));
}
#[test]
fn no_image_is_rejected() {
let result = build_fal_triposplat_state(base_builder());
assert!(matches!(result, Err(ArtcraftRouterError::InvalidInput(_))));
}
#[test]
fn media_tokens_are_rejected() {
use tokens::tokens::media_files::MediaFileToken;
let builder = GenerateSplatRequestBuilder {
reference_images: Some(ImageListRef::MediaFileTokens(vec![
MediaFileToken::new("mf_test123".to_string()),
])),
..base_builder()
};
assert!(matches!(
build_fal_triposplat_state(builder),
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
));
}
#[test]
fn multiple_images_error_out_under_error_out() {
let builder = GenerateSplatRequestBuilder {
reference_images: Some(ImageListRef::Urls(vec![
IMAGE_URL.to_string(),
"https://example.com/second.png".to_string(),
])),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_builder()
};
assert!(build_fal_triposplat_state(builder).is_err());
}
#[test]
fn multiple_images_keep_the_first_under_lenient_strategies() {
let builder = GenerateSplatRequestBuilder {
reference_images: Some(ImageListRef::Urls(vec![
IMAGE_URL.to_string(),
"https://example.com/second.png".to_string(),
])),
..base_builder()
};
let state = build_fal_triposplat_state(builder).expect("build");
assert_eq!(state.request.image_url, IMAGE_URL);
}
}
mod unsupported_option_tests {
use super::*;
#[test]
fn unsupported_options_error_out_under_error_out() {
let cases = [
GenerateSplatRequestBuilder {
prompt: Some("a cozy cabin".to_string()),
..strict_image_builder()
},
GenerateSplatRequestBuilder {
reference_video: Some(VideoRef::Url("https://example.com/v.mp4".to_string())),
..strict_image_builder()
},
GenerateSplatRequestBuilder {
is_panoramic: Some(true),
..strict_image_builder()
},
GenerateSplatRequestBuilder {
disable_recaption: Some(true),
..strict_image_builder()
},
];
for builder in cases {
assert!(build_fal_triposplat_state(builder).is_err());
}
}
#[test]
fn unsupported_options_are_dropped_under_lenient_strategies() {
let builder = GenerateSplatRequestBuilder {
prompt: Some("a cozy cabin".to_string()),
is_panoramic: Some(true),
disable_recaption: Some(true),
..image_builder()
};
assert!(build_fal_triposplat_state(builder).is_ok());
}
#[test]
fn is_panoramic_false_is_not_a_mismatch() {
let builder = GenerateSplatRequestBuilder {
is_panoramic: Some(false),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..image_builder()
};
assert!(build_fal_triposplat_state(builder).is_ok());
}
}
// ── Helpers ──
fn base_builder() -> GenerateSplatRequestBuilder {
GenerateSplatRequestBuilder {
model: RouterSplatModel::TripoSplat,
provider: RouterProvider::Fal,
..Default::default()
}
}
fn image_builder() -> GenerateSplatRequestBuilder {
GenerateSplatRequestBuilder {
reference_images: Some(ImageListRef::Urls(vec![IMAGE_URL.to_string()])),
..base_builder()
}
}
fn strict_image_builder() -> GenerateSplatRequestBuilder {
GenerateSplatRequestBuilder {
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..image_builder()
}
}
}
@@ -1,59 +0,0 @@
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
use crate::generate::generate_splat::providers::fal::triposplat::request::FalTripoSplatRequestState;
use crate::generate::generate_splat::splat_generation_cost_estimate::SplatGenerationCostEstimate;
/// Cost math is owned by fal_client's `FalRequestCostCalculator` — the
/// router state just forwards the result so router cost ≡ fal_client cost
/// by construction. TripoSplat is flat 5¢.
#[derive(Clone, Debug)]
pub struct FalTripoSplatCostState {
pub cost_in_usd_cents: u64,
}
impl FalTripoSplatCostState {
pub fn from_request(request: &FalTripoSplatRequestState) -> Self {
Self {
cost_in_usd_cents: request.request.calculate_cost_in_cents(),
}
}
pub fn estimate_cost(&self) -> SplatGenerationCostEstimate {
SplatGenerationCostEstimate {
cost_in_credits: Some(self.cost_in_usd_cents),
cost_in_usd_cents: Some(self.cost_in_usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
failures_are_refunded: None,
}
}
}
#[cfg(test)]
mod tests {
use crate::api::image_list_ref::ImageListRef;
use crate::api::router_provider::RouterProvider;
use crate::api::router_splat_model::RouterSplatModel;
use crate::generate::generate_splat::generate_splat_request_builder::GenerateSplatRequestBuilder;
#[test]
fn flat_five_cents() {
let builder = GenerateSplatRequestBuilder {
model: RouterSplatModel::TripoSplat,
provider: RouterProvider::Fal,
reference_images: Some(ImageListRef::Urls(vec![
"https://example.com/object.png".to_string(),
])),
..Default::default()
};
let cost = builder.build2()
.expect("build should succeed")
.estimate_cost()
.expect("estimate should succeed")
.cost_in_usd_cents
.expect("cost should be present");
assert_eq!(cost, 5);
}
}
@@ -1,3 +0,0 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -1,62 +0,0 @@
use fal_client::requests::api::splat::image::triposplat_image_to_splat::api::TripoSplatImageToSplatRequest;
use crate::client::router_fal_client::RouterFalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_splat::generate_splat_response::GenerateSplatResponse;
use crate::generate::generate_splat::providers::fal::request_common::send_fal_splat_request;
#[derive(Clone, Debug)]
pub struct FalTripoSplatRequestState {
/// Final materialized request; ready to fire.
pub request: TripoSplatImageToSplatRequest,
}
impl FalTripoSplatRequestState {
pub async fn send(&self, client: &RouterFalClient) -> Result<GenerateSplatResponse, ArtcraftRouterError> {
send_fal_splat_request(&self.request, client).await
}
}
#[cfg(test)]
mod tests {
use test_data::web::image_urls::ERNEST_SCARED_STUPID_IMAGE_URL;
use crate::api::image_list_ref::ImageListRef;
use crate::api::router_provider::RouterProvider;
use crate::api::router_splat_model::RouterSplatModel;
use crate::generate::generate_splat::generate_splat_request_builder::GenerateSplatRequestBuilder;
use crate::generate::generate_splat::generate_splat_response::GenerateSplatResponse;
use crate::generate::generate_splat::splat_generation_draft_or_request::SplatGenerationDraftOrRequest;
use crate::test_helpers::get_fal_client;
#[tokio::test]
#[ignore] // requires real API key, incurs cost
async fn send_triposplat_image_to_splat() {
let client = get_fal_client();
let builder = GenerateSplatRequestBuilder {
model: RouterSplatModel::TripoSplat,
provider: RouterProvider::Fal,
reference_images: Some(ImageListRef::Urls(vec![
ERNEST_SCARED_STUPID_IMAGE_URL.to_string(),
])),
..Default::default()
};
let draft_or_request = builder.build2().expect("build2 should succeed");
let request = match draft_or_request {
SplatGenerationDraftOrRequest::Request(r) => r,
_ => panic!("expected Request variant (Fal splat skips draft)"),
};
let response = request.send_request(&client).await.expect("send_request should succeed");
println!("response: {:?}", response);
let payload = match response {
GenerateSplatResponse::Fal(p) => p,
other => panic!("expected Fal payload, got {other:?}"),
};
assert!(payload.request_id.is_some() || payload.gateway_request_id.is_some());
assert_eq!(1, 2, "Inspect output above");
}
}
@@ -1,3 +1,2 @@
pub mod artcraft;
pub mod fal;
pub mod worldlabs;
@@ -10,10 +10,6 @@ use crate::generate::generate_splat::providers::artcraft::marble_1p1::cost::Artc
use crate::generate::generate_splat::providers::artcraft::marble_1p1::request::ArtcraftMarble1p1RequestState;
use crate::generate::generate_splat::providers::artcraft::marble_1p1_plus::cost::ArtcraftMarble1p1PlusCostState;
use crate::generate::generate_splat::providers::artcraft::marble_1p1_plus::request::ArtcraftMarble1p1PlusRequestState;
use crate::generate::generate_splat::providers::artcraft::triposplat::cost::ArtcraftTripoSplatCostState;
use crate::generate::generate_splat::providers::artcraft::triposplat::request::ArtcraftTripoSplatRequestState;
use crate::generate::generate_splat::providers::fal::triposplat::cost::FalTripoSplatCostState;
use crate::generate::generate_splat::providers::fal::triposplat::request::FalTripoSplatRequestState;
use crate::generate::generate_splat::providers::worldlabs::marble_1p0::cost::WorldLabsMarble1p0CostState;
use crate::generate::generate_splat::providers::worldlabs::marble_1p0::request::WorldLabsMarble1p0RequestState;
use crate::generate::generate_splat::providers::worldlabs::marble_1p0_draft::cost::WorldLabsMarble1p0DraftModelCostState;
@@ -30,8 +26,6 @@ pub enum SplatGenerationRequest {
ArtcraftMarble1p0Draft(ArtcraftMarble1p0DraftRequestState),
ArtcraftMarble1p1(ArtcraftMarble1p1RequestState),
ArtcraftMarble1p1Plus(ArtcraftMarble1p1PlusRequestState),
ArtcraftTripoSplat(ArtcraftTripoSplatRequestState),
FalTripoSplat(FalTripoSplatRequestState),
WorldLabsMarble1p0(WorldLabsMarble1p0RequestState),
WorldLabsMarble1p0Draft(WorldLabsMarble1p0DraftModelRequestState),
WorldLabsMarble1p1(WorldLabsMarble1p1RequestState),
@@ -46,8 +40,6 @@ impl SplatGenerationRequest {
Self::ArtcraftMarble1p0Draft(_) => RouterProvider::Artcraft,
Self::ArtcraftMarble1p1(_) => RouterProvider::Artcraft,
Self::ArtcraftMarble1p1Plus(_) => RouterProvider::Artcraft,
Self::ArtcraftTripoSplat(_) => RouterProvider::Artcraft,
Self::FalTripoSplat(_) => RouterProvider::Fal,
Self::WorldLabsMarble1p0(_) => RouterProvider::WorldLabs,
Self::WorldLabsMarble1p0Draft(_) => RouterProvider::WorldLabs,
Self::WorldLabsMarble1p1(_) => RouterProvider::WorldLabs,
@@ -62,8 +54,6 @@ impl SplatGenerationRequest {
SplatGenerationRequest::ArtcraftMarble1p0Draft(request) => Ok(ArtcraftMarble1p0DraftCostState::from_request(request).estimate_cost()),
SplatGenerationRequest::ArtcraftMarble1p1(request) => Ok(ArtcraftMarble1p1CostState::from_request(request).estimate_cost()),
SplatGenerationRequest::ArtcraftMarble1p1Plus(request) => Ok(ArtcraftMarble1p1PlusCostState::from_request(request).estimate_cost()),
SplatGenerationRequest::ArtcraftTripoSplat(request) => Ok(ArtcraftTripoSplatCostState::from_request(request).estimate_cost()),
SplatGenerationRequest::FalTripoSplat(request) => Ok(FalTripoSplatCostState::from_request(request).estimate_cost()),
SplatGenerationRequest::WorldLabsMarble1p0(request) => Ok(WorldLabsMarble1p0CostState::from_request(request).estimate_cost()),
SplatGenerationRequest::WorldLabsMarble1p0Draft(request) => Ok(WorldLabsMarble1p0DraftModelCostState::from_request(request).estimate_cost()),
SplatGenerationRequest::WorldLabsMarble1p1(request) => Ok(WorldLabsMarble1p1CostState::from_request(request).estimate_cost()),
@@ -91,14 +81,6 @@ impl SplatGenerationRequest {
let client_ref = client.get_artcraft_client_ref()?;
request.send(client_ref).await
},
SplatGenerationRequest::ArtcraftTripoSplat(request) => {
let client_ref = client.get_artcraft_client_ref()?;
request.send(client_ref).await
},
SplatGenerationRequest::FalTripoSplat(request) => {
let client_ref = client.get_fal_client_ref()?;
request.send(client_ref).await
},
SplatGenerationRequest::WorldLabsMarble1p0(request) => {
let client_ref = client.get_worldlabs_client_ref()?;
request.send(client_ref).await
@@ -1,5 +1,4 @@
pub mod audio;
pub mod image;
pub mod mesh;
pub mod splat;
pub mod video;
@@ -1 +0,0 @@
pub mod triposplat_image_to_splat;
@@ -1,160 +0,0 @@
use crate::error::fal_error_plus::FalErrorPlus;
use crate::requests::api::splat::image::triposplat_image_to_splat::raw_request::{
TripoSplatImageToSplatInput, TripoSplatImageToSplatOutput,
};
use crate::requests::traits::fal_endpoint_trait::FalEndpoint;
/// TripoSplat image-to-Gaussian-splat: reconstructs a 3D Gaussian splat from
/// a single input image.
#[derive(Clone, Debug)]
pub struct TripoSplatImageToSplatRequest {
/// URL of the input image (required).
pub image_url: String,
/// Number of Gaussians to generate. Range 32768-262144;
/// fal's default is 262144 when `None`.
pub num_gaussians: Option<u32>,
/// Number of inference steps. Range 1-50; fal's default is 20 when `None`.
pub num_inference_steps: Option<u32>,
/// Guidance scale. Range 0-10; fal's default is 3 when `None`.
pub guidance_scale: Option<f32>,
/// Output file format. fal's default is `Ply` when `None`.
pub output_format: Option<TripoSplatOutputFormat>,
/// Seed for reproducibility. fal picks a random seed when `None`.
pub seed: Option<i64>,
/// fal's default is true when `None`.
pub enable_safety_checker: Option<bool>,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum TripoSplatOutputFormat {
Ply,
Splat,
}
impl TripoSplatOutputFormat {
pub(crate) fn to_str(&self) -> &'static str {
match self {
Self::Ply => "ply",
Self::Splat => "splat",
}
}
}
impl FalEndpoint for TripoSplatImageToSplatRequest {
const ENDPOINT: &str = "tripo3d/triposplat";
type RawRequest = TripoSplatImageToSplatInput;
type RawResponse = TripoSplatImageToSplatOutput;
fn to_raw_request(&self) -> Result<Self::RawRequest, FalErrorPlus> {
Ok(Self::RawRequest {
image_url: self.image_url.clone(),
num_gaussians: self.num_gaussians,
num_inference_steps: self.num_inference_steps,
guidance_scale: self.guidance_scale,
output_format: self.output_format.map(|f| f.to_str().to_string()),
seed: self.seed,
enable_safety_checker: self.enable_safety_checker,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::traits::fal_endpoint_trait::FalEndpoint;
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::ERNEST_SCARED_STUPID_IMAGE_URL;
mod serialization_tests {
use super::*;
#[test]
fn minimal_request_serializes_image_url_only() {
let raw = base_request().to_raw_request().expect("to_raw_request");
let json = serde_json::to_value(&raw).unwrap();
let obj = json.as_object().unwrap();
assert_eq!(obj.len(), 1, "only image_url should serialize: {json}");
assert_eq!(json["image_url"], "https://example.com/input.png");
}
#[test]
fn output_format_wire_values() {
assert_eq!(TripoSplatOutputFormat::Ply.to_str(), "ply");
assert_eq!(TripoSplatOutputFormat::Splat.to_str(), "splat");
}
#[test]
fn full_request_serializes_all_fields() {
let request = TripoSplatImageToSplatRequest {
num_gaussians: Some(131_072),
num_inference_steps: Some(30),
guidance_scale: Some(5.0),
output_format: Some(TripoSplatOutputFormat::Ply),
seed: Some(42),
enable_safety_checker: Some(false),
..base_request()
};
let raw = request.to_raw_request().expect("to_raw_request");
let json = serde_json::to_value(&raw).unwrap();
assert_eq!(json["num_gaussians"], 131_072);
assert_eq!(json["num_inference_steps"], 30);
assert_eq!(json["guidance_scale"], 5.0);
assert_eq!(json["output_format"], "ply");
assert_eq!(json["seed"], 42);
assert_eq!(json["enable_safety_checker"], false);
}
}
// ── Helpers ──
fn base_request() -> TripoSplatImageToSplatRequest {
TripoSplatImageToSplatRequest {
image_url: "https://example.com/input.png".to_string(),
num_gaussians: None,
num_inference_steps: None,
guidance_scale: None,
output_format: None,
seed: None,
enable_safety_checker: None,
}
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_image_to_splat_webhook() -> AnyhowResult<()> {
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let request = TripoSplatImageToSplatRequest {
image_url: ERNEST_SCARED_STUPID_IMAGE_URL.to_string(),
..base_request()
};
let result = request.send_webhook_request(&api_key, "https://example.com/webhook").await?;
println!("Webhook result: {:?}", result);
assert!(result.request_id.is_some() || result.gateway_request_id.is_some());
Ok(())
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_image_to_splat_queue() -> AnyhowResult<()> {
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let request = TripoSplatImageToSplatRequest {
image_url: ERNEST_SCARED_STUPID_IMAGE_URL.to_string(),
..base_request()
};
let result = request.send_queue_request(&api_key).await?;
println!("Queue result — request_id: {}", result.request_id);
assert!(!result.request_id.is_empty());
Ok(())
}
}
@@ -1,47 +0,0 @@
use crate::requests::api::splat::image::triposplat_image_to_splat::api::TripoSplatImageToSplatRequest;
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
impl FalRequestCostCalculator for TripoSplatImageToSplatRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
// Pricing (see https://fal.ai/models/tripo3d/triposplat):
// "Price: $0.05 per generations"
5
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::requests::api::splat::image::triposplat_image_to_splat::api::TripoSplatOutputFormat;
fn base_request() -> TripoSplatImageToSplatRequest {
TripoSplatImageToSplatRequest {
image_url: "https://example.com/input.png".to_string(),
num_gaussians: None,
num_inference_steps: None,
guidance_scale: None,
output_format: None,
seed: None,
enable_safety_checker: None,
}
}
#[test]
fn flat_five_cents() {
assert_eq!(base_request().calculate_cost_in_cents(), 5);
}
#[test]
fn options_do_not_change_cost() {
let request = TripoSplatImageToSplatRequest {
num_gaussians: Some(32_768),
num_inference_steps: Some(50),
guidance_scale: Some(10.0),
output_format: Some(TripoSplatOutputFormat::Splat),
seed: Some(7),
enable_safety_checker: Some(false),
..base_request()
};
assert_eq!(request.calculate_cost_in_cents(), 5);
}
}
@@ -1,3 +0,0 @@
pub mod api;
pub mod cost;
pub mod raw_request;
@@ -1,37 +0,0 @@
use serde::{Deserialize, Serialize};
/// Over-the-wire input shape for `tripo3d/triposplat` (image-to-Gaussian-splat).
/// fal's schema: <https://fal.ai/models/tripo3d/triposplat/api>
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct TripoSplatImageToSplatInput {
/// URL of the input image.
pub image_url: String,
/// Number of Gaussians to generate. Range 32768-262144.
/// fal default: 262144.
#[serde(skip_serializing_if = "Option::is_none")]
pub num_gaussians: Option<u32>,
/// Number of inference steps. Range 1-50. fal default: 20.
#[serde(skip_serializing_if = "Option::is_none")]
pub num_inference_steps: Option<u32>,
/// Guidance scale. Range 0-10. fal default: 3.
#[serde(skip_serializing_if = "Option::is_none")]
pub guidance_scale: Option<f32>,
/// Options: "ply", "splat". fal default: "ply".
#[serde(skip_serializing_if = "Option::is_none")]
pub output_format: Option<String>,
/// Seed for reproducibility. fal default: random.
#[serde(skip_serializing_if = "Option::is_none")]
pub seed: Option<i64>,
/// fal default: true.
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_safety_checker: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TripoSplatImageToSplatOutput {}
@@ -1 +0,0 @@
pub mod image;
@@ -10,7 +10,6 @@ pub (crate) static CUSTOM_INFER: LazyLock<Infer> = LazyLock::new(|| {
fn add_custom_types(infer: &mut Infer) {
infer.add("model/gltf-binary", "glb", gltf_matcher);
infer.add("model/fbx", "fbx", fbx_matcher);
infer.add("model/ply", "ply", ply_matcher);
}
fn gltf_matcher(buf: &[u8]) -> bool {
@@ -19,14 +18,6 @@ fn gltf_matcher(buf: &[u8]) -> bool {
buf.len() >= 4 && buf[0] == 103 && buf[1] == 108 && buf[2] == 84 && buf[3] == 70
}
fn ply_matcher(buf: &[u8]) -> bool {
// PLY files (ASCII and binary alike) start with the line "ply".
// https://paulbourke.net/dataformats/ply/
buf.len() >= 4
&& &buf[..3] == b"ply"
&& (buf[3] == b'\n' || buf[3] == b'\r')
}
fn fbx_matcher(buf: &[u8]) -> bool {
// Binary FBX magic: "Kaydara FBX Binary " (two trailing spaces).
// https://code.blender.org/2013/08/fbx-binary-file-format-specification/
@@ -56,19 +47,6 @@ mod tests {
assert_eq!(maybe_type.mime_type(), "model/fbx");
}
#[test]
fn test_ply_magic() {
let bytes = b"ply\nformat binary_little_endian 1.0\n".to_vec();
let maybe_type = CUSTOM_INFER.get(&bytes).unwrap();
assert_eq!(maybe_type.mime_type(), "model/ply");
}
#[test]
fn test_ply_prefix_without_newline_is_not_detected() {
let bytes = b"plywood texture".to_vec();
assert!(CUSTOM_INFER.get(&bytes).is_none());
}
#[test]
fn test_ascii_fbx_is_not_detected() {
// ASCII FBX files have no magic bytes.
@@ -17,7 +17,6 @@ pub enum FileExtension {
Obj,
Ogg,
Opus,
Ply,
Png,
Wav,
Webm,
@@ -38,7 +37,6 @@ impl FileExtension {
"audio/mpeg" => Some(FileExtension::Mp3),
"video/mp4" => Some(FileExtension::Mp4),
"model/obj" => Some(FileExtension::Obj),
"model/ply" => Some(FileExtension::Ply),
"audio/ogg" => Some(FileExtension::Ogg),
"audio/opus" => Some(FileExtension::Opus),
"image/png" => Some(FileExtension::Png),
@@ -71,7 +69,6 @@ impl FileExtension {
FileExtension::Obj => (".obj", "obj"),
FileExtension::Ogg => (".ogg", "ogg"),
FileExtension::Opus => (".opus", "opus"),
FileExtension::Ply => (".ply", "ply"),
FileExtension::Png => (".png", "png"),
FileExtension::Wav => (".wav", "wav"),
FileExtension::Webm => (".webm", "webm"),
@@ -8,7 +8,6 @@ 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_project_type::MediaFileProjectType;
use enums::by_table::media_files::media_file_type::MediaFileType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::visibility::Visibility;
@@ -26,12 +25,6 @@ pub enum UploadType {
Filesystem,
DeviceCaptureApi,
StorytellerEngine,
/// Internal project documents (scenes, mood boards, timelines, etc.)
/// created by our apps and uploaded by the client. Stored with origin
/// `upload`; the project nature is captured by `media_class = 'project'`
/// and `maybe_project_type`.
ProjectFile,
}
pub struct InsertMediaFileFromUploadArgs<'a> {
@@ -40,9 +33,6 @@ pub struct InsertMediaFileFromUploadArgs<'a> {
pub media_file_type: MediaFileType,
pub maybe_media_class: Option<MediaFileClass>,
/// Only set for internal Artcraft project documents (`media_class = 'project'`).
pub maybe_project_type: Option<MediaFileProjectType>,
pub upload_type: UploadType,
pub maybe_engine_category: Option<MediaFileEngineCategory>,
@@ -84,7 +74,6 @@ pub async fn insert_media_file_from_file_upload(
let mut origin_category = match args.upload_type {
UploadType::ThirdPartyInference => MediaFileOriginCategory::ThirdPartyInference,
UploadType::Filesystem => MediaFileOriginCategory::Upload,
UploadType::ProjectFile => MediaFileOriginCategory::Upload,
UploadType::DeviceCaptureApi => MediaFileOriginCategory::DeviceApi,
UploadType::StorytellerEngine => MediaFileOriginCategory::StorytellerStudio,
};
@@ -137,7 +126,6 @@ SET
media_class = ?,
media_type = ?,
maybe_project_type = ?,
origin_category = ?,
origin_product_category = ?,
@@ -187,8 +175,6 @@ SET
args.media_file_type.to_str(),
args.maybe_project_type.map(|project_type| project_type.to_str()),
origin_category.to_str(),
ORIGIN_PRODUCT_CATEGORY.to_str(),
@@ -3,6 +3,5 @@ pub mod set_media_file_cover_image;
pub mod update_media_file_animation_type;
pub mod update_media_file_engine_category;
pub mod update_media_file_stored_cloud_contents;
pub mod update_project_media_file_contents;
pub mod update_media_file_visibility;
pub mod update_media_file_visibility_transactional;
@@ -1,7 +1,5 @@
use sqlx::MySqlPool;
use enums::by_table::media_files::media_file_class::MediaFileClass;
use enums::by_table::media_files::media_file_project_type::MediaFileProjectType;
use errors::AnyhowResult;
use tokens::tokens::media_files::MediaFileToken;
@@ -16,13 +14,6 @@ use tokens::tokens::media_files::MediaFileToken;
pub struct UpdateArgs<'a> {
pub media_file_token: &'a MediaFileToken,
/// The class the record should hold after the update. Also backfills legacy
/// records that were written before the current classification scheme.
pub media_class: MediaFileClass,
/// Only set for internal Artcraft project documents (`media_class = 'project'`).
pub maybe_project_type: Option<MediaFileProjectType>,
pub public_bucket_directory_hash: &'a str,
pub maybe_public_bucket_prefix: Option<&'a str>,
pub maybe_public_bucket_extension: Option<&'a str>,
@@ -46,9 +37,6 @@ pub async fn updated_media_file_stored_cloud_contents(args: UpdateArgs<'_>) -> A
r#"
UPDATE media_files
SET
media_class = ?,
maybe_project_type = ?,
public_bucket_directory_hash = ?,
maybe_public_bucket_prefix = ?,
maybe_public_bucket_extension = ?,
@@ -60,9 +48,6 @@ pub async fn updated_media_file_stored_cloud_contents(args: UpdateArgs<'_>) -> A
WHERE token = ?
LIMIT 1
"#,
args.media_class.to_str(),
args.maybe_project_type.map(|project_type| project_type.to_str()),
args.public_bucket_directory_hash,
args.maybe_public_bucket_prefix,
args.maybe_public_bucket_extension,
@@ -1,83 +0,0 @@
use std::marker::PhantomData;
use sqlx::{Executor, MySql};
use enums::by_table::media_files::media_file_class::MediaFileClass;
use enums::by_table::media_files::media_file_project_type::MediaFileProjectType;
use enums::by_table::media_files::media_file_type::MediaFileType;
use tokens::tokens::media_files::MediaFileToken;
pub struct UpdateProjectMediaFileContentsArgs<'e, 'c, E>
where
E: 'e + Executor<'c, Database = MySql>,
{
pub media_file_token: &'e MediaFileToken,
/// The class/type/project-type the record should hold after the update.
/// Also backfills legacy records that predate the `project` class.
pub media_class: MediaFileClass,
pub media_type: MediaFileType,
pub project_type: MediaFileProjectType,
pub public_bucket_directory_hash: &'e str,
pub maybe_public_bucket_prefix: Option<&'e str>,
pub maybe_public_bucket_extension: Option<&'e str>,
pub maybe_mime_type: Option<&'e str>,
pub file_size_bytes: u64,
pub sha256_checksum: &'e str,
pub update_ip_address: &'e str,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
}
/// Point a project media file record at freshly uploaded bucket contents.
pub async fn update_project_media_file_contents<'e, 'c: 'e, E>(
args: UpdateProjectMediaFileContentsArgs<'e, 'c, E>,
) -> Result<(), sqlx::Error>
where
E: 'e + Executor<'c, Database = MySql>,
{
sqlx::query!(
r#"
UPDATE media_files
SET
media_class = ?,
media_type = ?,
maybe_project_type = ?,
public_bucket_directory_hash = ?,
maybe_public_bucket_prefix = ?,
maybe_public_bucket_extension = ?,
maybe_mime_type = ?,
file_size_bytes = ?,
checksum_sha2 = ?,
creator_ip_address = ?
WHERE token = ?
LIMIT 1
"#,
args.media_class.to_str(),
args.media_type.to_str(),
args.project_type.to_str(),
args.public_bucket_directory_hash,
args.maybe_public_bucket_prefix,
args.maybe_public_bucket_extension,
args.maybe_mime_type,
args.file_size_bytes,
args.sha256_checksum,
args.update_ip_address,
args.media_file_token.as_str(),
)
.execute(args.mysql_executor)
.await?;
Ok(())
}
@@ -1,62 +0,0 @@
use std::marker::PhantomData;
use sqlx::{Executor, MySql};
use enums::by_table::media_files::media_file_project_type::MediaFileProjectType;
use enums::by_table::media_files::media_file_type::MediaFileType;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::users::UserToken;
#[derive(Debug, Clone)]
pub struct MediaFileForProjectUpdate {
pub maybe_creator_user_token: Option<UserToken>,
pub maybe_creator_anonymous_visitor_token: Option<AnonymousVisitorTrackingToken>,
pub media_type: MediaFileType,
pub maybe_project_type: Option<MediaFileProjectType>,
}
pub struct LookupMediaFileForProjectUpdateArgs<'e, 'c, E>
where
E: 'e + Executor<'c, Database = MySql>,
{
pub media_file_token: &'e MediaFileToken,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
}
/// Minimal lookup of a media file for a project-save (overwrite) request:
/// the creator tokens (for ownership checks) and the media/project types
/// (to confirm the record is the right kind of project document).
/// Excludes soft-deleted rows.
pub async fn lookup_media_file_for_project_update<'e, 'c: 'e, E>(
args: LookupMediaFileForProjectUpdateArgs<'e, 'c, E>,
) -> Result<Option<MediaFileForProjectUpdate>, sqlx::Error>
where
E: 'e + Executor<'c, Database = MySql>,
{
let result = sqlx::query!(
r#"
SELECT
maybe_creator_user_token as `maybe_creator_user_token: UserToken`,
maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: AnonymousVisitorTrackingToken`,
media_type as `media_type: MediaFileType`,
maybe_project_type as `maybe_project_type: MediaFileProjectType`
FROM media_files
WHERE token = ?
AND user_deleted_at IS NULL
AND mod_deleted_at IS NULL
LIMIT 1
"#,
args.media_file_token.as_str(),
)
.fetch_optional(args.mysql_executor)
.await?;
Ok(result.map(|record| MediaFileForProjectUpdate {
maybe_creator_user_token: record.maybe_creator_user_token,
maybe_creator_anonymous_visitor_token: record.maybe_creator_anonymous_visitor_token,
media_type: record.media_type,
maybe_project_type: record.maybe_project_type,
}))
}
@@ -3,5 +3,4 @@ pub mod batch_get_media_files;
pub mod batch_get_media_files_by_tokens;
pub mod get_media_file;
pub mod get_media_file_for_inference;
pub mod lookup_media_file_for_cover_check;
pub mod lookup_media_file_for_project_update;
pub mod lookup_media_file_for_cover_check;
@@ -1,375 +0,0 @@
use std::marker::PhantomData;
use chrono::{DateTime, Utc};
use sqlx::{Executor, MySql};
use enums::by_table::media_files::media_file_class::MediaFileClass;
use enums::by_table::media_files::media_file_project_type::MediaFileProjectType;
use enums::by_table::media_files::media_file_type::MediaFileType;
use enums::common::visibility::Visibility;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::users::UserToken;
pub struct ProjectMediaFileListPage {
pub records: Vec<ProjectMediaFileListItem>,
/// ID of the first record (for building the "previous" cursor).
pub first_id: Option<u64>,
/// ID of the last record (for building the "next" cursor).
pub last_id: Option<u64>,
}
pub struct ProjectMediaFileListItem {
pub id: u64,
pub token: MediaFileToken,
pub media_class: MediaFileClass,
pub project_type: MediaFileProjectType,
pub media_type: MediaFileType,
pub public_bucket_directory_hash: String,
pub maybe_public_bucket_prefix: Option<String>,
pub maybe_public_bucket_extension: Option<String>,
pub maybe_creator_user_token: Option<UserToken>,
pub maybe_creator_username: Option<String>,
pub maybe_creator_display_name: Option<String>,
pub maybe_creator_gravatar_hash: Option<String>,
pub maybe_file_cover_image_public_bucket_hash: Option<String>,
pub maybe_file_cover_image_public_bucket_prefix: Option<String>,
pub maybe_file_cover_image_public_bucket_extension: Option<String>,
pub creator_set_visibility: Visibility,
pub maybe_title: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
pub struct ListSessionProjectMediaFilesArgs<'a, 'c, E>
where
E: 'a + Executor<'c, Database = MySql>,
{
pub user_token: &'a UserToken,
/// Optional filter on the specific project document type.
pub maybe_filter_project_type: Option<MediaFileProjectType>,
pub limit: usize,
/// Keyset cursor: a `media_files.id` from a previous page.
pub maybe_cursor_id: Option<u64>,
pub cursor_is_reversed: bool,
pub sort_ascending: bool,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
}
/// List the session user's project files (`media_class = 'project'`), newest first
/// by default, with keyset pagination on `id`.
///
/// Records with a NULL `maybe_project_type` are excluded so `project_type` is
/// always present in the results.
pub async fn list_session_project_media_files<'a, 'c: 'a, E>(
args: ListSessionProjectMediaFilesArgs<'a, 'c, E>,
) -> Result<ProjectMediaFileListPage, sqlx::Error>
where
E: 'a + Executor<'c, Database = MySql>,
{
// A reversed cursor walks the opposite direction of the sort, so the effective
// scan direction is the XOR of the two flags (same scheme as `list_media_files`).
let ascending = args.sort_ascending != args.cursor_is_reversed;
let limit = args.limit as u64;
let records = if ascending {
// NB: IDs start at 1, so a zero sentinel means "no cursor".
let cursor_id = args.maybe_cursor_id.unwrap_or(0);
select_page_ascending(
args.user_token,
args.maybe_filter_project_type,
cursor_id,
limit,
args.mysql_executor,
).await?
} else {
let cursor_id = args.maybe_cursor_id.unwrap_or(u64::MAX);
select_page_descending(
args.user_token,
args.maybe_filter_project_type,
cursor_id,
limit,
args.mysql_executor,
).await?
};
let first_id = records.first().map(|record| record.id);
let last_id = records.last().map(|record| record.id);
Ok(ProjectMediaFileListPage {
records,
first_id,
last_id,
})
}
// NB: The four queries below are intentionally duplicated so that sqlx can check
// them at compile time (no QueryBuilder). Keep the SELECT lists and predicates
// in sync; they differ only in the `maybe_project_type = ?` filter, the cursor
// comparison direction, and the sort order.
async fn select_page_ascending<'a, 'c: 'a, E>(
user_token: &'a UserToken,
maybe_filter_project_type: Option<MediaFileProjectType>,
cursor_id: u64,
limit: u64,
mysql_executor: E,
) -> Result<Vec<ProjectMediaFileListItem>, sqlx::Error>
where
E: 'a + Executor<'c, Database = MySql>,
{
match maybe_filter_project_type {
Some(project_type) => {
sqlx::query_as!(
ProjectMediaFileListItem,
r#"
SELECT
m.id as `id: u64`,
m.token as `token: tokens::tokens::media_files::MediaFileToken`,
m.media_class as `media_class: enums::by_table::media_files::media_file_class::MediaFileClass`,
m.maybe_project_type as `project_type!: enums::by_table::media_files::media_file_project_type::MediaFileProjectType`,
m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,
m.public_bucket_directory_hash,
m.maybe_public_bucket_prefix,
m.maybe_public_bucket_extension,
users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,
users.username as maybe_creator_username,
users.display_name as maybe_creator_display_name,
users.email_gravatar_hash as maybe_creator_gravatar_hash,
media_file_cover_image.public_bucket_directory_hash as maybe_file_cover_image_public_bucket_hash,
media_file_cover_image.maybe_public_bucket_prefix as maybe_file_cover_image_public_bucket_prefix,
media_file_cover_image.maybe_public_bucket_extension as maybe_file_cover_image_public_bucket_extension,
m.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,
m.maybe_title,
m.created_at,
m.updated_at
FROM media_files AS m FORCE INDEX (fk_maybe_creator_user_token)
LEFT OUTER JOIN users
ON m.maybe_creator_user_token = users.token
LEFT OUTER JOIN media_files as media_file_cover_image
ON media_file_cover_image.token = m.maybe_cover_image_media_file_token
WHERE
m.maybe_creator_user_token = ?
AND m.media_class = 'project'
AND m.maybe_project_type IS NOT NULL
AND m.maybe_project_type = ?
AND m.user_deleted_at IS NULL
AND m.mod_deleted_at IS NULL
AND NOT m.is_intermediate_system_file
AND m.id > ?
ORDER BY m.id ASC
LIMIT ?
"#,
user_token,
project_type.to_str(),
cursor_id,
limit,
)
.fetch_all(mysql_executor)
.await
}
None => {
sqlx::query_as!(
ProjectMediaFileListItem,
r#"
SELECT
m.id as `id: u64`,
m.token as `token: tokens::tokens::media_files::MediaFileToken`,
m.media_class as `media_class: enums::by_table::media_files::media_file_class::MediaFileClass`,
m.maybe_project_type as `project_type!: enums::by_table::media_files::media_file_project_type::MediaFileProjectType`,
m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,
m.public_bucket_directory_hash,
m.maybe_public_bucket_prefix,
m.maybe_public_bucket_extension,
users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,
users.username as maybe_creator_username,
users.display_name as maybe_creator_display_name,
users.email_gravatar_hash as maybe_creator_gravatar_hash,
media_file_cover_image.public_bucket_directory_hash as maybe_file_cover_image_public_bucket_hash,
media_file_cover_image.maybe_public_bucket_prefix as maybe_file_cover_image_public_bucket_prefix,
media_file_cover_image.maybe_public_bucket_extension as maybe_file_cover_image_public_bucket_extension,
m.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,
m.maybe_title,
m.created_at,
m.updated_at
FROM media_files AS m FORCE INDEX (fk_maybe_creator_user_token)
LEFT OUTER JOIN users
ON m.maybe_creator_user_token = users.token
LEFT OUTER JOIN media_files as media_file_cover_image
ON media_file_cover_image.token = m.maybe_cover_image_media_file_token
WHERE
m.maybe_creator_user_token = ?
AND m.media_class = 'project'
AND m.maybe_project_type IS NOT NULL
AND m.user_deleted_at IS NULL
AND m.mod_deleted_at IS NULL
AND NOT m.is_intermediate_system_file
AND m.id > ?
ORDER BY m.id ASC
LIMIT ?
"#,
user_token,
cursor_id,
limit,
)
.fetch_all(mysql_executor)
.await
}
}
}
async fn select_page_descending<'a, 'c: 'a, E>(
user_token: &'a UserToken,
maybe_filter_project_type: Option<MediaFileProjectType>,
cursor_id: u64,
limit: u64,
mysql_executor: E,
) -> Result<Vec<ProjectMediaFileListItem>, sqlx::Error>
where
E: 'a + Executor<'c, Database = MySql>,
{
match maybe_filter_project_type {
Some(project_type) => {
sqlx::query_as!(
ProjectMediaFileListItem,
r#"
SELECT
m.id as `id: u64`,
m.token as `token: tokens::tokens::media_files::MediaFileToken`,
m.media_class as `media_class: enums::by_table::media_files::media_file_class::MediaFileClass`,
m.maybe_project_type as `project_type!: enums::by_table::media_files::media_file_project_type::MediaFileProjectType`,
m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,
m.public_bucket_directory_hash,
m.maybe_public_bucket_prefix,
m.maybe_public_bucket_extension,
users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,
users.username as maybe_creator_username,
users.display_name as maybe_creator_display_name,
users.email_gravatar_hash as maybe_creator_gravatar_hash,
media_file_cover_image.public_bucket_directory_hash as maybe_file_cover_image_public_bucket_hash,
media_file_cover_image.maybe_public_bucket_prefix as maybe_file_cover_image_public_bucket_prefix,
media_file_cover_image.maybe_public_bucket_extension as maybe_file_cover_image_public_bucket_extension,
m.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,
m.maybe_title,
m.created_at,
m.updated_at
FROM media_files AS m FORCE INDEX (fk_maybe_creator_user_token)
LEFT OUTER JOIN users
ON m.maybe_creator_user_token = users.token
LEFT OUTER JOIN media_files as media_file_cover_image
ON media_file_cover_image.token = m.maybe_cover_image_media_file_token
WHERE
m.maybe_creator_user_token = ?
AND m.media_class = 'project'
AND m.maybe_project_type IS NOT NULL
AND m.maybe_project_type = ?
AND m.user_deleted_at IS NULL
AND m.mod_deleted_at IS NULL
AND NOT m.is_intermediate_system_file
AND m.id < ?
ORDER BY m.id DESC
LIMIT ?
"#,
user_token,
project_type.to_str(),
cursor_id,
limit,
)
.fetch_all(mysql_executor)
.await
}
None => {
sqlx::query_as!(
ProjectMediaFileListItem,
r#"
SELECT
m.id as `id: u64`,
m.token as `token: tokens::tokens::media_files::MediaFileToken`,
m.media_class as `media_class: enums::by_table::media_files::media_file_class::MediaFileClass`,
m.maybe_project_type as `project_type!: enums::by_table::media_files::media_file_project_type::MediaFileProjectType`,
m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,
m.public_bucket_directory_hash,
m.maybe_public_bucket_prefix,
m.maybe_public_bucket_extension,
users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,
users.username as maybe_creator_username,
users.display_name as maybe_creator_display_name,
users.email_gravatar_hash as maybe_creator_gravatar_hash,
media_file_cover_image.public_bucket_directory_hash as maybe_file_cover_image_public_bucket_hash,
media_file_cover_image.maybe_public_bucket_prefix as maybe_file_cover_image_public_bucket_prefix,
media_file_cover_image.maybe_public_bucket_extension as maybe_file_cover_image_public_bucket_extension,
m.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,
m.maybe_title,
m.created_at,
m.updated_at
FROM media_files AS m FORCE INDEX (fk_maybe_creator_user_token)
LEFT OUTER JOIN users
ON m.maybe_creator_user_token = users.token
LEFT OUTER JOIN media_files as media_file_cover_image
ON media_file_cover_image.token = m.maybe_cover_image_media_file_token
WHERE
m.maybe_creator_user_token = ?
AND m.media_class = 'project'
AND m.maybe_project_type IS NOT NULL
AND m.user_deleted_at IS NULL
AND m.mod_deleted_at IS NULL
AND NOT m.is_intermediate_system_file
AND m.id < ?
ORDER BY m.id DESC
LIMIT ?
"#,
user_token,
cursor_id,
limit,
)
.fetch_all(mysql_executor)
.await
}
}
}
@@ -4,4 +4,3 @@ pub mod list_media_files;
pub mod list_media_files_by_batch_token;
pub mod list_media_files_for_elastic_search_backfill_using_cursor;
pub mod list_media_files_for_user;
pub mod list_session_project_media_files;
@@ -4,3 +4,4 @@ pub mod edit;
pub mod get;
pub mod thumbnails;
pub mod list;
pub mod upsert;
@@ -0,0 +1 @@
pub mod upsert_media_file_from_file_upload;
@@ -0,0 +1,250 @@
use anyhow::anyhow;
use sqlx;
use sqlx::MySqlPool;
use enums::by_table::generic_synthetic_ids::id_category::IdCategory;
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_type::MediaFileType;
use enums::common::visibility::Visibility;
use errors::AnyhowResult;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::users::UserToken;
use crate::queries::generic_synthetic_ids::transactional_increment_generic_synthetic_id::transactional_increment_generic_synthetic_id;
pub enum UploadType {
Filesystem,
DeviceCaptureApi,
StorytellerEngine,
}
pub struct UpsertMediaFileFromUploadArgs<'a> {
pub pool: &'a MySqlPool,
/// If the media file token is present, we're upserting an existing media file.
pub maybe_media_file_token: Option<&'a MediaFileToken>,
pub maybe_media_class: Option<MediaFileClass>,
pub media_file_type: MediaFileType,
pub maybe_engine_category: Option<MediaFileEngineCategory>,
pub maybe_animation_type: Option<MediaFileAnimationType>,
pub maybe_creator_user_token: Option<&'a UserToken>,
pub maybe_creator_anonymous_visitor_token: Option<&'a AnonymousVisitorTrackingToken>,
pub creator_ip_address: &'a str,
pub creator_set_visibility: Visibility,
pub upload_type: UploadType,
pub maybe_mime_type: Option<&'a str>,
pub file_size_bytes: u64,
pub duration_millis: u64,
pub sha256_checksum: &'a str,
// TODO: Media duration.
//pub duration_millis: u64,
pub public_bucket_directory_hash: &'a str,
pub maybe_public_bucket_prefix: Option<&'a str>,
pub maybe_public_bucket_extension: Option<&'a str>,
}
pub async fn upsert_media_file_from_file_upload(
args: UpsertMediaFileFromUploadArgs<'_>
) -> AnyhowResult<(MediaFileToken, u64)>
{
let token = args.maybe_media_file_token
.map(|token| token.clone())
.unwrap_or_else(|| MediaFileToken::generate());
let origin_category = match args.upload_type {
UploadType::Filesystem => MediaFileOriginCategory::Upload,
UploadType::DeviceCaptureApi => MediaFileOriginCategory::DeviceApi,
UploadType::StorytellerEngine => MediaFileOriginCategory::StorytellerStudio,
};
let mut maybe_creator_file_synthetic_id : Option<u64> = None;
let mut maybe_creator_category_synthetic_id : Option<u64> = None;
let mut transaction = args.pool.begin().await?;
if let Some(user_token) = args.maybe_creator_user_token.as_deref() {
let next_media_file_id = transactional_increment_generic_synthetic_id(
&user_token,
IdCategory::MediaFile,
&mut transaction
).await?;
let next_voice_conversion_id = transactional_increment_generic_synthetic_id(
&user_token,
IdCategory::FileUpload,
&mut transaction
).await?;
maybe_creator_file_synthetic_id = Some(next_media_file_id);
maybe_creator_category_synthetic_id = Some(next_voice_conversion_id);
}
const ORIGIN_PRODUCT_CATEGORY : MediaFileOriginProductCategory = MediaFileOriginProductCategory::Unknown;
let media_class = args.maybe_media_class.unwrap_or(MediaFileClass::Unknown);
let record_id = {
let query_result = sqlx::query!(
r#"
INSERT INTO media_files
SET
token = ?,
media_class = ?,
media_type = ?,
origin_category = ?,
origin_product_category = ?,
maybe_engine_category = ?,
maybe_animation_type = ?,
maybe_mime_type = ?,
file_size_bytes = ?,
checksum_sha2 = ?,
public_bucket_directory_hash = ?,
maybe_public_bucket_prefix = ?,
maybe_public_bucket_extension = ?,
maybe_creator_user_token = ?,
maybe_creator_anonymous_visitor_token = ?,
creator_ip_address = ?,
creator_set_visibility = ?,
maybe_creator_file_synthetic_id = ?,
maybe_creator_category_synthetic_id = ?,
is_user_upload = TRUE,
maybe_origin_model_type = NULL,
maybe_origin_model_token = NULL
ON DUPLICATE KEY UPDATE
media_class = ?,
media_type = ?,
origin_category = ?,
origin_product_category = ?,
maybe_engine_category = ?,
maybe_animation_type = ?,
maybe_mime_type = ?,
file_size_bytes = ?,
checksum_sha2 = ?,
public_bucket_directory_hash = ?,
maybe_public_bucket_prefix = ?,
maybe_public_bucket_extension = ?,
maybe_creator_user_token = ?,
maybe_creator_anonymous_visitor_token = ?,
creator_ip_address = ?,
creator_set_visibility = ?,
maybe_creator_file_synthetic_id = ?,
maybe_creator_category_synthetic_id = ?,
is_user_upload = TRUE,
maybe_origin_model_type = NULL,
maybe_origin_model_token = NULL
"#,
token.as_str(),
media_class.to_str(),
args.media_file_type.to_str(),
origin_category.to_str(),
ORIGIN_PRODUCT_CATEGORY.to_str(),
args.maybe_engine_category.map(|s| s.to_str()),
args.maybe_animation_type.map(|s| s.to_str()),
args.maybe_mime_type,
args.file_size_bytes,
args.sha256_checksum,
args.public_bucket_directory_hash,
args.maybe_public_bucket_prefix,
args.maybe_public_bucket_extension,
args.maybe_creator_user_token,
args.maybe_creator_anonymous_visitor_token,
args.creator_ip_address,
args.creator_set_visibility.to_str(),
maybe_creator_file_synthetic_id,
maybe_creator_category_synthetic_id,
// Duplicate key
media_class.to_str(),
args.media_file_type.to_str(),
origin_category.to_str(),
ORIGIN_PRODUCT_CATEGORY.to_str(),
args.maybe_engine_category.map(|s| s.to_str()),
args.maybe_animation_type.map(|s| s.to_str()),
args.maybe_mime_type,
args.file_size_bytes,
args.sha256_checksum,
args.public_bucket_directory_hash,
args.maybe_public_bucket_prefix,
args.maybe_public_bucket_extension,
args.maybe_creator_user_token,
args.maybe_creator_anonymous_visitor_token,
args.creator_ip_address,
args.creator_set_visibility.to_str(),
maybe_creator_file_synthetic_id,
maybe_creator_category_synthetic_id,
)
.execute(&mut *transaction)
.await;
let record_id = match query_result {
Ok(res) => {
res.last_insert_id()
},
Err(err) => {
// TODO: handle better
//transaction.rollback().await?;
return Err(anyhow!("Mysql error: {:?}", err));
}
};
record_id
};
transaction.commit().await?;
Ok((token, record_id))
}
@@ -32,6 +32,7 @@ pub mod prompts;
pub mod public_event_feed;
pub mod staff_audit_logs;
pub mod stats;
pub mod tags;
pub mod trending_model_analytics;
pub mod tts;
pub mod twitch;
@@ -0,0 +1,27 @@
use crate::errors::database_insert_error::DatabaseInsertError;
use crate::utils::transactor::Transactor;
use tokens::tokens::tags::TagToken;
pub async fn create_tag(
tag: &str,
transactor: Transactor<'_, '_>,
) -> Result<TagToken, DatabaseInsertError>
{
let tag = tag.trim().to_lowercase();
let token = TagToken::generate();
let query = sqlx::query!(
r#"
INSERT INTO tags
SET
token = ?,
tag_value = ?
"#,
token,
tag,
);
let _r = transactor.execute(query).await?;
Ok(token)
}
@@ -0,0 +1,64 @@
use composite_identifiers::by_table::tag_uses::tag_use_entity::TagUseEntity;
use errors::AnyhowResult;
use tokens::tokens::tags::TagToken;
use crate::utils::transactor::Transactor;
pub struct EntityTag {
pub token: TagToken,
pub tag_value: String,
}
pub async fn list_tags_for_entity(
tag_entity: TagUseEntity,
transactor: Transactor<'_, '_>,
) -> AnyhowResult<Vec<EntityTag>> {
let (entity_type, entity_token) = tag_entity.get_composite_keys();
let query = sqlx::query_as!(
RawTag,
r#"
SELECT
t.token as `token: tokens::tokens::tags::TagToken`,
t.tag_value
FROM
tags AS t
JOIN tag_uses AS u
ON t.token = u.tag_token
WHERE
u.entity_type = ?
AND u.entity_token = ?
AND u.maybe_deleted_at IS NULL
ORDER BY t.id DESC
LIMIT 500
"#,
entity_type,
entity_token
);
let results = match transactor {
Transactor::Pool { pool } => {
query.fetch_all(pool).await?
},
Transactor::Connection { connection } => {
query.fetch_all(connection).await?
},
Transactor::Transaction { transaction } => {
query.fetch_all(&mut **transaction).await?
},
};
Ok(results.into_iter()
.map(|record| EntityTag {
token: record.token,
tag_value: record.tag_value,
})
.collect())
}
struct RawTag {
token: TagToken,
tag_value: String,
}
@@ -0,0 +1,4 @@
pub mod create_tag;
pub mod list_tags_for_entity;
pub mod select_matching_tags;
pub mod update_tags_for_entity;
@@ -0,0 +1,78 @@
use crate::utils::transactor::Transactor;
use errors::AnyhowResult;
use sqlx::mysql::MySqlRow;
use sqlx::{FromRow, MySql, QueryBuilder, Row};
use tokens::tokens::tags::TagToken;
pub struct MatchingTag {
pub token: TagToken,
pub tag_value: String,
}
pub async fn select_matching_tags(
tag_values: &[String],
transactor: Transactor<'_, '_>,
) -> AnyhowResult<Vec<MatchingTag>> {
if tag_values.is_empty() {
return Ok(Vec::new()) // This prevents a SQL error when the IN clause is empty.
}
let tag_values = tag_values.iter()
.map(|tag| tag.trim().to_lowercase())
.collect::<Vec<_>>();
let mut query_builder: QueryBuilder<MySql> = QueryBuilder::new(
r#"
SELECT
token,
tag_value
FROM
tags
WHERE
tag_value IN (
"#,
);
let mut separated = query_builder.separated(", ");
for tag_value in tag_values.iter() {
separated.push_bind(tag_value);
}
separated.push_unseparated(") ");
let query = query_builder.build_query_as::<RawTag>();
let results = match transactor {
Transactor::Pool { pool } => {
query.fetch_all(pool).await?
},
Transactor::Connection { connection } => {
query.fetch_all(connection).await?
},
Transactor::Transaction { transaction } => {
query.fetch_all(&mut **transaction).await?
},
};
Ok(results.into_iter()
.map(|record| MatchingTag {
token: record.token,
tag_value: record.tag_value,
})
.collect())
}
struct RawTag {
token: TagToken,
tag_value: String,
}
impl FromRow<'_, MySqlRow> for RawTag {
fn from_row(row: &MySqlRow) -> Result<Self, sqlx::Error> {
Ok(Self {
token: TagToken::new(row.try_get("token")?),
tag_value: row.try_get("tag_value")?,
})
}
}
@@ -0,0 +1,111 @@
use composite_identifiers::by_table::tag_uses::tag_use_entity::TagUseEntity;
use errors::AnyhowResult;
use sqlx::pool::PoolConnection;
use sqlx::{Acquire, Executor, MySql, QueryBuilder, Transaction};
use tokens::tokens::tags::TagToken;
pub async fn update_tags_for_entity(
entity: TagUseEntity,
tags: &[TagToken],
mysql_connection: &mut PoolConnection<MySql>,
) -> AnyhowResult<()>
{
if tags.is_empty() {
delete_all_query(&entity, mysql_connection).await?;
return Ok(());
}
let mut transaction = mysql_connection.begin().await?;
delete_query(&entity, tags, &mut transaction).await?;
insert_query(&entity, tags, &mut transaction).await?;
transaction.commit().await?;
Ok(())
}
async fn delete_query(
entity: &TagUseEntity,
tags: &[TagToken],
transaction: &mut Transaction<'_, MySql>,
) -> AnyhowResult<()> {
let mut query_builder: QueryBuilder<MySql> = QueryBuilder::new("DELETE FROM tag_uses ");
query_builder.push(" WHERE entity_type = ");;
query_builder.push_bind(entity.get_entity_type().to_str());
query_builder.push(" AND entity_token = ");;
query_builder.push_bind(entity.get_entity_token_str());
query_builder.push(" AND tag_token NOT IN ( ");
let mut separated = query_builder.separated(", ");
for tag in tags.iter() {
separated.push_bind(tag.as_str());
}
separated.push_unseparated(") ");
let query = query_builder.build();
transaction.execute(query).await?;
Ok(())
}
async fn delete_all_query(
entity: &TagUseEntity,
mysql_connection: &mut PoolConnection<MySql>,
) -> AnyhowResult<()> {
let query = sqlx::query!(
r#"
DELETE FROM tag_uses
WHERE entity_type = ?
AND entity_token = ?
LIMIT 1
"#,
entity.get_entity_type().to_str(),
entity.get_entity_token_str(),
);
mysql_connection.execute(query).await?;
Ok(())
}
async fn insert_query(
entity: &TagUseEntity,
tags: &[TagToken],
transaction: &mut Transaction<'_, MySql>,
) -> AnyhowResult<()> {
if tags.is_empty() {
return Ok(()) // This prevents a SQL error when the IN clause is empty.
}
// NB: Insert ignore will insert non-duplicate rows and silently ignore duplicates
let mut query_builder: QueryBuilder<MySql> = QueryBuilder::new("INSERT IGNORE INTO tag_uses ");
query_builder.push(" (entity_type, entity_token, tag_token) VALUES ");
for (i, tag) in tags.iter().enumerate() {
query_builder.push(" ( ");;
query_builder.push_bind(entity.get_entity_type().to_str());
query_builder.push(", ");;
query_builder.push_bind(entity.get_entity_token_str());
query_builder.push(", ");;
query_builder.push_bind(tag.as_str());
query_builder.push(" ) ");
if i < tags.len() - 1 {
query_builder.push(", ");
}
}
let query = query_builder.build();
transaction.execute(query).await?;
Ok(())
}
@@ -171,15 +171,11 @@ impl RawUserBookmarkRecord {
MediaFileType::Bvh => Some("3d file".to_string()),
MediaFileType::Fbx => Some("3d file".to_string()),
MediaFileType::Obj => Some("3d file".to_string()),
MediaFileType::Ply => Some("3d splat".to_string()),
MediaFileType::Glb => Some("3d file".to_string()),
MediaFileType::Gltf => Some("3d file".to_string()),
MediaFileType::Spz => Some("3d splat".to_string()),
MediaFileType::SceneRon => Some("3d scene".to_string()),
MediaFileType::SceneJson => Some("3d scene".to_string()),
MediaFileType::MoodJson => Some("mood board".to_string()),
MediaFileType::TimelineJson => Some("video timeline".to_string()),
MediaFileType::EditorJson => Some("2d editor project".to_string()),
MediaFileType::Pmd => Some("polygon data".to_string()),
MediaFileType::Vmd => Some("motion data".to_string()),
MediaFileType::Pmx => Some("pmx data".to_string()),
@@ -1,3 +1,4 @@
pub mod audit_logs;
pub mod batch_generations;
pub mod featured_items;
pub mod tag_uses;
@@ -0,0 +1 @@
pub mod tag_use_entity;
@@ -0,0 +1,40 @@
use enums::by_table::tag_uses::tag_use_entity_type::TagUseEntityType;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::model_weights::ModelWeightToken;
// TODO(bt, 2024-01-16): A ref type with <'a> lifetime of inner data instead of ownership?
pub enum TagUseEntity {
MediaFile(MediaFileToken),
ModelWeight(ModelWeightToken),
}
// TODO(bt, 2024-01-16): Codegen? Make traits for these? Maybe overkill.
impl TagUseEntity {
pub fn from_entity_type_and_token(entity_type: TagUseEntityType, token: &str) -> Self {
match entity_type {
TagUseEntityType::ModelWeight => Self::ModelWeight(ModelWeightToken::new_from_str(token)),
TagUseEntityType::MediaFile => Self::MediaFile(MediaFileToken::new_from_str(token)),
}
}
pub fn get_entity_type(&self) -> TagUseEntityType {
match self {
Self::MediaFile(_) => TagUseEntityType::MediaFile,
Self::ModelWeight(_) => TagUseEntityType::ModelWeight,
}
}
pub fn get_entity_token_str(&self) -> &str {
match self {
Self::MediaFile(token) => token.as_str(),
Self::ModelWeight(token) => token.as_str(),
}
}
pub fn get_composite_keys(&self) -> (TagUseEntityType, &str) {
match self {
Self::MediaFile(token) => (TagUseEntityType::MediaFile, token.as_str()),
Self::ModelWeight(token) => (TagUseEntityType::ModelWeight, token.as_str()),
}
}
}
@@ -279,8 +279,6 @@ pub enum InferenceModelType {
Marble1p1,
#[serde(rename = "marble_1p1_plus")]
Marble1p1Plus,
#[serde(rename = "triposplat")]
TripoSplat,
}
// TODO(bt, 2022-12-21): This desperately needs MySQL integration tests!
@@ -413,7 +411,6 @@ impl InferenceModelType {
Self::Marble1p0Draft => "marble_1p0_draft",
Self::Marble1p1 => "marble_1p1",
Self::Marble1p1Plus => "marble_1p1_plus",
Self::TripoSplat => "triposplat",
}
}
@@ -541,7 +538,6 @@ impl InferenceModelType {
"marble_1p0_draft" => Ok(Self::Marble1p0Draft),
"marble_1p1" => Ok(Self::Marble1p1),
"marble_1p1_plus" => Ok(Self::Marble1p1Plus),
"triposplat" => Ok(Self::TripoSplat),
_ => Err(format!("invalid value: {:?}", value)),
}
@@ -673,7 +669,6 @@ impl InferenceModelType {
Self::Marble1p0Draft,
Self::Marble1p1,
Self::Marble1p1Plus,
Self::TripoSplat,
])
}
@@ -788,7 +783,6 @@ impl InferenceModelType {
CommonModelType::Marble1p0Draft => Self::Marble1p0Draft,
CommonModelType::Marble1p1 => Self::Marble1p1,
CommonModelType::Marble1p1Plus => Self::Marble1p1Plus,
CommonModelType::TripoSplat => Self::TripoSplat,
}
}
}
@@ -924,7 +918,6 @@ mod tests {
assert_serialization(InferenceModelType::Marble1p0Draft, "marble_1p0_draft");
assert_serialization(InferenceModelType::Marble1p1, "marble_1p1");
assert_serialization(InferenceModelType::Marble1p1Plus, "marble_1p1_plus");
assert_serialization(InferenceModelType::TripoSplat, "triposplat");
}
#[test]
@@ -1051,7 +1044,6 @@ mod tests {
assert_eq!(InferenceModelType::Marble1p0Draft.to_str(), "marble_1p0_draft");
assert_eq!(InferenceModelType::Marble1p1.to_str(), "marble_1p1");
assert_eq!(InferenceModelType::Marble1p1Plus.to_str(), "marble_1p1_plus");
assert_eq!(InferenceModelType::TripoSplat.to_str(), "triposplat");
}
#[test]
@@ -1176,7 +1168,6 @@ mod tests {
assert_eq!(InferenceModelType::from_str("marble_1p0_draft").unwrap(), InferenceModelType::Marble1p0Draft);
assert_eq!(InferenceModelType::from_str("marble_1p1").unwrap(), InferenceModelType::Marble1p1);
assert_eq!(InferenceModelType::from_str("marble_1p1_plus").unwrap(), InferenceModelType::Marble1p1Plus);
assert_eq!(InferenceModelType::from_str("triposplat").unwrap(), InferenceModelType::TripoSplat);
}
#[test]
@@ -27,21 +27,7 @@ pub enum MediaFileClass {
Video,
/// 3D engine data: glb, gltf, etc.
/// Being split into `Mesh`, `Splat`, and `Project`. Do not write new records
/// with this value; it remains readable until existing rows are backfilled.
#[deprecated(note = "Split into Mesh, Splat, and Project. Read-only until backfilled.")]
Dimensional,
/// 3D mesh assets: glb, gltf, fbx, obj, pmx, pmd, etc.
Mesh,
/// Gaussian splats: spz, etc.
Splat,
/// Internal Artcraft project documents (JSON): 3D editor scenes, mood boards,
/// graph workflows, video editor timelines, etc.
/// The specific kind is stored in `maybe_project_type`.
Project,
}
// TODO(bt, 2022-12-21): This desperately needs MySQL integration tests!
@@ -58,9 +44,6 @@ impl MediaFileClass {
Self::Image => "image",
Self::Video => "video",
Self::Dimensional => "dimensional",
Self::Mesh => "mesh",
Self::Splat => "splat",
Self::Project => "project",
}
}
@@ -71,9 +54,6 @@ impl MediaFileClass {
"image" => Ok(Self::Image),
"video" => Ok(Self::Video),
"dimensional" => Ok(Self::Dimensional),
"mesh" => Ok(Self::Mesh),
"splat" => Ok(Self::Splat),
"project" => Ok(Self::Project),
_ => Err(format!("invalid value: {:?}", value)),
}
}
@@ -87,9 +67,6 @@ impl MediaFileClass {
Self::Image,
Self::Video,
Self::Dimensional,
Self::Mesh,
Self::Splat,
Self::Project,
])
}
}
@@ -109,9 +86,6 @@ mod tests {
assert_serialization(MediaFileClass::Image, "image");
assert_serialization(MediaFileClass::Video, "video");
assert_serialization(MediaFileClass::Dimensional, "dimensional");
assert_serialization(MediaFileClass::Mesh, "mesh");
assert_serialization(MediaFileClass::Splat, "splat");
assert_serialization(MediaFileClass::Project, "project");
}
}
@@ -125,9 +99,6 @@ mod tests {
assert_eq!(MediaFileClass::Image.to_str(), "image");
assert_eq!(MediaFileClass::Video.to_str(), "video");
assert_eq!(MediaFileClass::Dimensional.to_str(), "dimensional");
assert_eq!(MediaFileClass::Mesh.to_str(), "mesh");
assert_eq!(MediaFileClass::Splat.to_str(), "splat");
assert_eq!(MediaFileClass::Project.to_str(), "project");
}
#[test]
@@ -137,9 +108,6 @@ mod tests {
assert_eq!(MediaFileClass::from_str("image").unwrap(), MediaFileClass::Image);
assert_eq!(MediaFileClass::from_str("video").unwrap(), MediaFileClass::Video);
assert_eq!(MediaFileClass::from_str("dimensional").unwrap(), MediaFileClass::Dimensional);
assert_eq!(MediaFileClass::from_str("mesh").unwrap(), MediaFileClass::Mesh);
assert_eq!(MediaFileClass::from_str("splat").unwrap(), MediaFileClass::Splat);
assert_eq!(MediaFileClass::from_str("project").unwrap(), MediaFileClass::Project);
assert!(MediaFileClass::from_str("foo").is_err());
}
}
@@ -150,15 +118,12 @@ mod tests {
#[test]
fn all_variants() {
let mut variants = MediaFileClass::all_variants();
assert_eq!(variants.len(), 8);
assert_eq!(variants.len(), 5);
assert_eq!(variants.pop_first(), Some(MediaFileClass::Unknown));
assert_eq!(variants.pop_first(), Some(MediaFileClass::Audio));
assert_eq!(variants.pop_first(), Some(MediaFileClass::Image));
assert_eq!(variants.pop_first(), Some(MediaFileClass::Video));
assert_eq!(variants.pop_first(), Some(MediaFileClass::Dimensional));
assert_eq!(variants.pop_first(), Some(MediaFileClass::Mesh));
assert_eq!(variants.pop_first(), Some(MediaFileClass::Splat));
assert_eq!(variants.pop_first(), Some(MediaFileClass::Project));
assert_eq!(variants.pop_first(), None);
}
}
@@ -1,168 +0,0 @@
use std::collections::BTreeSet;
#[cfg(test)]
use strum::EnumCount;
#[cfg(test)]
use strum::EnumIter;
use utoipa::ToSchema;
/// Used in the `media_files` table in the nullable `maybe_project_type`
/// VARCHAR(32) field.
///
/// Identifies which kind of internal Artcraft project document a media file is.
/// Only set when `media_class = 'project'`. The file format lives in
/// `media_type` (`json` / legacy `scene_json`); the JSON payload carries its
/// own schema version.
///
/// DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY.
#[cfg_attr(test, derive(EnumIter, EnumCount))]
#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum MediaFileProjectType {
/// 3D editor scene descriptor: objects with locations, skyboxes, animations,
/// etc. — a full editable 3D scene.
/// NB: serde's snake_case renames this to "scene3d" (no underscore before
/// the digit), so the rename must be explicit.
#[serde(rename = "scene_3d")]
Scene3d,
/// Mood board: a collection of images on a lightweight graph.
MoodBoard,
/// Graph workflow: a node-graph pipeline of image and video generation steps.
Workflow,
/// Video editor project: a nonlinear video editing timeline.
VideoTimeline,
/// 2D editor project: a 2D image editing document.
/// NB: serde's snake_case renames this to "editor2d" (no underscore before
/// the digit), so the rename must be explicit.
#[serde(rename = "editor_2d")]
Editor2d,
}
impl_enum_display_and_debug_using_to_str!(MediaFileProjectType);
impl_mysql_enum_coders!(MediaFileProjectType);
impl_mysql_from_row!(MediaFileProjectType);
impl MediaFileProjectType {
pub const fn to_str(&self) -> &'static str {
match self {
Self::Scene3d => "scene_3d",
Self::MoodBoard => "mood_board",
Self::Workflow => "workflow",
Self::VideoTimeline => "video_timeline",
Self::Editor2d => "editor_2d",
}
}
pub fn from_str(value: &str) -> Result<Self, String> {
match value {
"scene_3d" => Ok(Self::Scene3d),
"mood_board" => Ok(Self::MoodBoard),
"workflow" => Ok(Self::Workflow),
"video_timeline" => Ok(Self::VideoTimeline),
"editor_2d" => Ok(Self::Editor2d),
_ => Err(format!("invalid value: {:?}", value)),
}
}
pub fn all_variants() -> BTreeSet<Self> {
// NB: BTreeSet is sorted
BTreeSet::from([
Self::Scene3d,
Self::MoodBoard,
Self::Workflow,
Self::VideoTimeline,
Self::Editor2d,
])
}
}
#[cfg(test)]
mod tests {
use crate::by_table::media_files::media_file_project_type::MediaFileProjectType;
use crate::test_helpers::assert_serialization;
mod serde {
use super::*;
#[test]
fn test_serialization() {
assert_serialization(MediaFileProjectType::Scene3d, "scene_3d");
assert_serialization(MediaFileProjectType::MoodBoard, "mood_board");
assert_serialization(MediaFileProjectType::Workflow, "workflow");
assert_serialization(MediaFileProjectType::VideoTimeline, "video_timeline");
assert_serialization(MediaFileProjectType::Editor2d, "editor_2d");
}
}
mod impl_methods {
use super::*;
#[test]
fn to_str() {
assert_eq!(MediaFileProjectType::Scene3d.to_str(), "scene_3d");
assert_eq!(MediaFileProjectType::MoodBoard.to_str(), "mood_board");
assert_eq!(MediaFileProjectType::Workflow.to_str(), "workflow");
assert_eq!(MediaFileProjectType::VideoTimeline.to_str(), "video_timeline");
assert_eq!(MediaFileProjectType::Editor2d.to_str(), "editor_2d");
}
#[test]
fn from_str() {
assert_eq!(MediaFileProjectType::from_str("scene_3d").unwrap(), MediaFileProjectType::Scene3d);
assert_eq!(MediaFileProjectType::from_str("mood_board").unwrap(), MediaFileProjectType::MoodBoard);
assert_eq!(MediaFileProjectType::from_str("workflow").unwrap(), MediaFileProjectType::Workflow);
assert_eq!(MediaFileProjectType::from_str("video_timeline").unwrap(), MediaFileProjectType::VideoTimeline);
assert_eq!(MediaFileProjectType::from_str("editor_2d").unwrap(), MediaFileProjectType::Editor2d);
assert!(MediaFileProjectType::from_str("foo").is_err());
}
}
mod manual_variant_checks {
use super::*;
#[test]
fn all_variants() {
let mut variants = MediaFileProjectType::all_variants();
assert_eq!(variants.len(), 5);
assert_eq!(variants.pop_first(), Some(MediaFileProjectType::Scene3d));
assert_eq!(variants.pop_first(), Some(MediaFileProjectType::MoodBoard));
assert_eq!(variants.pop_first(), Some(MediaFileProjectType::Workflow));
assert_eq!(variants.pop_first(), Some(MediaFileProjectType::VideoTimeline));
assert_eq!(variants.pop_first(), Some(MediaFileProjectType::Editor2d));
assert_eq!(variants.pop_first(), None);
}
}
mod mechanical_checks {
use super::*;
#[test]
fn variant_length() {
use strum::IntoEnumIterator;
assert_eq!(MediaFileProjectType::all_variants().len(), MediaFileProjectType::iter().len());
}
#[test]
fn round_trip() {
for variant in MediaFileProjectType::all_variants() {
assert_eq!(variant, MediaFileProjectType::from_str(variant.to_str()).unwrap());
assert_eq!(variant, MediaFileProjectType::from_str(&format!("{}", variant)).unwrap());
assert_eq!(variant, MediaFileProjectType::from_str(&format!("{:?}", variant)).unwrap());
}
}
#[test]
fn serialized_length_ok_for_database() {
const MAX_LENGTH : usize = 32;
for variant in MediaFileProjectType::all_variants() {
let serialized = variant.to_str();
assert!(serialized.len() > 0, "variant {:?} is too short", variant);
assert!(serialized.len() <= MAX_LENGTH, "variant {:?} is too long", variant);
}
}
}
}
@@ -40,9 +40,6 @@ pub enum MediaFileType {
/// Wavefront OBJ mesh files
Obj,
/// PLY (polygon file format) files; also used for Gaussian splats.
Ply,
/// glTF binary files (for Bevy)
Glb,
@@ -61,15 +58,6 @@ pub enum MediaFileType {
/// Alternate scene files.
SceneJson,
/// Mood board project documents (JSON).
MoodJson,
/// Video editor timeline project documents (JSON).
TimelineJson,
/// 2D editor project documents (JSON).
EditorJson,
/// "Polygon Model Data", character data for MikuMikuDance
/// See: https://mikumikudance.fandom.com/wiki/MMD:Polygon_Model_Data
Pmd,
@@ -183,28 +171,21 @@ impl MediaFileType {
| Self::Webm
| Self::Mov => MediaFileClass::Video,
Self::Fbx
Self::Bvh
| Self::Fbx
| Self::Obj
| Self::Glb
| Self::Gltf
| Self::Spz
| Self::SceneRon
| Self::SceneJson
| Self::Pmd
| Self::Pmx => MediaFileClass::Mesh,
Self::Spz
| Self::Ply => MediaFileClass::Splat,
// Animation and auxiliary data formats with no dedicated class yet.
#[allow(deprecated)]
Self::Bvh
| Self::Vmd
| Self::Pmx
| Self::Csv => MediaFileClass::Dimensional,
Self::SceneRon
| Self::SceneJson
| Self::MoodJson
| Self::TimelineJson
| Self::EditorJson
| Self::Json => MediaFileClass::Project,
// Generic project documents don't have a class yet.
Self::Json => MediaFileClass::Unknown,
}
}
@@ -239,7 +220,6 @@ impl MediaFileType {
"model/fbx" => Some(Self::Fbx),
"application/fbx" => Some(Self::Fbx),
"model/obj" => Some(Self::Obj),
"model/ply" => Some(Self::Ply),
_ => None,
}
}
@@ -299,15 +279,11 @@ impl MediaFileType {
Self::Bvh => "bvh",
Self::Fbx => "fbx",
Self::Obj => "obj",
Self::Ply => "ply",
Self::Glb => "glb",
Self::Gltf => "gltf",
Self::Spz => "spz",
Self::SceneRon => "scene_ron",
Self::SceneJson => "scene_json",
Self::MoodJson => "mood_json",
Self::TimelineJson => "timeline_json",
Self::EditorJson => "editor_json",
Self::Pmd => "pmd",
Self::Vmd => "vmd",
Self::Pmx => "pmx",
@@ -338,15 +314,11 @@ impl MediaFileType {
"bvh" => Ok(Self::Bvh),
"fbx" => Ok(Self::Fbx),
"obj" => Ok(Self::Obj),
"ply" => Ok(Self::Ply),
"glb" => Ok(Self::Glb),
"gltf" => Ok(Self::Gltf),
"spz" => Ok(Self::Spz),
"scene_ron" => Ok(Self::SceneRon),
"scene_json" => Ok(Self::SceneJson),
"mood_json" => Ok(Self::MoodJson),
"timeline_json" => Ok(Self::TimelineJson),
"editor_json" => Ok(Self::EditorJson),
"pmd" => Ok(Self::Pmd),
"vmd" => Ok(Self::Vmd),
"pmx" => Ok(Self::Pmx),
@@ -380,15 +352,11 @@ impl MediaFileType {
Self::Bvh,
Self::Fbx,
Self::Obj,
Self::Ply,
Self::Glb,
Self::Gltf,
Self::Spz,
Self::SceneRon,
Self::SceneJson,
Self::MoodJson,
Self::TimelineJson,
Self::EditorJson,
Self::Pmd,
Self::Vmd,
Self::Pmx,
@@ -452,15 +420,11 @@ mod tests {
assert_serialization(MediaFileType::Bvh, "bvh");
assert_serialization(MediaFileType::Fbx, "fbx");
assert_serialization(MediaFileType::Obj, "obj");
assert_serialization(MediaFileType::Ply, "ply");
assert_serialization(MediaFileType::Glb, "glb");
assert_serialization(MediaFileType::Gltf, "gltf");
assert_serialization(MediaFileType::Spz, "spz");
assert_serialization(MediaFileType::SceneRon, "scene_ron");
assert_serialization(MediaFileType::SceneJson, "scene_json");
assert_serialization(MediaFileType::MoodJson, "mood_json");
assert_serialization(MediaFileType::TimelineJson, "timeline_json");
assert_serialization(MediaFileType::EditorJson, "editor_json");
assert_serialization(MediaFileType::Pmd, "pmd");
assert_serialization(MediaFileType::Vmd, "vmd");
assert_serialization(MediaFileType::Pmx, "pmx");
@@ -498,9 +462,6 @@ mod tests {
assert_eq!(MediaFileType::Spz.to_str(), "spz");
assert_eq!(MediaFileType::SceneRon.to_str(), "scene_ron");
assert_eq!(MediaFileType::SceneJson.to_str(), "scene_json");
assert_eq!(MediaFileType::MoodJson.to_str(), "mood_json");
assert_eq!(MediaFileType::TimelineJson.to_str(), "timeline_json");
assert_eq!(MediaFileType::EditorJson.to_str(), "editor_json");
assert_eq!(MediaFileType::Pmd.to_str(), "pmd");
assert_eq!(MediaFileType::Vmd.to_str(), "vmd");
assert_eq!(MediaFileType::Pmx.to_str(), "pmx");
@@ -534,9 +495,6 @@ mod tests {
assert_eq!(MediaFileType::from_str("spz").unwrap(), MediaFileType::Spz);
assert_eq!(MediaFileType::from_str("scene_ron").unwrap(), MediaFileType::SceneRon);
assert_eq!(MediaFileType::from_str("scene_json").unwrap(), MediaFileType::SceneJson);
assert_eq!(MediaFileType::from_str("mood_json").unwrap(), MediaFileType::MoodJson);
assert_eq!(MediaFileType::from_str("timeline_json").unwrap(), MediaFileType::TimelineJson);
assert_eq!(MediaFileType::from_str("editor_json").unwrap(), MediaFileType::EditorJson);
assert_eq!(MediaFileType::from_str("pmd").unwrap(), MediaFileType::Pmd);
assert_eq!(MediaFileType::from_str("vmd").unwrap(), MediaFileType::Vmd);
assert_eq!(MediaFileType::from_str("pmx").unwrap(), MediaFileType::Pmx);
@@ -565,20 +523,16 @@ mod tests {
assert_eq!(MediaFileType::Image.to_media_class(), MediaFileClass::Image);
assert_eq!(MediaFileType::Video.to_media_class(), MediaFileClass::Video);
assert_eq!(MediaFileType::Bvh.to_media_class(), MediaFileClass::Dimensional);
assert_eq!(MediaFileType::Fbx.to_media_class(), MediaFileClass::Mesh);
assert_eq!(MediaFileType::Obj.to_media_class(), MediaFileClass::Mesh);
assert_eq!(MediaFileType::Glb.to_media_class(), MediaFileClass::Mesh);
assert_eq!(MediaFileType::Gltf.to_media_class(), MediaFileClass::Mesh);
assert_eq!(MediaFileType::Spz.to_media_class(), MediaFileClass::Splat);
assert_eq!(MediaFileType::Ply.to_media_class(), MediaFileClass::Splat);
assert_eq!(MediaFileType::SceneRon.to_media_class(), MediaFileClass::Project);
assert_eq!(MediaFileType::SceneJson.to_media_class(), MediaFileClass::Project);
assert_eq!(MediaFileType::MoodJson.to_media_class(), MediaFileClass::Project);
assert_eq!(MediaFileType::TimelineJson.to_media_class(), MediaFileClass::Project);
assert_eq!(MediaFileType::EditorJson.to_media_class(), MediaFileClass::Project);
assert_eq!(MediaFileType::Pmd.to_media_class(), MediaFileClass::Mesh);
assert_eq!(MediaFileType::Fbx.to_media_class(), MediaFileClass::Dimensional);
assert_eq!(MediaFileType::Obj.to_media_class(), MediaFileClass::Dimensional);
assert_eq!(MediaFileType::Glb.to_media_class(), MediaFileClass::Dimensional);
assert_eq!(MediaFileType::Gltf.to_media_class(), MediaFileClass::Dimensional);
assert_eq!(MediaFileType::Spz.to_media_class(), MediaFileClass::Dimensional);
assert_eq!(MediaFileType::SceneRon.to_media_class(), MediaFileClass::Dimensional);
assert_eq!(MediaFileType::SceneJson.to_media_class(), MediaFileClass::Dimensional);
assert_eq!(MediaFileType::Pmd.to_media_class(), MediaFileClass::Dimensional);
assert_eq!(MediaFileType::Vmd.to_media_class(), MediaFileClass::Dimensional);
assert_eq!(MediaFileType::Pmx.to_media_class(), MediaFileClass::Mesh);
assert_eq!(MediaFileType::Pmx.to_media_class(), MediaFileClass::Dimensional);
assert_eq!(MediaFileType::Csv.to_media_class(), MediaFileClass::Dimensional);
assert_eq!(MediaFileType::Jpg.to_media_class(), MediaFileClass::Image);
assert_eq!(MediaFileType::Png.to_media_class(), MediaFileClass::Image);
@@ -594,7 +548,7 @@ mod tests {
assert_eq!(MediaFileType::Aac.to_media_class(), MediaFileClass::Audio);
assert_eq!(MediaFileType::M4a.to_media_class(), MediaFileClass::Audio);
assert_eq!(MediaFileType::Flac.to_media_class(), MediaFileClass::Audio);
assert_eq!(MediaFileType::Json.to_media_class(), MediaFileClass::Project);
assert_eq!(MediaFileType::Json.to_media_class(), MediaFileClass::Unknown);
}
}
@@ -738,22 +692,18 @@ mod tests {
#[test]
fn all_variants() {
let mut variants = MediaFileType::all_variants();
assert_eq!(variants.len(), 34);
assert_eq!(variants.len(), 30);
assert_eq!(variants.pop_first(), Some(MediaFileType::Audio));
assert_eq!(variants.pop_first(), Some(MediaFileType::Image));
assert_eq!(variants.pop_first(), Some(MediaFileType::Video));
assert_eq!(variants.pop_first(), Some(MediaFileType::Bvh));
assert_eq!(variants.pop_first(), Some(MediaFileType::Fbx));
assert_eq!(variants.pop_first(), Some(MediaFileType::Obj));
assert_eq!(variants.pop_first(), Some(MediaFileType::Ply));
assert_eq!(variants.pop_first(), Some(MediaFileType::Glb));
assert_eq!(variants.pop_first(), Some(MediaFileType::Gltf));
assert_eq!(variants.pop_first(), Some(MediaFileType::Spz));
assert_eq!(variants.pop_first(), Some(MediaFileType::SceneRon));
assert_eq!(variants.pop_first(), Some(MediaFileType::SceneJson));
assert_eq!(variants.pop_first(), Some(MediaFileType::MoodJson));
assert_eq!(variants.pop_first(), Some(MediaFileType::TimelineJson));
assert_eq!(variants.pop_first(), Some(MediaFileType::EditorJson));
assert_eq!(variants.pop_first(), Some(MediaFileType::Pmd));
assert_eq!(variants.pop_first(), Some(MediaFileType::Vmd));
assert_eq!(variants.pop_first(), Some(MediaFileType::Pmx));
@@ -4,6 +4,5 @@ pub mod media_file_engine_category;
pub mod media_file_origin_category;
pub mod media_file_origin_model_type;
pub mod media_file_origin_product_category;
pub mod media_file_project_type;
pub mod media_file_subtype;
pub mod media_file_type;
@@ -18,6 +18,7 @@ pub mod model_weights;
pub mod prompt_context_items;
pub mod prompts;
pub mod staff_audit_logs;
pub mod tag_uses;
pub mod trending_model_analytics;
pub mod tts_models;
pub mod uploaded_video_notes;
@@ -0,0 +1 @@
pub mod tag_use_entity_type;
@@ -0,0 +1,127 @@
use std::collections::BTreeSet;
use serde::Deserialize;
use serde::Serialize;
#[cfg(test)]
use strum::EnumCount;
#[cfg(test)]
use strum::EnumIter;
use utoipa::ToSchema;
/// Used in the `tag_uses` table in a `VARCHAR(32)` field named `entity_type`.
#[cfg_attr(test, derive(EnumIter, EnumCount))]
#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, ToSchema)]
pub enum TagUseEntityType {
/// Media files
#[serde(rename = "media_file")]
MediaFile,
/// Model weights
#[serde(rename = "model_weight")]
ModelWeight,
}
// TODO(bt, 2023-01-17): This desperately needs MySQL integration tests!
impl_enum_display_and_debug_using_to_str!(TagUseEntityType);
impl_mysql_enum_coders!(TagUseEntityType);
/// NB: Legacy API for older code.
impl TagUseEntityType {
pub fn to_str(&self) -> &'static str {
match self {
Self::MediaFile => "media_file",
Self::ModelWeight => "model_weight",
}
}
pub fn from_str(value: &str) -> Result<Self, String> {
match value {
"media_file" => Ok(Self::MediaFile),
"model_weight" => Ok(Self::ModelWeight),
_ => Err(format!("invalid value: {:?}", value)),
}
}
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::MediaFile,
Self::ModelWeight,
])
}
}
#[cfg(test)]
mod tests {
use crate::test_helpers::assert_serialization;
use crate::by_table::tag_uses::tag_use_entity_type::TagUseEntityType;
mod serde {
use super::*;
#[test]
fn test_serialization() {
assert_serialization(TagUseEntityType::MediaFile, "media_file");
assert_serialization(TagUseEntityType::ModelWeight, "model_weight");
}
}
mod impl_methods {
use super::*;
#[test]
fn test_to_str() {
assert_eq!(TagUseEntityType::MediaFile.to_str(), "media_file");
assert_eq!(TagUseEntityType::ModelWeight.to_str(), "model_weight");
}
#[test]
fn test_from_str() {
assert_eq!(TagUseEntityType::from_str("media_file").unwrap(), TagUseEntityType::MediaFile);
assert_eq!(TagUseEntityType::from_str("model_weight").unwrap(), TagUseEntityType::ModelWeight);
assert!(TagUseEntityType::from_str("foo").is_err());
}
}
mod manual_variant_checks {
use super::*;
#[test]
fn all_variants() {
let mut variants = TagUseEntityType::all_variants();
assert_eq!(variants.len(), 2);
assert_eq!(variants.pop_first(), Some(TagUseEntityType::MediaFile));
assert_eq!(variants.pop_first(), Some(TagUseEntityType::ModelWeight));
assert_eq!(variants.pop_first(), None);
}
}
mod mechanical_checks {
use super::*;
#[test]
fn variant_length() {
use strum::IntoEnumIterator;
assert_eq!(TagUseEntityType::all_variants().len(), TagUseEntityType::iter().len());
}
#[test]
fn round_trip() {
for variant in TagUseEntityType::all_variants() {
assert_eq!(variant, TagUseEntityType::from_str(variant.to_str()).unwrap());
assert_eq!(variant, TagUseEntityType::from_str(&format!("{}", variant)).unwrap());
assert_eq!(variant, TagUseEntityType::from_str(&format!("{:?}", variant)).unwrap());
}
}
#[test]
fn serialized_length_ok_for_database() {
const MAX_LENGTH : usize = 32;
for variant in TagUseEntityType::all_variants() {
let serialized = variant.to_str();
assert!(serialized.len() > 0, "variant {:?} is too short", variant);
assert!(serialized.len() <= MAX_LENGTH, "variant {:?} is too long", variant);
}
}
}
}
@@ -248,8 +248,6 @@ pub enum CommonModelType {
Marble1p1,
#[serde(rename = "marble_1p1_plus")]
Marble1p1Plus,
#[serde(rename = "triposplat")]
TripoSplat,
}
impl_enum_display_and_debug_using_to_str!(CommonModelType);
@@ -366,7 +364,6 @@ impl CommonModelType {
Self::Marble1p0Draft => "marble_1p0_draft",
Self::Marble1p1 => "marble_1p1",
Self::Marble1p1Plus => "marble_1p1_plus",
Self::TripoSplat => "triposplat",
}
}
@@ -477,7 +474,6 @@ impl CommonModelType {
"marble_1p0_draft" => Ok(Self::Marble1p0Draft),
"marble_1p1" => Ok(Self::Marble1p1),
"marble_1p1_plus" => Ok(Self::Marble1p1Plus),
"triposplat" => Ok(Self::TripoSplat),
_ => Err(format!("invalid model_type: {:?}", job_status)),
}
@@ -592,7 +588,6 @@ impl CommonModelType {
Self::Marble1p0Draft,
Self::Marble1p1,
Self::Marble1p1Plus,
Self::TripoSplat,
])
}
@@ -704,7 +699,6 @@ impl CommonModelType {
Self::Marble1p0Draft => CommonModelClass::DimensionalSplat,
Self::Marble1p1 => CommonModelClass::DimensionalSplat,
Self::Marble1p1Plus => CommonModelClass::DimensionalSplat,
Self::TripoSplat => CommonModelClass::DimensionalSplat,
}
}
}
@@ -821,7 +815,6 @@ mod tests {
assert_serialization(CommonModelType::Marble1p0Draft, "marble_1p0_draft");
assert_serialization(CommonModelType::Marble1p1, "marble_1p1");
assert_serialization(CommonModelType::Marble1p1Plus, "marble_1p1_plus");
assert_serialization(CommonModelType::TripoSplat, "triposplat");
}
#[test]
@@ -930,7 +923,6 @@ mod tests {
assert_eq!(CommonModelType::Marble1p0Draft.to_str(), "marble_1p0_draft");
assert_eq!(CommonModelType::Marble1p1.to_str(), "marble_1p1");
assert_eq!(CommonModelType::Marble1p1Plus.to_str(), "marble_1p1_plus");
assert_eq!(CommonModelType::TripoSplat.to_str(), "triposplat");
}
#[test]
@@ -1036,13 +1028,12 @@ mod tests {
assert_eq!(CommonModelType::from_str("marble_1p0_draft").unwrap(), CommonModelType::Marble1p0Draft);
assert_eq!(CommonModelType::from_str("marble_1p1").unwrap(), CommonModelType::Marble1p1);
assert_eq!(CommonModelType::from_str("marble_1p1_plus").unwrap(), CommonModelType::Marble1p1Plus);
assert_eq!(CommonModelType::from_str("triposplat").unwrap(), CommonModelType::TripoSplat);
}
#[test]
fn all_variants() {
let mut variants = CommonModelType::all_variants();
assert_eq!(variants.len(), 96);
assert_eq!(variants.len(), 95);
// Image models
assert_eq!(variants.pop_first(), Some(CommonModelType::Flux1Dev));
assert_eq!(variants.pop_first(), Some(CommonModelType::Flux1Schnell));
@@ -1143,7 +1134,6 @@ mod tests {
assert_eq!(variants.pop_first(), Some(CommonModelType::Marble1p0Draft));
assert_eq!(variants.pop_first(), Some(CommonModelType::Marble1p1));
assert_eq!(variants.pop_first(), Some(CommonModelType::Marble1p1Plus));
assert_eq!(variants.pop_first(), Some(CommonModelType::TripoSplat));
assert_eq!(variants.pop_first(), None);
}
@@ -21,10 +21,6 @@ pub enum CommonSplatModel {
#[serde(rename = "marble_1p1_plus")]
Marble1p1Plus,
/// TripoSplat: single-image to 3D Gaussian splat (via Fal).
#[serde(rename = "triposplat")]
TripoSplat,
}
impl CommonSplatModel {
@@ -37,7 +33,6 @@ impl CommonSplatModel {
Self::Marble1p0Draft => CommonModelType::Marble1p0Draft,
Self::Marble1p1 => CommonModelType::Marble1p1,
Self::Marble1p1Plus => CommonModelType::Marble1p1Plus,
Self::TripoSplat => CommonModelType::TripoSplat,
}
}
}
@@ -56,7 +51,6 @@ mod tests {
assert_serialization(CommonSplatModel::Marble1p0Draft, "marble_1p0_draft");
assert_serialization(CommonSplatModel::Marble1p1, "marble_1p1");
assert_serialization(CommonSplatModel::Marble1p1Plus, "marble_1p1_plus");
assert_serialization(CommonSplatModel::TripoSplat, "triposplat");
}
#[test]
@@ -68,7 +62,6 @@ mod tests {
("marble_1p0_draft", CommonSplatModel::Marble1p0Draft),
("marble_1p1", CommonSplatModel::Marble1p1),
("marble_1p1_plus", CommonSplatModel::Marble1p1Plus),
("triposplat", CommonSplatModel::TripoSplat),
];
for (json_str, expected) in cases {
let json = format!("\"{}\"", json_str);
@@ -87,7 +80,6 @@ mod tests {
CommonSplatModel::Marble1p0Draft,
CommonSplatModel::Marble1p1,
CommonSplatModel::Marble1p1Plus,
CommonSplatModel::TripoSplat,
];
for variant in all {
let json = serde_json::to_string(&variant).unwrap();
@@ -105,7 +97,6 @@ mod tests {
(CommonSplatModel::Marble1p0Draft, CommonModelType::Marble1p0Draft),
(CommonSplatModel::Marble1p1, CommonModelType::Marble1p1),
(CommonSplatModel::Marble1p1Plus, CommonModelType::Marble1p1Plus),
(CommonSplatModel::TripoSplat, CommonModelType::TripoSplat),
];
for (splat_model, expected) in models {
assert_eq!(splat_model.to_common_model_type(), expected);
@@ -187,7 +187,7 @@ pub async fn process_job(args: FbxToGltfJobArgs<'_>) -> Result<JobSuccessResult,
let (inference_result_token, id) = insert_media_file_generic_from_job(InsertFromJobArgs {
pool: &args.job_dependencies.db.mysql_pool,
job: &job,
media_class: MediaFileClass::Mesh,
media_class: MediaFileClass::Dimensional,
media_type: upload_details.media_type,
origin_category: MediaFileOriginCategory::Processed,
origin_product_category: MediaFileOriginProductCategory::Mocap,
@@ -122,7 +122,7 @@ pub async fn process_successful_job(
.maybe_generation_provider(Some(GenerationProvider::Artcraft))
.maybe_prompt_token(job.maybe_prompt_token.as_ref())
.maybe_platform_type(job.maybe_platform_type)
.media_file_class(MediaFileClass::Splat)
.media_file_class(MediaFileClass::Dimensional)
.media_file_origin_category(MediaFileOriginCategory::Inference)
.media_file_origin_product_category(MediaFileOriginProductCategory::WorldGeneration)
.media_file_type(MediaFileType::Spz)
@@ -28,7 +28,6 @@ fn build_omni_gen_splat_models() -> Vec<OmniGenSplatModelDetails> {
marble_model(CommonSplatModel::Marble1p0Draft, "Marble 1.0 Draft"),
marble_model(CommonSplatModel::Marble1p1, "Marble 1.1"),
marble_model(CommonSplatModel::Marble1p1Plus, "Marble 1.1 Plus"),
triposplat_model(),
]
}
@@ -54,31 +53,12 @@ fn build_omni_gen_splat_model_providers() -> Vec<OmniGenSplatModelProviderDetail
model: CommonSplatModel::Marble1p1Plus,
overrides: None,
},
OmniGenSplatProviderModelDetails {
model: CommonSplatModel::TripoSplat,
overrides: None,
},
],
});
providers
}
/// TripoSplat reconstructs an object-scale Gaussian splat from exactly one
/// image. No prompt, video, panorama, or recaption toggle.
fn triposplat_model() -> OmniGenSplatModelDetails {
OmniGenSplatModelDetails {
model: CommonSplatModel::TripoSplat,
model_creator: Some(ModelCreator::Tripo),
full_name: Some("TripoSplat".to_string()),
extra_info: Some("Reconstructs a 3D Gaussian splat from a single image".to_string()),
extra_info_short: Some("Image to splat".to_string()),
image_references_supported: Some(true),
image_references_max: Some(1),
..Default::default()
}
}
/// All Marble models share the same capability surface: text prompt, image
/// references (up to 4, multi-view), a reference video, 360-degree panorama
/// input, and the "disable recaption" toggle.
@@ -12,8 +12,10 @@ use crate::http_server::common_responses::pagination_cursors::PaginationCursors;
use crate::http_server::common_responses::pagination_page::PaginationPage;
use crate::http_server::common_responses::simple_entity_stats::SimpleEntityStats;
use crate::http_server::common_responses::simple_response::SimpleResponse;
use crate::http_server::common_responses::tag_info::TagInfo;
use crate::http_server::common_responses::user_details_lite::{UserDefaultAvatarInfo, UserDetailsLight};
use crate::http_server::deprecated_endpoints::conversion::enqueue_fbx_to_gltf_handler::*;
use crate::http_server::deprecated_endpoints::engine::create_scene_handler::*;
use crate::http_server::deprecated_endpoints::workflows::enqueue::enqueue_face_fusion_workflow_handler::*;
use crate::http_server::deprecated_endpoints::workflows::enqueue::enqueue_live_portrait_workflow_handler::*;
use crate::http_server::deprecated_endpoints::workflows::enqueue::vst_common::vst_error::*;
@@ -64,21 +66,12 @@ use crate::http_server::endpoints::media_files::list::list_pinned_media_files_ha
use crate::http_server::endpoints::media_files::search::search_featured_media_files_handler::*;
use crate::http_server::endpoints::media_files::search::search_session_media_files_handler::*;
use crate::http_server::endpoints::media_files::upload::upload_audio_media_file_handler::*;
use crate::http_server::endpoints::media_files::upload::upload_engine_asset::upload_engine_asset_media_file_handler::*;
use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError;
use crate::http_server::endpoints::media_files::upload::upload_generic::upload_media_file_handler::*;
use crate::http_server::endpoints::media_files::upload::upload_image_media_file_handler::*;
use crate::http_server::endpoints::media_files::upload::upload_new_engine_asset_media_file_handler::*;
use crate::http_server::endpoints::media_files::upload::upload_new_scene_media_file_handler::*;
use crate::http_server::endpoints::media_files::upload::project::upload_updated_mood_board_project_handler::*;
use crate::http_server::endpoints::media_files::upload::project::upload_new_mood_board_project_handler::*;
use crate::http_server::endpoints::media_files::upload::project::upload_updated_video_timeline_project_handler::*;
use crate::http_server::endpoints::media_files::upload::project::upload_new_video_timeline_project_handler::*;
use crate::http_server::endpoints::media_files::upload::project::upload_updated_editor_2d_project_handler::*;
use crate::http_server::endpoints::media_files::upload::project::upload_new_editor_2d_project_handler::*;
use crate::http_server::endpoints::media_files::upload::project::upload_updated_scene_3d_project_handler::*;
use crate::http_server::endpoints::media_files::upload::project::upload_new_scene_3d_project_handler::*;
use crate::http_server::endpoints::media_files::upload::project::save_new_project::NewProjectMultipartForm;
use crate::http_server::endpoints::media_files::upload::project::update_project::UpdateProjectMultipartForm;
use crate::http_server::endpoints::media_files::upload::upload_pmx::upload_pmx_media_file_handler::*;
use crate::http_server::endpoints::media_files::upload::upload_saved_scene_media_file_handler::*;
use crate::http_server::endpoints::media_files::upload::upload_scene_snapshot_media_file_handler::*;
@@ -89,6 +82,9 @@ use crate::http_server::endpoints::media_files::upload::upload_video_old::upload
use crate::http_server::endpoints::omni_api::upload::omni_upload_audio_media_file_handler::*;
use crate::http_server::endpoints::omni_api::upload::omni_upload_image_media_file_handler::*;
use crate::http_server::endpoints::omni_api::upload::omni_upload_video_media_file_handler::*;
use crate::http_server::endpoints::media_files::upsert_upload::write_engine_asset::write_engine_asset_media_file_handler::*;
use crate::http_server::endpoints::media_files::upsert_upload::write_error::MediaFileWriteError;
use crate::http_server::endpoints::media_files::upsert_upload::write_scene_file::write_scene_file_media_file_handler::*;
use crate::http_server::endpoints::model_download::enqueue_gptsovits_model_download_handler::*;
use artcraft_api_defs::generate::video::edit::beeble_switchx_edit_video::*;
use artcraft_api_defs::moderation::debug_logs::debug_log_entry::*;
@@ -221,6 +217,8 @@ use billing_component::stripe::http_endpoints::checkout::create::stripe_create_c
use crate::http_server::endpoints::billing_fakeyou::list_active_user_subscriptions_handler::*;
use crate::http_server::endpoints::service::status_alert_handler::*;
use crate::http_server::endpoints::stats::get_unified_queue_stats_handler::*;
use crate::http_server::endpoints::tags::list_tags_for_entity_handler::*;
use crate::http_server::endpoints::tags::set_tags_for_entity_handler::*;
use crate::http_server::endpoints::tts::enqueue_infer_tts_handler::enqueue_infer_tts_handler::*;
use crate::http_server::endpoints::user_bookmarks::batch_get_user_bookmarks_handler::*;
use crate::http_server::endpoints::user_bookmarks::create_user_bookmark_handler::*;
@@ -335,15 +333,6 @@ use artcraft_api_defs::analytics::log_active_user::*;
use artcraft_api_defs::credits::get_session_credits::*;
use artcraft_api_defs::subscriptions::get_session_subscription::*;
use artcraft_api_defs::media_file::list_batch_generated_media_files::*;
use artcraft_api_defs::media_file::list_session_project_media_files::*;
use artcraft_api_defs::media_file::project::upload_updated_mood_board_project::*;
use artcraft_api_defs::media_file::project::upload_new_mood_board_project::*;
use artcraft_api_defs::media_file::project::upload_updated_video_timeline_project::*;
use artcraft_api_defs::media_file::project::upload_new_video_timeline_project::*;
use artcraft_api_defs::media_file::project::upload_updated_editor_2d_project::*;
use artcraft_api_defs::media_file::project::upload_new_editor_2d_project::*;
use artcraft_api_defs::media_file::project::upload_updated_scene_3d_project::*;
use artcraft_api_defs::media_file::project::upload_new_scene_3d_project::*;
// Handler modules with locally-defined types
use crate::http_server::endpoints::moderation::info::moderator_token_info_handler::*;
use artcraft_api_defs::characters::create_character::*;
@@ -423,6 +412,7 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
billing_component::stripe::http_endpoints::checkout::create::stripe_create_checkout_session_json_handler::stripe_create_checkout_session_json_handler,
crate::http_server::endpoints::billing_fakeyou::list_active_user_subscriptions_handler::list_active_user_subscriptions_handler,
crate::http_server::deprecated_endpoints::conversion::enqueue_fbx_to_gltf_handler::enqueue_fbx_to_gltf_handler,
crate::http_server::deprecated_endpoints::engine::create_scene_handler::create_scene_handler,
crate::http_server::deprecated_endpoints::workflows::enqueue::enqueue_face_fusion_workflow_handler::enqueue_face_fusion_workflow_handler,
crate::http_server::deprecated_endpoints::workflows::enqueue::enqueue_live_portrait_workflow_handler::enqueue_live_portrait_workflow_handler,
crate::http_server::deprecated_endpoints::workflows::enqueue::enqueue_studio_workflow_handler::enqueue_studio_workflow_handler,
@@ -477,22 +467,14 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
crate::http_server::endpoints::media_files::list::list_media_files_for_user_handler::list_media_files_for_user_handler,
crate::http_server::endpoints::media_files::list::list_media_files_handler::list_media_files_handler,
crate::http_server::endpoints::media_files::list::list_pinned_media_files_handler::list_pinned_media_files_handler,
crate::http_server::endpoints::media_files::list::list_session_project_media_files_handler::list_session_project_media_files_handler,
crate::http_server::endpoints::media_files::search::search_featured_media_files_handler::search_featured_media_files_handler,
crate::http_server::endpoints::media_files::search::search_session_media_files_handler::search_session_media_files_handler,
crate::http_server::endpoints::media_files::upload::upload_audio_media_file_handler::upload_audio_media_file_handler,
crate::http_server::endpoints::media_files::upload::upload_engine_asset::upload_engine_asset_media_file_handler::upload_engine_asset_media_file_handler,
crate::http_server::endpoints::media_files::upload::upload_generic::upload_media_file_handler::upload_media_file_handler,
crate::http_server::endpoints::media_files::upload::upload_image_media_file_handler::upload_image_media_file_handler,
crate::http_server::endpoints::media_files::upload::upload_new_engine_asset_media_file_handler::upload_new_engine_asset_media_file_handler,
crate::http_server::endpoints::media_files::upload::upload_new_scene_media_file_handler::upload_new_scene_media_file_handler,
crate::http_server::endpoints::media_files::upload::project::upload_new_mood_board_project_handler::upload_new_mood_board_project_handler,
crate::http_server::endpoints::media_files::upload::project::upload_updated_mood_board_project_handler::upload_updated_mood_board_project_handler,
crate::http_server::endpoints::media_files::upload::project::upload_new_video_timeline_project_handler::upload_new_video_timeline_project_handler,
crate::http_server::endpoints::media_files::upload::project::upload_updated_video_timeline_project_handler::upload_updated_video_timeline_project_handler,
crate::http_server::endpoints::media_files::upload::project::upload_new_editor_2d_project_handler::upload_new_editor_2d_project_handler,
crate::http_server::endpoints::media_files::upload::project::upload_updated_editor_2d_project_handler::upload_updated_editor_2d_project_handler,
crate::http_server::endpoints::media_files::upload::project::upload_new_scene_3d_project_handler::upload_new_scene_3d_project_handler,
crate::http_server::endpoints::media_files::upload::project::upload_updated_scene_3d_project_handler::upload_updated_scene_3d_project_handler,
crate::http_server::endpoints::media_files::upload::upload_spz_media_file_handler::upload_spz_media_file_handler,
crate::http_server::endpoints::media_files::upload::upload_pmx::upload_pmx_media_file_handler::upload_pmx_media_file_handler,
crate::http_server::endpoints::media_files::upload::upload_saved_scene_media_file_handler::upload_saved_scene_media_file_handler,
@@ -500,6 +482,8 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
crate::http_server::endpoints::media_files::upload::upload_studio_shot::upload_studio_shot_media_file_handler::upload_studio_shot_media_file_handler,
crate::http_server::endpoints::media_files::upload::upload_video_new::upload_new_video_media_file_handler::upload_new_video_media_file_handler,
crate::http_server::endpoints::media_files::upload::upload_video_old::upload_video_media_file_handler::upload_video_media_file_handler,
crate::http_server::endpoints::media_files::upsert_upload::write_engine_asset::write_engine_asset_media_file_handler::write_engine_asset_media_file_handler,
crate::http_server::endpoints::media_files::upsert_upload::write_scene_file::write_scene_file_media_file_handler::write_scene_file_media_file_handler,
crate::http_server::endpoints::model_download::enqueue_gptsovits_model_download_handler::enqueue_gptsovits_model_download_handler,
crate::http_server::endpoints::moderation::user_feature_flags::moderator_edit_user_feature_flags_handler::moderator_edit_user_feature_flags_handler,
crate::http_server::endpoints::moderation::user_feature_flags::moderator_list_all_available_user_feature_flags_handler::moderator_list_all_available_user_feature_flags_handler,
@@ -509,6 +493,8 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
crate::http_server::endpoints::prompts::get_prompt_handler::get_prompt_handler,
crate::http_server::endpoints::service::status_alert_handler::status_alert_handler,
crate::http_server::endpoints::stats::get_unified_queue_stats_handler::get_unified_queue_stats_handler,
crate::http_server::endpoints::tags::list_tags_for_entity_handler::list_tags_for_entity_handler,
crate::http_server::endpoints::tags::set_tags_for_entity_handler::set_tags_for_entity_handler,
crate::http_server::endpoints::tts::enqueue_infer_tts_handler::enqueue_infer_tts_handler::enqueue_infer_tts_handler,
crate::http_server::endpoints::user_bookmarks::batch_get_user_bookmarks_handler::batch_get_user_bookmarks_handler,
crate::http_server::endpoints::user_bookmarks::create_user_bookmark_handler::create_user_bookmark_handler,
@@ -751,12 +737,15 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
MediaFileModelDetails,
MediaFileOriginDetails,
MediaFileSocialMetaLight,
MediaFileWriteError,
MediaFileWriteError,
MediaLinks,
PaginationCursors,
PaginationPage,
SimpleEntityStats,
SimpleGenericJsonSuccess,
SimpleResponse,
TagInfo,
UserDetailsLight,
VideoPreviews,
Visibility,
@@ -828,6 +817,7 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
CreateCommentSuccessResponse,
CreateFeaturedItemRequest,
CreateFeaturedItemSuccessResponse,
CreateSceneSuccessResponse,
CreateUserBookmarkError,
CreateUserBookmarkRequest,
CreateUserBookmarkSuccessResponse,
@@ -1090,11 +1080,12 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
ListPinnedWeightsSuccessResponse,
ListSessionJobsItem,
ListSessionJobsQueryParams,
ListSessionProjectMediaFilesSuccessResponse,
ListSessionJobsSuccessResponse,
ListSessionRequestDetailsResponse,
ListSessionResultDetailsResponse,
ListSessionStatusDetailsResponse,
ListTagsForEntityPathInfo,
ListTagsForEntitySuccessResponse,
ListUserBookmarksForEntityError,
ListUserBookmarksForEntityPathInfo,
ListUserBookmarksForEntitySuccessResponse,
@@ -1130,10 +1121,8 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
ModeratorUserEmailChangeItem,
ModeratorUserEmailChangeUserSummary,
ModernInferenceQueueStats,
NewProjectMultipartForm,
PinnedMediaFile,
PinnedModelWeightForList,
ProjectMediaFileInfo,
PromptInfo,
RatingRow,
RedeemBetaKeyRequest,
@@ -1160,6 +1149,9 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
SetModelWeightCoverImagePathInfo,
SetModelWeightCoverImageRequest,
SetModelWeightCoverImageResponse,
SetTagsForEntityPathInfo,
SetTagsForEntityRequest,
SetTagsForEntitySuccessResponse,
SetUserRatingRequest,
SetUserRatingResponse,
StatusAlertCategory,
@@ -1173,6 +1165,7 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
UpdateWeightRequest,
UploadAudioMediaFileForm,
UploadAudioMediaFileSuccessResponse,
UploadEngineAssetMediaSuccessResponse,
UploadImageMediaFileForm,
UploadImageMediaFileSuccessResponse,
OmniUploadAudioMediaFileForm,
@@ -1185,19 +1178,6 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
UploadMediaSuccessResponse,
UploadNewEngineAssetFileForm,
UploadNewEngineAssetSuccessResponse,
UploadUpdatedEditor2dProjectPathInfo,
UpdateProjectMultipartForm,
UploadUpdatedEditor2dProjectSuccessResponse,
UploadUpdatedMoodBoardProjectPathInfo,
UploadUpdatedMoodBoardProjectSuccessResponse,
UploadUpdatedScene3dProjectPathInfo,
UploadUpdatedScene3dProjectSuccessResponse,
UploadUpdatedVideoTimelineProjectPathInfo,
UploadUpdatedVideoTimelineProjectSuccessResponse,
UploadNewEditor2dProjectSuccessResponse,
UploadNewMoodBoardProjectSuccessResponse,
UploadNewScene3dProjectSuccessResponse,
UploadNewVideoTimelineProjectSuccessResponse,
UploadNewSceneMediaFileForm,
UploadNewSceneMediaFileSuccessResponse,
UploadSpzMediaFileForm,
@@ -1230,6 +1210,8 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
VstSuccessResponse,
Weight,
WeightsData,
WriteEngineAssetMediaSuccessResponse,
WriteSceneFileMediaSuccessResponse,
// Cost Estimate types
EstimateImageCostRequest,
@@ -6,6 +6,7 @@ pub mod pagination_cursors;
pub mod pagination_page;
pub mod simple_entity_stats;
pub mod simple_response;
pub mod tag_info;
pub mod user_avatars;
pub mod user_details_lite;
pub mod user_details_lite_builder;

Some files were not shown because too many files have changed in this diff Show More