eventing to support generation batches + inpainting results (#772)

This commit is contained in:
Brandon Thomas authored and GitHub committed 2025-08-21 02:26:04 -04:00
1 parent ea7259d5e8
commit 2007c7af91
63 files changed
+2090 -149

No files matched your search

@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n\n jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,\n jobs.attempt_count,\n\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n\n jobs.product_category as `product_category: enums::by_table::generic_inference_jobs::inference_job_product_category::InferenceJobProductCategory`,\n jobs.inference_category as `inference_category: enums::by_table::generic_inference_jobs::inference_category::InferenceCategory`,\n jobs.maybe_model_type,\n jobs.maybe_model_token,\n jobs.maybe_raw_inference_text,\n jobs.maybe_inference_args,\n\n jobs.frontend_failure_category as `maybe_frontend_failure_category: enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory`,\n\n jobs.on_success_result_entity_type as maybe_result_entity_type,\n jobs.on_success_result_entity_token as maybe_result_entity_token,\n\n model_weights.title as maybe_model_weights_title,\n tts_models.title as maybe_tts_model_title,\n voice_conversion_models.title as maybe_voice_conversion_model_title,\n\n tts_results.public_bucket_wav_audio_path as maybe_tts_public_bucket_path,\n voice_conversion_results.public_bucket_hash as maybe_voice_conversion_public_bucket_hash,\n\n media_files.public_bucket_directory_hash as maybe_media_file_public_bucket_directory_hash,\n media_files.maybe_public_bucket_prefix as maybe_media_file_public_bucket_prefix,\n media_files.maybe_public_bucket_extension as maybe_media_file_public_bucket_extension,\n\n jobs.assigned_worker as maybe_assigned_worker,\n jobs.assigned_cluster as maybe_assigned_cluster,\n\n jobs.is_keepalive_required,\n\n jobs.created_at,\n jobs.updated_at,\n\n jobs.first_started_at as maybe_first_started_at,\n jobs.successfully_completed_at as maybe_successfully_completed_at,\n\n NOW() as database_clock\n\nFROM generic_inference_jobs as jobs\n\nLEFT OUTER JOIN model_weights ON jobs.maybe_model_token = model_weights.token\nLEFT OUTER JOIN tts_models ON jobs.maybe_model_token = tts_models.token\nLEFT OUTER JOIN voice_conversion_models ON jobs.maybe_model_token = voice_conversion_models.token\n\nLEFT OUTER JOIN tts_results ON jobs.on_success_result_entity_token = tts_results.token\nLEFT OUTER JOIN voice_conversion_results ON jobs.on_success_result_entity_token = voice_conversion_results.token\nLEFT OUTER JOIN media_files ON jobs.on_success_result_entity_token = media_files.token\n\nWHERE jobs.token = ?\n ",
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n\n jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,\n jobs.attempt_count,\n\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n\n jobs.product_category as `product_category: enums::by_table::generic_inference_jobs::inference_job_product_category::InferenceJobProductCategory`,\n jobs.inference_category as `inference_category: enums::by_table::generic_inference_jobs::inference_category::InferenceCategory`,\n jobs.maybe_model_type,\n jobs.maybe_model_token,\n jobs.maybe_raw_inference_text,\n jobs.maybe_inference_args,\n\n jobs.frontend_failure_category as `maybe_frontend_failure_category: enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory`,\n\n jobs.on_success_result_entity_type as maybe_result_entity_type,\n jobs.on_success_result_entity_token as maybe_result_entity_token,\n jobs.on_success_result_batch_token as maybe_result_batch_token,\n\n model_weights.title as maybe_model_weights_title,\n tts_models.title as maybe_tts_model_title,\n voice_conversion_models.title as maybe_voice_conversion_model_title,\n\n tts_results.public_bucket_wav_audio_path as maybe_tts_public_bucket_path,\n voice_conversion_results.public_bucket_hash as maybe_voice_conversion_public_bucket_hash,\n\n media_files.public_bucket_directory_hash as maybe_media_file_public_bucket_directory_hash,\n media_files.maybe_public_bucket_prefix as maybe_media_file_public_bucket_prefix,\n media_files.maybe_public_bucket_extension as maybe_media_file_public_bucket_extension,\n\n jobs.assigned_worker as maybe_assigned_worker,\n jobs.assigned_cluster as maybe_assigned_cluster,\n\n jobs.is_keepalive_required,\n\n jobs.created_at,\n jobs.updated_at,\n\n jobs.first_started_at as maybe_first_started_at,\n jobs.successfully_completed_at as maybe_successfully_completed_at,\n\n NOW() as database_clock\n\nFROM generic_inference_jobs as jobs\n\nLEFT OUTER JOIN model_weights ON jobs.maybe_model_token = model_weights.token\nLEFT OUTER JOIN tts_models ON jobs.maybe_model_token = tts_models.token\nLEFT OUTER JOIN voice_conversion_models ON jobs.maybe_model_token = voice_conversion_models.token\n\nLEFT OUTER JOIN tts_results ON jobs.on_success_result_entity_token = tts_results.token\nLEFT OUTER JOIN voice_conversion_results ON jobs.on_success_result_entity_token = voice_conversion_results.token\nLEFT OUTER JOIN media_files ON jobs.on_success_result_entity_token = media_files.token\n\nWHERE jobs.token = ?\n ",
"describe": {
"columns": [
{
@@ -155,6 +155,16 @@
},
{
"ordinal": 15,
"name": "maybe_result_batch_token",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 16,
"name": "maybe_model_weights_title",
"type_info": {
"type": "VarString",
@@ -164,7 +174,7 @@
}
},
{
"ordinal": 16,
"ordinal": 17,
"name": "maybe_tts_model_title",
"type_info": {
"type": "VarString",
@@ -174,7 +184,7 @@
}
},
{
"ordinal": 17,
"ordinal": 18,
"name": "maybe_voice_conversion_model_title",
"type_info": {
"type": "VarString",
@@ -184,7 +194,7 @@
}
},
{
"ordinal": 18,
"ordinal": 19,
"name": "maybe_tts_public_bucket_path",
"type_info": {
"type": "VarString",
@@ -194,7 +204,7 @@
}
},
{
"ordinal": 19,
"ordinal": 20,
"name": "maybe_voice_conversion_public_bucket_hash",
"type_info": {
"type": "VarString",
@@ -204,7 +214,7 @@
}
},
{
"ordinal": 20,
"ordinal": 21,
"name": "maybe_media_file_public_bucket_directory_hash",
"type_info": {
"type": "VarString",
@@ -214,7 +224,7 @@
}
},
{
"ordinal": 21,
"ordinal": 22,
"name": "maybe_media_file_public_bucket_prefix",
"type_info": {
"type": "VarString",
@@ -224,7 +234,7 @@
}
},
{
"ordinal": 22,
"ordinal": 23,
"name": "maybe_media_file_public_bucket_extension",
"type_info": {
"type": "VarString",
@@ -234,7 +244,7 @@
}
},
{
"ordinal": 23,
"ordinal": 24,
"name": "maybe_assigned_worker",
"type_info": {
"type": "VarString",
@@ -244,7 +254,7 @@
}
},
{
"ordinal": 24,
"ordinal": 25,
"name": "maybe_assigned_cluster",
"type_info": {
"type": "VarString",
@@ -254,7 +264,7 @@
}
},
{
"ordinal": 25,
"ordinal": 26,
"name": "is_keepalive_required",
"type_info": {
"type": "Tiny",
@@ -264,7 +274,7 @@
}
},
{
"ordinal": 26,
"ordinal": 27,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -274,7 +284,7 @@
}
},
{
"ordinal": 27,
"ordinal": 28,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -284,7 +294,7 @@
}
},
{
"ordinal": 28,
"ordinal": 29,
"name": "maybe_first_started_at",
"type_info": {
"type": "Timestamp",
@@ -294,7 +304,7 @@
}
},
{
"ordinal": 29,
"ordinal": 30,
"name": "maybe_successfully_completed_at",
"type_info": {
"type": "Timestamp",
@@ -304,7 +314,7 @@
}
},
{
"ordinal": 30,
"ordinal": 31,
"name": "database_clock",
"type_info": {
"type": "Datetime",
@@ -343,6 +353,7 @@
true,
true,
true,
true,
false,
false,
false,
@@ -351,5 +362,5 @@
false
]
},
"hash": "90366e1705a0343bdfe4fc472d701b8e1dbd3a2dabedaadb633ac3542de72fa7"
"hash": "243afe0bc574f9f233c7797add9faf96440e2bafd52032e07951272fb1ec78b1"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nUPDATE generic_inference_jobs\nSET\n status = ?,\n on_success_result_entity_type = ?,\n on_success_result_entity_token = ?,\n on_success_result_batch_token = ?,\n failure_reason = NULL,\n internal_debugging_failure_reason = NULL,\n success_execution_millis = NULL,\n success_inference_execution_millis = NULL,\n retry_at = NULL,\n successfully_completed_at = NOW()\nWHERE token = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 5
},
"nullable": []
},
"hash": "7e223e284b992b6eaf31bec057bae00fc4e085d68b064e370f0e3d92b82049b2"
}
@@ -0,0 +1,311 @@
{
"db_name": "MySQL",
"query": "\nSELECT\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.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.maybe_mime_type,\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 m.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n\n m.maybe_batch_token as `maybe_batch_token: tokens::tokens::batch_generations::BatchGenerationToken`,\n\n m.maybe_title,\n m.maybe_text_transcript,\n\n m.maybe_origin_filename,\n\n m.maybe_duration_millis,\n\n m.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n prompts.maybe_other_args as maybe_other_prompt_args,\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.is_user_upload,\n m.is_intermediate_system_file,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m\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\nLEFT OUTER JOIN prompts\n ON prompts.token = m.maybe_prompt_token\nWHERE\n m.maybe_batch_token = ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"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": 1,
"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": 2,
"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": 3,
"name": "maybe_mime_type",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 4,
"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": 5,
"name": "maybe_creator_username",
"type_info": {
"type": "VarString",
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 6,
"name": "maybe_creator_display_name",
"type_info": {
"type": "VarString",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 7,
"name": "maybe_creator_gravatar_hash",
"type_info": {
"type": "String",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 8,
"name": "maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 9,
"name": "maybe_batch_token: tokens::tokens::batch_generations::BatchGenerationToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 10,
"name": "maybe_title",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 11,
"name": "maybe_text_transcript",
"type_info": {
"type": "Blob",
"flags": "BLOB | BINARY",
"char_set": 224,
"max_size": 262140
}
},
{
"ordinal": 12,
"name": "maybe_origin_filename",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 13,
"name": "maybe_duration_millis",
"type_info": {
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 14,
"name": "maybe_prompt_token: tokens::tokens::prompts::PromptToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 15,
"name": "maybe_other_prompt_args",
"type_info": {
"type": "Blob",
"flags": "BLOB | BINARY",
"char_set": 224,
"max_size": 262140
}
},
{
"ordinal": 16,
"name": "maybe_file_cover_image_public_bucket_hash",
"type_info": {
"type": "VarString",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 17,
"name": "maybe_file_cover_image_public_bucket_prefix",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 18,
"name": "maybe_file_cover_image_public_bucket_extension",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 19,
"name": "creator_set_visibility: enums::common::visibility::Visibility",
"type_info": {
"type": "String",
"flags": "NOT_NULL | BINARY | ENUM",
"char_set": 224,
"max_size": 28
}
},
{
"ordinal": 20,
"name": "is_user_upload",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL | MULTIPLE_KEY",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 21,
"name": "is_intermediate_system_file",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL | MULTIPLE_KEY",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 22,
"name": "public_bucket_directory_hash",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 23,
"name": "maybe_public_bucket_prefix",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 24,
"name": "maybe_public_bucket_extension",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 25,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 26,
"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": 1
},
"nullable": [
false,
false,
false,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
false,
false,
false,
false,
true,
true,
false,
false
]
},
"hash": "971471f349bcd22364b16b5d6c68e807ed897101eec1e8a62a7b8fab3359fa0c"
}
@@ -1,12 +0,0 @@
{
"db_name": "MySQL",
"query": "\nUPDATE generic_inference_jobs\nSET\n status = ?,\n on_success_result_entity_type = ?,\n on_success_result_entity_token = ?,\n failure_reason = NULL,\n internal_debugging_failure_reason = NULL,\n success_execution_millis = NULL,\n success_inference_execution_millis = NULL,\n retry_at = NULL,\n successfully_completed_at = NOW()\nWHERE token = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
},
"hash": "f68a8917e639418a0bb952bef606c96299001e143c745ee30df99b72d1235d35"
}
@@ -0,0 +1,311 @@
{
"db_name": "MySQL",
"query": "\nSELECT\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.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.maybe_mime_type,\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 m.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n\n m.maybe_batch_token as `maybe_batch_token: tokens::tokens::batch_generations::BatchGenerationToken`,\n\n m.maybe_title,\n m.maybe_text_transcript,\n\n m.maybe_origin_filename,\n\n m.maybe_duration_millis,\n\n m.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n prompts.maybe_other_args as maybe_other_prompt_args,\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.is_user_upload,\n m.is_intermediate_system_file,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m\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\nLEFT OUTER JOIN prompts\n ON prompts.token = m.maybe_prompt_token\nWHERE\n m.maybe_batch_token = ?\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"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": 1,
"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": 2,
"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": 3,
"name": "maybe_mime_type",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 4,
"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": 5,
"name": "maybe_creator_username",
"type_info": {
"type": "VarString",
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 6,
"name": "maybe_creator_display_name",
"type_info": {
"type": "VarString",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 7,
"name": "maybe_creator_gravatar_hash",
"type_info": {
"type": "String",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 8,
"name": "maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 9,
"name": "maybe_batch_token: tokens::tokens::batch_generations::BatchGenerationToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 10,
"name": "maybe_title",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 11,
"name": "maybe_text_transcript",
"type_info": {
"type": "Blob",
"flags": "BLOB | BINARY",
"char_set": 224,
"max_size": 262140
}
},
{
"ordinal": 12,
"name": "maybe_origin_filename",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 13,
"name": "maybe_duration_millis",
"type_info": {
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 14,
"name": "maybe_prompt_token: tokens::tokens::prompts::PromptToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 15,
"name": "maybe_other_prompt_args",
"type_info": {
"type": "Blob",
"flags": "BLOB | BINARY",
"char_set": 224,
"max_size": 262140
}
},
{
"ordinal": 16,
"name": "maybe_file_cover_image_public_bucket_hash",
"type_info": {
"type": "VarString",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 17,
"name": "maybe_file_cover_image_public_bucket_prefix",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 18,
"name": "maybe_file_cover_image_public_bucket_extension",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 19,
"name": "creator_set_visibility: enums::common::visibility::Visibility",
"type_info": {
"type": "String",
"flags": "NOT_NULL | BINARY | ENUM",
"char_set": 224,
"max_size": 28
}
},
{
"ordinal": 20,
"name": "is_user_upload",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL | MULTIPLE_KEY",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 21,
"name": "is_intermediate_system_file",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL | MULTIPLE_KEY",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 22,
"name": "public_bucket_directory_hash",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 23,
"name": "maybe_public_bucket_prefix",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 24,
"name": "maybe_public_bucket_extension",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 25,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 26,
"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": 1
},
"nullable": [
false,
false,
false,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
false,
false,
false,
false,
true,
true,
false,
false
]
},
"hash": "f75fedb86a93ac2542315ddb7ef0d46aa42ecc25a3cf9856219114064183337a"
}
Generated
+1
View File
@@ -709,6 +709,7 @@ version = "0.0.1"
dependencies = [
"chrono",
"enums",
"enums_public",
"serde",
"serde_derive",
"serde_json",
@@ -7,5 +7,4 @@ pub mod jobs;
pub mod media_files;
pub mod prompts;
pub mod recipes;
pub mod shared_response_types;
pub mod utils;
@@ -1,12 +1,12 @@
use crate::error::api_error::ApiError;
use crate::error::storyteller_error::StorytellerError;
use crate::shared_response_types::media_file_cover_image_details::MediaFileCoverImageDetails;
use crate::shared_response_types::media_links::MediaLinks;
use crate::shared_response_types::simple_entity_stats::SimpleEntityStats;
use crate::shared_response_types::user_details_light::UserDetailsLight;
use crate::utils::api_host::ApiHost;
use crate::utils::filter_bad_response::filter_bad_response;
use crate::utils::http_get_anonymous::http_get_anonymous;
use artcraft_api_defs::common::responses::media_file_cover_image_details::MediaFileCoverImageDetails;
use artcraft_api_defs::common::responses::media_links::MediaLinks;
use artcraft_api_defs::common::responses::simple_entity_stats::SimpleEntityStats;
use artcraft_api_defs::common::responses::user_details_light::UserDetailsLight;
use chrono::{DateTime, Utc};
use enums::by_table::media_files::media_file_animation_type::MediaFileAnimationType;
use enums::by_table::media_files::media_file_class::MediaFileClass;
@@ -0,0 +1,19 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_get_request::basic_json_get_request;
use artcraft_api_defs::media_file::list_batch_generated_media_files::{ListBatchGeneratedReduxMediaFilesSuccessResponse, LIST_BATCH_GENERATED_REDUX_MEDIA_FILES_URL_PATH};
use tokens::tokens::batch_generations::BatchGenerationToken;
pub async fn list_batch_generated_redux_media_files(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
batch_token: &BatchGenerationToken,
) -> Result<ListBatchGeneratedReduxMediaFilesSuccessResponse, StorytellerError> {
let url = format!("{}/{}", LIST_BATCH_GENERATED_REDUX_MEDIA_FILES_URL_PATH, batch_token.to_string());
Ok(basic_json_get_request(
api_host,
&url,
maybe_creds,
).await?)
}
@@ -1,4 +1,5 @@
pub mod get_media_file;
pub mod list_batch_generated_redux_media_files;
pub mod upload_image_media_file_from_bytes;
pub mod upload_image_media_file_from_file;
pub mod upload_new_engine_asset_from_file;
@@ -2,10 +2,6 @@ use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::api_error::ApiError;
use crate::error::client_error::ClientError;
use crate::error::storyteller_error::StorytellerError;
use crate::shared_response_types::media_file_cover_image_details::MediaFileCoverImageDetails;
use crate::shared_response_types::media_links::MediaLinks;
use crate::shared_response_types::simple_entity_stats::SimpleEntityStats;
use crate::shared_response_types::user_details_light::UserDetailsLight;
use crate::utils::api_host::ApiHost;
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
use crate::utils::filter_bad_response::filter_bad_response;
@@ -2,10 +2,6 @@ use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::api_error::ApiError;
use crate::error::client_error::ClientError;
use crate::error::storyteller_error::StorytellerError;
use crate::shared_response_types::media_file_cover_image_details::MediaFileCoverImageDetails;
use crate::shared_response_types::media_links::MediaLinks;
use crate::shared_response_types::simple_entity_stats::SimpleEntityStats;
use crate::shared_response_types::user_details_light::UserDetailsLight;
use crate::utils::api_host::ApiHost;
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
use crate::utils::filter_bad_response::filter_bad_response;
@@ -1,9 +1,5 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::api_error::ApiError;
use crate::shared_response_types::media_file_cover_image_details::MediaFileCoverImageDetails;
use crate::shared_response_types::media_links::MediaLinks;
use crate::shared_response_types::simple_entity_stats::SimpleEntityStats;
use crate::shared_response_types::user_details_light::UserDetailsLight;
use crate::utils::api_host::ApiHost;
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
use crate::utils::filter_bad_response::filter_bad_response;
@@ -1,9 +1,5 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::api_error::ApiError;
use crate::shared_response_types::media_file_cover_image_details::MediaFileCoverImageDetails;
use crate::shared_response_types::media_links::MediaLinks;
use crate::shared_response_types::simple_entity_stats::SimpleEntityStats;
use crate::shared_response_types::user_details_light::UserDetailsLight;
use crate::utils::api_host::ApiHost;
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
use crate::utils::filter_bad_response::filter_bad_response;
@@ -1,34 +0,0 @@
use serde_derive::Deserialize;
use url::Url;
/// Links to media file locations (bucket, CDN, etc.)
#[derive(Deserialize, Debug)]
pub struct MediaLinks {
/// Primary link to the asset via the CDN.
pub cdn_url: Url,
/// Template to construct thumbnail URLs.
/// Replace the string `{WIDTH}` with the desired width.
/// Only relevant for image media files. (Video media files instead have
/// video previews, which, in turn, have their own thumbnail templates.)
pub maybe_thumbnail_template: Option<String>,
/// Video preview images (still and animated gif) for mp4 video files.
/// These are only set for video media files.
/// These are not set for "cover images" that are gif or webms, but rather video files.
pub maybe_video_previews: Option<VideoPreviews>,
}
#[derive(Deserialize, Debug)]
pub struct VideoPreviews {
/// A static single frame preview image of the video.
pub still: Url,
/// An animated gif preview of the video.
pub animated: Url,
/// A template used to construct the still thumbnail URL.
/// Replace the string `{WIDTH}` with the desired width.
pub still_thumbnail_template: String,
/// A template used to construct the animated thumbnail URL.
/// Replace the string `{WIDTH}` with the desired width.
pub animated_thumbnail_template: String,
}
@@ -1,5 +0,0 @@
pub mod cover_image_links;
pub mod media_file_cover_image_details;
pub mod media_links;
pub mod simple_entity_stats;
pub mod user_details_light;
@@ -41,6 +41,7 @@ use storyteller_client::error::storyteller_error::StorytellerError;
use storyteller_client::media_files::get_media_file::get_media_file;
use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, Manager, State};
use enums::tauri::ux::tauri_command_caller::TauriCommandCaller;
use tokens::tokens::media_files::MediaFileToken;
#[derive(Deserialize, Debug)]
@@ -72,6 +73,21 @@ pub struct EnqueueContextualEditImageCommand {
/// Image quality.
pub image_quality: Option<EditImageQuality>,
/// OPTIONAL.
/// Name of the frontend caller.
/// We'll use this to selectively trigger events.
pub frontend_caller: Option<TauriCommandCaller>,
/// OPTIONAL.
/// A frontend-defined identifier that we'll send back to the frontend
/// as a Tauri event on task completion.
pub frontend_subscriber_id: Option<String>,
/// OPTIONAL.
/// A frontend-defined payload that we'll send back to the frontend
/// as a Tauri event on task completion.
pub frontend_subscriber_payload: Option<String>,
}
#[derive(Deserialize, Debug, Copy, Clone)]
@@ -237,7 +253,12 @@ pub async fn handle_request(
};
let result = success_event
.insert_into_task_database(task_database)
.insert_into_task_database_with_frontend_payload(
task_database,
request.frontend_caller.clone(),
request.frontend_subscriber_id.as_deref(),
request.frontend_subscriber_payload.as_deref(),
)
.await;
if let Err(err) = result {
@@ -135,7 +135,7 @@ pub async fn handle_flux_dev_juggernaut_inpaint_artcraft(
provider: GenerationProvider::Artcraft,
model: Some(GenerationModel::FluxDevJuggernaut),
provider_job_id: Some(job_id.to_string()),
task_type: TaskType::ImageGeneration,
task_type: TaskType::ImageInpaintEdit,
})
}
@@ -134,7 +134,7 @@ pub async fn handle_flux_pro_1_inpaint_artcraft(
provider: GenerationProvider::Artcraft,
model: Some(GenerationModel::FluxProKontextMax),
provider_job_id: Some(job_id.to_string()),
task_type: TaskType::ImageGeneration,
task_type: TaskType::ImageInpaintEdit,
})
}
@@ -24,6 +24,7 @@ impl TaskEnqueueSuccess{
TaskType::VideoGeneration => GenerationAction::GenerateVideo,
TaskType::BackgroundRemoval => GenerationAction::RemoveBackground,
TaskType::ObjectGeneration => GenerationAction::ImageTo3d,
TaskType::ImageInpaintEdit => GenerationAction::ImageInpaintEdit,
}
}
@@ -0,0 +1,27 @@
use crate::core::events::basic_sendable_event_trait::{BasicEventStatus, BasicSendableEvent};
use enums::tauri::ux::tauri_event_name::TauriEventName;
use serde_derive::Serialize;
use tokens::tokens::media_files::MediaFileToken;
use url::Url;
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct ImageEditCompleteEvent {
// pub source_media_token: MediaFileToken,
pub edited_images: Vec<EditedImage>,
pub maybe_frontend_subscriber_id: Option<String>,
pub maybe_frontend_subscriber_payload: Option<String>,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct EditedImage {
pub media_token: MediaFileToken,
pub cdn_url: Url,
pub maybe_thumbnail_template: Option<String>,
}
impl BasicSendableEvent for ImageEditCompleteEvent {
const FRONTEND_EVENT_NAME: TauriEventName = TauriEventName::ImageEditCompleteEvent;
const EVENT_STATUS: BasicEventStatus = BasicEventStatus::Success;
}
@@ -1,3 +1,4 @@
pub mod canvas_background_removal_complete_event;
pub mod image_edit_complete_event;
pub mod refresh_account_state_event;
pub mod show_provider_login_modal_event;
@@ -65,4 +65,6 @@ pub enum GenerationAction {
RemoveBackground,
#[serde(rename = "image_to_3d")]
ImageTo3d,
#[serde(rename = "image_inpaint_edit")]
ImageInpaintEdit,
}
@@ -7,5 +7,6 @@ pub fn to_generation_action(task_type: TaskType) -> GenerationAction {
TaskType::VideoGeneration => GenerationAction::GenerateVideo,
TaskType::BackgroundRemoval => GenerationAction::RemoveBackground,
TaskType::ObjectGeneration => GenerationAction::ImageTo3d,
TaskType::ImageInpaintEdit => GenerationAction::ImageInpaintEdit,
}
}
@@ -0,0 +1,125 @@
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::functional_events::canvas_background_removal_complete_event::CanvasBackgroundRemovalCompleteEvent;
use crate::core::events::functional_events::image_edit_complete_event::{EditedImage, ImageEditCompleteEvent};
use crate::core::events::generation_events::generation_complete_event::GenerationCompleteEvent;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use anyhow::anyhow;
use artcraft_api_defs::jobs::list_session_jobs::{ListSessionJobsItem, ListSessionResultDetailsResponse};
use enums::tauri::tasks::task_type::TaskType;
use enums::tauri::ux::tauri_command_caller::TauriCommandCaller;
use errors::AnyhowResult;
use log::{error, warn};
use sqlite_tasks::queries::list_tasks_by_provider_and_tokens::{list_tasks_by_provider_and_tokens, ListTasksArgs, Task};
use storyteller_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
use storyteller_client::media_files::list_batch_generated_redux_media_files::list_batch_generated_redux_media_files;
use tauri::AppHandle;
use tokens::tokens::batch_generations::BatchGenerationToken;
use tokens::tokens::media_files::MediaFileToken;
pub async fn maybe_handle_inpainting_complete_event(
app: &AppHandle,
app_env_configs: &AppEnvConfigs,
maybe_creds: Option<&StorytellerCredentialSet>,
task: &Task,
job: &ListSessionJobsItem,
) -> AnyhowResult<()> {
match task.task_type {
TaskType::ImageInpaintEdit => {} // NB: Fall-through
_ => return Ok(()),
}
match task.frontend_caller {
Some(TauriCommandCaller::ImageEditor) => {} // NB: Fall-through
_ => return Ok(()),
}
let job_result = match job.maybe_result {
Some(ref res) => res,
None => {
error!("Job result is None for task: {:?}", task);
return Ok(()); // No result, nothing to do
},
};
let event = match job_result.maybe_batch_token.as_ref() {
Some(batch_token) => {
handle_batch(
app,
app_env_configs,
maybe_creds,
task,
job,
job_result,
batch_token
).await?
}
None => {
handle_single(
app,
task,
job,
job_result,
).await?
}
};
if let Err(err) = event.send(&app) {
error!("Failed to send ImageEditCompleteEvent: {:?}", err); // Fail open
}
Ok(())
}
pub async fn handle_single(
app: &AppHandle,
task: &Task,
job: &ListSessionJobsItem,
job_result: &ListSessionResultDetailsResponse,
) -> AnyhowResult<ImageEditCompleteEvent> {
Ok(ImageEditCompleteEvent {
edited_images: vec![EditedImage {
media_token: MediaFileToken::new_from_str(&job_result.entity_token),
cdn_url: job_result.media_links.cdn_url.clone(),
maybe_thumbnail_template: job_result.media_links.maybe_thumbnail_template.clone(),
}],
maybe_frontend_subscriber_id: task.frontend_subscriber_id.clone(),
maybe_frontend_subscriber_payload: task.frontend_subscriber_payload.clone(),
})
}
pub async fn handle_batch(
app: &AppHandle,
app_env_configs: &AppEnvConfigs,
maybe_creds: Option<&StorytellerCredentialSet>,
task: &Task,
job: &ListSessionJobsItem,
job_result: &ListSessionResultDetailsResponse,
batch_token: &BatchGenerationToken,
) -> AnyhowResult<ImageEditCompleteEvent> {
let result = list_batch_generated_redux_media_files(
&app_env_configs.storyteller_host,
maybe_creds,
batch_token,
).await?;
if result.media_files.is_empty() {
return Err(anyhow!("No media files found for batch token: {}", batch_token));
}
let media_files = result.media_files
.into_iter()
.map(|file| EditedImage {
media_token: file.token,
cdn_url: file.media_links.cdn_url,
maybe_thumbnail_template: file.media_links.maybe_thumbnail_template,
})
.collect();
Ok(ImageEditCompleteEvent {
edited_images: media_files,
maybe_frontend_subscriber_id: task.frontend_subscriber_id.clone(),
maybe_frontend_subscriber_payload: task.frontend_subscriber_payload.clone(),
})
}
@@ -1 +1,2 @@
pub mod maybe_handle_inpainting_complete_event;
pub mod maybe_send_background_removal_complete_event;
@@ -33,6 +33,9 @@ use storyteller_client::jobs::list_session_jobs::{list_session_jobs, States};
use storyteller_client::media_files::upload_image_media_file_from_file::upload_image_media_file_from_file;
use tauri::AppHandle;
use tempdir::TempDir;
use artcraft_api_defs::jobs::list_session_jobs::ListSessionJobsItem;
use storyteller_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::services::storyteller::threads::events::maybe_handle_inpainting_complete_event::maybe_handle_inpainting_complete_event;
pub async fn storyteller_task_polling_thread(
app_handle: AppHandle,
@@ -140,16 +143,14 @@ async fn polling_loop(
continue; // If anything breaks with queries, don't spam events.
}
let result = maybe_send_background_removal_complete_event(
send_additional_events(
&app_handle,
&task,
&app_env_configs,
creds.as_ref(),
&job,
&task,
).await;
if let Err(err) = result {
error!("Failed to send background removal complete event: {:?}", err);
}
let service = to_generation_service_provider(task.provider);
let action = to_generation_action(task.task_type);
@@ -166,3 +167,35 @@ async fn polling_loop(
}
}
async fn send_additional_events(
app_handle: &AppHandle,
app_env_configs: &AppEnvConfigs,
creds: Option<&StorytellerCredentialSet>,
job: &ListSessionJobsItem,
task: &Task
) {
info!("Attempting to dispatch events for completed Storyteller : {:?}", task);
let result = maybe_handle_inpainting_complete_event(
app_handle,
app_env_configs,
creds,
task,
job,
).await;
if let Err(err) = result {
error!("Failed to send image inpainting complete event: {:?}", err);
}
let result = maybe_send_background_removal_complete_event(
app_handle,
task,
job,
).await;
if let Err(err) = result {
error!("Failed to send background removal complete event: {:?}", err);
}
}
@@ -1,19 +1,27 @@
use anyhow::anyhow;
use sqlx::MySqlPool;
use sqlx::{Executor, MySql};
use std::marker::PhantomData;
use enums::by_table::generic_inference_jobs::inference_result_type::InferenceResultType;
use enums::common::job_status_plus::JobStatusPlus;
use errors::AnyhowResult;
use tokens::tokens::batch_generations::BatchGenerationToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
// TODO: Maybe in the future we set a batch_token instead of media token
pub async fn mark_fal_generic_inference_job_successfully_done(
pool: &MySqlPool,
job_token: &InferenceJobToken,
media_file_token: MediaFileToken,
pub struct MarkJobArgs<'e, 'c, E>
where E: 'e + Executor<'c, Database = MySql>
{
pub job_token: &'e InferenceJobToken,
pub media_file_token: &'e MediaFileToken,
pub maybe_batch_token: Option<&'e BatchGenerationToken>,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
}
pub async fn mark_fal_generic_inference_job_successfully_done<'e, 'c : 'e, E>(
args: MarkJobArgs<'e, 'c, E>
) -> AnyhowResult<()>
where E: 'e + Executor<'c, Database = MySql>
{
const STATUS : JobStatusPlus = JobStatusPlus::CompleteSuccess;
const RESULT_TYPE : InferenceResultType = InferenceResultType::MediaFile;
@@ -25,6 +33,7 @@ SET
status = ?,
on_success_result_entity_type = ?,
on_success_result_entity_token = ?,
on_success_result_batch_token = ?,
failure_reason = NULL,
internal_debugging_failure_reason = NULL,
success_execution_millis = NULL,
@@ -35,10 +44,11 @@ WHERE token = ?
"#,
STATUS.to_str(),
RESULT_TYPE.to_str(),
media_file_token,
job_token,
args.media_file_token,
args.maybe_batch_token.map(|t| t.as_str()),
args.job_token,
)
.execute(pool)
.execute(args.mysql_executor)
.await;
match query_result {
@@ -11,6 +11,7 @@ use enums::common::job_status_plus::JobStatusPlus;
use enums::traits::mysql_from_row::MySqlFromRow;
use errors::AnyhowResult;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::batch_generations::BatchGenerationToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::users::UserToken;
use tokens::traits::mysql_token_from_row::MySqlTokenFromRow;
@@ -102,6 +103,7 @@ SELECT
jobs.on_success_result_entity_type as maybe_result_entity_type,
jobs.on_success_result_entity_token as maybe_result_entity_token,
jobs.on_success_result_batch_token as maybe_result_batch_token,
model_weights.title as maybe_model_weights_title,
tts_models.title as maybe_tts_model_title,
@@ -230,6 +232,9 @@ fn raw_records_to_public_result(records: Vec<RawGenericInferenceJobStatus>) -> V
ResultDetails {
entity_type: entity_type.to_string(),
entity_token: entity_token.to_string(),
maybe_batch_token: record.maybe_result_batch_token
.as_ref()
.map(|token| BatchGenerationToken::new_from_str(token)),
public_bucket_location_or_hash: public_bucket_hash.to_string(),
maybe_media_file_public_bucket_prefix: record.maybe_media_file_public_bucket_prefix.clone(),
maybe_media_file_public_bucket_extension: record.maybe_media_file_public_bucket_extension.clone(),
@@ -293,6 +298,7 @@ struct RawGenericInferenceJobStatus {
pub maybe_result_entity_type: Option<String>,
pub maybe_result_entity_token: Option<String>,
pub maybe_result_batch_token: Option<String>,
pub maybe_model_weights_title: Option<String>,
pub maybe_tts_model_title: Option<String>,
@@ -349,6 +355,7 @@ impl FromRow<'_, MySqlRow> for RawGenericInferenceJobStatus {
maybe_inference_args: row.try_get("maybe_inference_args")?,
maybe_result_entity_type: row.try_get("maybe_result_entity_type")?,
maybe_result_entity_token: row.try_get("maybe_result_entity_token")?,
maybe_result_batch_token: row.try_get("maybe_result_batch_token")?,
maybe_model_weights_title: row.try_get("maybe_model_weights_title")?,
maybe_tts_model_title: row.try_get("maybe_tts_model_title")?,
maybe_voice_conversion_model_title: row.try_get("maybe_voice_conversion_model_title")?,
@@ -11,6 +11,7 @@ use enums::common::job_status_plus::JobStatusPlus;
use enums::no_table::style_transfer::style_transfer_name::StyleTransferName;
use errors::AnyhowResult;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::batch_generations::BatchGenerationToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::users::UserToken;
@@ -61,6 +62,7 @@ SELECT
jobs.on_success_result_entity_type as maybe_result_entity_type,
jobs.on_success_result_entity_token as maybe_result_entity_token,
jobs.on_success_result_batch_token as maybe_result_batch_token,
model_weights.title as maybe_model_weights_title,
tts_models.title as maybe_tts_model_title,
@@ -206,6 +208,9 @@ fn raw_record_to_public_result(record: RawGenericInferenceJobStatus) -> GenericI
ResultDetails {
entity_type: entity_type.to_string(),
entity_token: entity_token.to_string(),
maybe_batch_token: record.maybe_result_batch_token
.as_ref()
.map(|token| BatchGenerationToken::new_from_str(token)),
public_bucket_location_or_hash: public_bucket_hash.to_string(),
maybe_media_file_public_bucket_prefix: record.maybe_media_file_public_bucket_prefix.clone(),
maybe_media_file_public_bucket_extension: record.maybe_media_file_public_bucket_extension.clone(),
@@ -267,6 +272,7 @@ struct RawGenericInferenceJobStatus {
pub maybe_result_entity_type: Option<String>,
pub maybe_result_entity_token: Option<String>,
pub maybe_result_batch_token: Option<String>,
pub maybe_model_weights_title: Option<String>,
pub maybe_tts_model_title: Option<String>,
@@ -6,6 +6,7 @@ use enums::by_table::generic_inference_jobs::inference_job_product_category::Inf
use enums::common::job_status_plus::JobStatusPlus;
use enums::no_table::style_transfer::style_transfer_name::StyleTransferName;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::batch_generations::BatchGenerationToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::users::UserToken;
@@ -67,6 +68,8 @@ pub struct ResultDetails {
pub entity_type: String,
pub entity_token: String,
pub maybe_batch_token: Option<BatchGenerationToken>,
/// The bucket storage hash (for vc and media_files) or full path (for tts)
pub public_bucket_location_or_hash: String,
pub maybe_media_file_public_bucket_prefix: Option<String>,
@@ -13,6 +13,7 @@ use enums::common::job_status_plus::JobStatusPlus;
use enums::traits::mysql_from_row::MySqlFromRow;
use errors::AnyhowResult;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::batch_generations::BatchGenerationToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::users::UserToken;
use tokens::traits::mysql_token_from_row::MySqlTokenFromRow;
@@ -168,6 +169,7 @@ SELECT
jobs.on_success_result_entity_type as maybe_result_entity_type,
jobs.on_success_result_entity_token as maybe_result_entity_token,
jobs.on_success_result_batch_token as maybe_result_batch_token,
model_weights.title as maybe_model_weights_title,
tts_models.title as maybe_tts_model_title,
@@ -296,6 +298,9 @@ fn raw_records_to_public_result(records: Vec<RawGenericInferenceJobStatus>) -> V
ResultDetails {
entity_type: entity_type.to_string(),
entity_token: entity_token.to_string(),
maybe_batch_token: record.maybe_result_batch_token
.as_ref()
.map(|token| BatchGenerationToken::new_from_str(token)),
public_bucket_location_or_hash: public_bucket_hash.to_string(),
maybe_media_file_public_bucket_prefix: record.maybe_media_file_public_bucket_prefix.clone(),
maybe_media_file_public_bucket_extension: record.maybe_media_file_public_bucket_extension.clone(),
@@ -359,6 +364,7 @@ struct RawGenericInferenceJobStatus {
pub maybe_result_entity_type: Option<String>,
pub maybe_result_entity_token: Option<String>,
pub maybe_result_batch_token: Option<String>,
pub maybe_model_weights_title: Option<String>,
pub maybe_tts_model_title: Option<String>,
@@ -415,6 +421,7 @@ impl FromRow<'_, MySqlRow> for RawGenericInferenceJobStatus {
maybe_inference_args: row.try_get("maybe_inference_args")?,
maybe_result_entity_type: row.try_get("maybe_result_entity_type")?,
maybe_result_entity_token: row.try_get("maybe_result_entity_token")?,
maybe_result_batch_token: row.try_get("maybe_result_batch_token")?,
maybe_model_weights_title: row.try_get("maybe_model_weights_title")?,
maybe_tts_model_title: row.try_get("maybe_tts_model_title")?,
maybe_voice_conversion_model_title: row.try_get("maybe_voice_conversion_model_title")?,
@@ -10,6 +10,7 @@ use enums::by_table::media_files::media_file_type::MediaFileType;
use enums::common::visibility::Visibility;
use sqlx::MySqlPool;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::batch_generations::BatchGenerationToken;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::prompts::PromptToken;
use tokens::tokens::users::UserToken;
@@ -67,7 +68,7 @@ pub struct MediaFileInsertBuilder {
// maybe_prompt_token: Option<&'a PromptToken>,
// // If batch generated, this is the batch token.
// maybe_batch_token: Option<&'a BatchGenerationToken>,
maybe_batch_generation_token: Option<BatchGenerationToken>,
// Storage details
public_bucket_directory_hash: Option<MediaFileBucketPath>, // NB: Non-nullable field(s)
@@ -100,6 +101,7 @@ impl MediaFileInsertBuilder {
maybe_frame_height: None,
checksum_sha2: None,
maybe_engine_category: None,
maybe_batch_generation_token: None,
public_bucket_directory_hash: None,
maybe_prompt_token: None,
}
@@ -228,7 +230,12 @@ impl MediaFileInsertBuilder {
// TODO: maybe_text_transcript
// TODO: maybe_scene_source_media_file_token
// TODO: maybe_prompt_token
// TODO: maybe_batch_token
pub fn maybe_batch_generation_token(mut self, maybe_batch_token: Option<&BatchGenerationToken>) -> Self {
self.maybe_batch_generation_token = maybe_batch_token
.map(|token| token.clone());
self
}
pub fn public_bucket_directory_hash(mut self, public_bucket_directory_hash: &MediaFileBucketPath) -> Self {
self.public_bucket_directory_hash = Some(public_bucket_directory_hash.clone());
@@ -281,7 +288,7 @@ impl MediaFileInsertBuilder {
maybe_text_transcript: None, // TODO
maybe_scene_source_media_file_token: None, // TODO
maybe_prompt_token: self.maybe_prompt_token.as_ref(),
maybe_batch_token: None, // TODO
maybe_batch_token: self.maybe_batch_generation_token.as_ref(),
public_bucket_directory_hash: bucket_path.get_object_hash(),
maybe_public_bucket_prefix: bucket_path.get_optional_prefix(),
maybe_public_bucket_extension: bucket_path.get_optional_extension(),
@@ -0,0 +1,385 @@
// NB: Incrementally getting rid of build warnings...
#![forbid(unused_imports)]
#![forbid(unused_mut)]
#![forbid(unused_variables)]
use chrono::{DateTime, Utc};
use enums::by_table::media_files::media_file_class::MediaFileClass;
use enums::by_table::media_files::media_file_type::MediaFileType;
use enums::common::visibility::Visibility;
use errors::AnyhowResult;
use log::warn;
use sqlx::pool::PoolConnection;
use sqlx::{MySql, MySqlPool};
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::batch_generations::BatchGenerationToken;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::prompts::PromptToken;
use tokens::tokens::users::UserToken;
use crate::helpers::boolean_converters::i8_to_bool;
use crate::payloads::prompt_args::prompt_inner_payload::PromptInnerPayload;
use crate::utils::transactor::Transactor;
pub struct MediaFileBatch {
pub media_files: Vec<MediaFile>,
}
#[derive(Serialize, Debug)]
pub struct MediaFile {
pub token: MediaFileToken,
pub media_class: MediaFileClass,
pub media_type: MediaFileType,
pub maybe_mime_type: Option<String>,
pub maybe_batch_token: Option<BatchGenerationToken>,
pub maybe_title: Option<String>,
pub maybe_text_transcript: Option<String>,
pub maybe_origin_filename: Option<String>,
pub maybe_duration_millis: Option<u64>,
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>,
/// This should not be exposed for GET endpoints, but is useful for permission checking.
pub maybe_creator_anonymous_visitor_token: Option<AnonymousVisitorTrackingToken>,
pub creator_set_visibility: Visibility,
pub is_user_upload: bool,
pub is_intermediate_system_file: bool,
pub maybe_prompt_token: Option<PromptToken>,
pub maybe_prompt_args: Option<PromptInnerPayload>,
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 public_bucket_directory_hash: String,
pub maybe_public_bucket_prefix: Option<String>,
pub maybe_public_bucket_extension: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
// pub maybe_moderator_fields: Option<MediaFileModeratorFields>,
}
#[derive(Serialize)]
pub struct MediaFileRaw {
pub token: MediaFileToken,
pub media_class: MediaFileClass,
pub media_type: MediaFileType,
pub maybe_mime_type: Option<String>,
pub maybe_batch_token: Option<BatchGenerationToken>,
pub maybe_title: Option<String>,
pub maybe_text_transcript: Option<String>,
pub maybe_origin_filename: Option<String>,
pub maybe_duration_millis: Option<i32>,
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>,
/// This should not be exposed for GET endpoints, but is useful for permission checking.
pub maybe_creator_anonymous_visitor_token: Option<AnonymousVisitorTrackingToken>,
pub creator_set_visibility: Visibility,
pub is_user_upload: i8,
pub is_intermediate_system_file: i8,
pub maybe_prompt_token: Option<PromptToken>,
pub maybe_other_prompt_args: 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 public_bucket_directory_hash: String,
pub maybe_public_bucket_prefix: Option<String>,
pub maybe_public_bucket_extension: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
pub async fn list_batch_generated_redux_media_files(
batch_token: &BatchGenerationToken,
can_see_deleted: bool,
mysql_pool: &MySqlPool
) -> AnyhowResult<MediaFileBatch> {
list_batch_generated_redux_media_files_with_transactor(
batch_token,
can_see_deleted,
Transactor::for_pool(mysql_pool)
).await
}
pub async fn list_batch_generated_redux_media_files_with_connection(
batch_token: &BatchGenerationToken,
can_see_deleted: bool,
mysql_connection: &mut PoolConnection<MySql>,
) -> AnyhowResult<MediaFileBatch> {
list_batch_generated_redux_media_files_with_transactor(
batch_token,
can_see_deleted,
Transactor::for_connection(mysql_connection)
).await
}
pub async fn list_batch_generated_redux_media_files_with_transactor(
batch_token: &BatchGenerationToken,
can_see_deleted: bool,
transactor: Transactor<'_, '_>,
) -> AnyhowResult<MediaFileBatch> {
let records = if can_see_deleted {
select_including_deleted(batch_token, transactor).await?
} else {
select_without_deleted(batch_token, transactor).await?
};
let records = records
.into_iter()
.map(|record| {
MediaFile {
token: record.token,
media_type: record.media_type,
media_class: record.media_class,
maybe_mime_type: record.maybe_mime_type,
maybe_batch_token: record.maybe_batch_token,
maybe_title: record.maybe_title,
maybe_text_transcript: record.maybe_text_transcript,
maybe_origin_filename: record.maybe_origin_filename,
maybe_duration_millis: record.maybe_duration_millis.map(|d| d as u64),
maybe_creator_user_token: record.maybe_creator_user_token,
maybe_creator_username: record.maybe_creator_username,
maybe_creator_display_name: record.maybe_creator_display_name,
maybe_creator_gravatar_hash: record.maybe_creator_gravatar_hash,
maybe_creator_anonymous_visitor_token: record.maybe_creator_anonymous_visitor_token,
creator_set_visibility: record.creator_set_visibility,
is_user_upload: i8_to_bool(record.is_user_upload),
is_intermediate_system_file: i8_to_bool(record.is_intermediate_system_file),
maybe_prompt_token: record.maybe_prompt_token,
maybe_prompt_args: record.maybe_other_prompt_args
.as_deref()
.map(|args| PromptInnerPayload::from_json(args))
.transpose()
.ok() // NB: Fail open
.flatten(),
maybe_file_cover_image_public_bucket_hash: record.maybe_file_cover_image_public_bucket_hash,
maybe_file_cover_image_public_bucket_prefix: record.maybe_file_cover_image_public_bucket_prefix,
maybe_file_cover_image_public_bucket_extension: record.maybe_file_cover_image_public_bucket_extension,
public_bucket_directory_hash: record.public_bucket_directory_hash,
maybe_public_bucket_prefix: record.maybe_public_bucket_prefix,
maybe_public_bucket_extension: record.maybe_public_bucket_extension,
created_at: record.created_at,
updated_at: record.updated_at,
}
})
.collect();
Ok(MediaFileBatch {
media_files: records,
})
}
async fn select_including_deleted(
batch_token: &BatchGenerationToken,
transactor: Transactor<'_, '_>,
) -> Result<Vec<MediaFileRaw>, sqlx::Error> {
let query = sqlx::query_as!(
MediaFileRaw,
r#"
SELECT
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.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,
m.maybe_mime_type,
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,
m.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,
m.maybe_batch_token as `maybe_batch_token: tokens::tokens::batch_generations::BatchGenerationToken`,
m.maybe_title,
m.maybe_text_transcript,
m.maybe_origin_filename,
m.maybe_duration_millis,
m.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,
prompts.maybe_other_args as maybe_other_prompt_args,
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.is_user_upload,
m.is_intermediate_system_file,
m.public_bucket_directory_hash,
m.maybe_public_bucket_prefix,
m.maybe_public_bucket_extension,
m.created_at,
m.updated_at
FROM media_files AS m
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
LEFT OUTER JOIN prompts
ON prompts.token = m.maybe_prompt_token
WHERE
m.maybe_batch_token = ?
"#,
batch_token
);
let maybe_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
},
};
match maybe_results {
Err(err) => {
match err {
sqlx::Error::RowNotFound => {
Ok(Vec::new())
},
_ => {
warn!("list ip bans db error: {:?}", err);
Err(err)
}
}
},
Ok(results) => Ok(results),
}
}
async fn select_without_deleted(
batch_token: &BatchGenerationToken,
transactor: Transactor<'_, '_>,
) -> Result<Vec<MediaFileRaw>, sqlx::Error> {
let query = sqlx::query_as!(
MediaFileRaw,
r#"
SELECT
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.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,
m.maybe_mime_type,
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,
m.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,
m.maybe_batch_token as `maybe_batch_token: tokens::tokens::batch_generations::BatchGenerationToken`,
m.maybe_title,
m.maybe_text_transcript,
m.maybe_origin_filename,
m.maybe_duration_millis,
m.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,
prompts.maybe_other_args as maybe_other_prompt_args,
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.is_user_upload,
m.is_intermediate_system_file,
m.public_bucket_directory_hash,
m.maybe_public_bucket_prefix,
m.maybe_public_bucket_extension,
m.created_at,
m.updated_at
FROM media_files AS m
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
LEFT OUTER JOIN prompts
ON prompts.token = m.maybe_prompt_token
WHERE
m.maybe_batch_token = ?
AND m.user_deleted_at IS NULL
AND m.mod_deleted_at IS NULL
"#,
batch_token
);
let maybe_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
},
};
match maybe_results {
Err(err) => {
match err {
sqlx::Error::RowNotFound => {
Ok(Vec::new())
},
_ => {
warn!("list ip bans db error: {:?}", err);
Err(err)
}
}
},
Ok(results) => Ok(results),
}
}
@@ -1,3 +1,4 @@
pub mod list_batch_generated_redux_media_files;
pub mod list_featured_media_files;
pub mod list_media_files;
pub mod list_media_files_by_batch_token;
@@ -21,6 +21,7 @@ workspace-build-acceleration.workspace = true
# Internal
enums.workspace = true
enums_public.workspace = true
tokens.workspace = true
# External
@@ -1,8 +1,11 @@
use serde_derive::Deserialize;
use serde_derive::{Deserialize, Serialize};
use url::Url;
// TODO(bt,2025-08-20): Replace the storyteller-web version of this.
// All it needs is the impl ported into a builder.
/// Cover image links can only be jpg, png, etc. No videos.
#[derive(Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug)]
pub struct CoverImageLinks {
/// Primary link to the cover image via the CDN.
pub cdn_url: Url,
@@ -1,19 +1,22 @@
use serde_derive::{Deserialize};
use url::Url;
use crate::shared_response_types::cover_image_links::CoverImageLinks;
use crate::common::responses::cover_image_links::CoverImageLinks;
use serde_derive::{Deserialize, Serialize};
// TODO(bt,2025-08-20): Replace the storyteller-web version of this.
// We can't drop the old version just yet due to the `impl` (easy)
// and a few differing fields.
/// Everything we need to create a cover image.
/// Cover images are small descriptive images that can be set for any media file.
/// If a cover image is set, this is the path to the asset.
#[derive(Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug)]
pub struct MediaFileCoverImageDetails {
// /// (DEPRECATED) URL path to the media file
// #[deprecated(note="This field doesn't point to the full URL. Use media_links instead to leverage the CDN.")]
// pub maybe_cover_image_public_bucket_path: Option<String>,
// /// (DEPRECATED) URL path to the media file
// #[deprecated(note="This field doesn't point to the full URL. Use media_links instead to leverage the CDN.")]
// pub maybe_cover_image_public_bucket_path: Option<String>,
// /// (DEPRECATED) Full URL to the media file
// #[deprecated(note="This points to the bucket. Use media_links instead to leverage the CDN.")]
// pub maybe_cover_image_public_bucket_url: Option<Url>,
// /// (DEPRECATED) Full URL to the media file
// #[deprecated(note="This points to the bucket. Use media_links instead to leverage the CDN.")]
// pub maybe_cover_image_public_bucket_url: Option<Url>,
// NB(bt,2024-09-19): I accidentally rolled this field out to production.
// I don't think this field is in use, but maybe ...
@@ -33,7 +36,7 @@ pub struct MediaFileCoverImageDetails {
/// The default cover is composed of an image and color pair that are
/// predefined by the frontend.
#[derive(Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug)]
pub struct MediaFileDefaultCover {
pub image_index: u8,
pub color_index: u8,
@@ -1,2 +1,6 @@
pub mod cover_image_links;
pub mod job_details;
pub mod media_links;
pub mod media_file_cover_image_details;
pub mod media_links;
pub mod simple_entity_stats;
pub mod user_details_light;
@@ -1,7 +1,9 @@
use serde_derive::Deserialize;
use serde_derive::{Deserialize, Serialize};
// TODO(bt,2025-08-20): Replace the storyteller-web version of this.
/// Simple stats that can be attached to any entity
#[derive(Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug)]
pub struct SimpleEntityStats {
/// Number of positive ratings (or "likes") for this item
pub positive_rating_count: u32,
@@ -1,8 +1,8 @@
use serde_derive::Deserialize;
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::users::UserToken;
/// Everything we need to refer to a user on the public web interface.
#[derive(Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug)]
pub struct UserDetailsLight {
/// The token for the user
pub user_token: UserToken,
@@ -26,7 +26,7 @@ pub struct UserDetailsLight {
// In the future, we'll also support user-uploaded avatars that we store on our servers.
}
#[derive(Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug)]
pub struct UserDefaultAvatarInfo {
pub image_index: u8,
pub color_index: u8,
@@ -9,6 +9,7 @@ use serde::Deserialize;
use serde::Serialize;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use utoipa::{IntoParams, ToSchema};
use tokens::tokens::batch_generations::BatchGenerationToken;
pub const LIST_SESSION_JOBS_URL_PATH: &str = "/v1/jobs/session";
@@ -111,6 +112,9 @@ pub struct ListSessionStatusDetailsResponse {
pub struct ListSessionResultDetailsResponse {
pub entity_type: String,
pub entity_token: String,
/// If generated as part of a batch, the batch token.
pub maybe_batch_token: Option<BatchGenerationToken>,
/// (DEPRECATED) URL path to the media file
#[deprecated(note="This field doesn't point to the full URL. Use media_links instead to leverage the CDN.")]
@@ -1,4 +1,5 @@
pub mod common;
pub mod generate;
pub mod jobs;
pub mod media_file;
pub mod prompts;
@@ -0,0 +1,77 @@
use crate::common::responses::media_file_cover_image_details::MediaFileCoverImageDetails;
use crate::common::responses::media_links::MediaLinks;
use crate::common::responses::user_details_light::UserDetailsLight;
use chrono::{DateTime, Utc};
use enums::by_table::media_files::media_file_class::MediaFileClass;
use enums::by_table::media_files::media_file_type::MediaFileType;
use enums::common::visibility::Visibility;
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::batch_generations::BatchGenerationToken;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::prompts::PromptToken;
use utoipa::ToSchema;
pub const LIST_BATCH_GENERATED_REDUX_MEDIA_FILES_URL_PATH: &str = "/v1/media_files/batch_gen_redux";
/// For the URL PathInfo
#[derive(Serialize, Deserialize, ToSchema)]
pub struct ListBatchGeneratedReduxMediaFilesPathInfo {
pub token: BatchGenerationToken,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct ListBatchGeneratedReduxMediaFilesSuccessResponse {
pub success: bool,
pub media_files: Vec<BatchGeneratedReduxMediaFileInfo>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct BatchGeneratedReduxMediaFileInfo {
/// Primary key identifier
pub token: MediaFileToken,
/// The coarse-grained class of media file: image, video, etc.
pub media_class: MediaFileClass,
/// Type of media will dictate which fields are populated and what
/// the frontend should display (e.g. video player vs audio player).
/// This is closer in meaning to a "mime type".
pub media_type: MediaFileType,
/// If the file was generated as part of a batch, this is the token for the batch.
pub maybe_batch_token: Option<BatchGenerationToken>,
/// 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>,
/// File visibility.
pub creator_set_visibility: Visibility,
/// The foreign key to the prompt used to generate the media, if applicable.
pub maybe_prompt_token: Option<PromptToken>,
/// The name or title of the media file (optional)
pub maybe_title: Option<String>,
/// The original filename for uploaded files, if they were provided.
/// In the future we'll provide our own internal optional filenames.
pub maybe_original_filename: Option<String>,
/// Duration for audio and video files, if available.
/// Measured in milliseconds.
pub maybe_duration_millis: Option<u64>,
/// Created timestamp
pub created_at: DateTime<Utc>,
/// Updated timestamp
pub updated_at: DateTime<Utc>,
}
@@ -0,0 +1 @@
pub mod list_batch_generated_media_files;
@@ -11,6 +11,7 @@ use utoipa::ToSchema;
#[serde(rename_all = "snake_case")]
pub enum TaskType {
ImageGeneration,
ImageInpaintEdit,
VideoGeneration,
ObjectGeneration,
BackgroundRemoval,
@@ -26,6 +27,7 @@ impl TaskType {
pub fn to_str(&self) -> &'static str {
match self {
Self::ImageGeneration => "image_generation",
Self::ImageInpaintEdit => "image_inpaint_edit",
Self::VideoGeneration => "video_generation",
Self::ObjectGeneration => "object_generation",
Self::BackgroundRemoval => "background_removal",
@@ -35,6 +37,7 @@ impl TaskType {
pub fn from_str(job_status: &str) -> Result<Self, String> {
match job_status {
"image_generation" => Ok(Self::ImageGeneration),
"image_inpaint_edit" => Ok(Self::ImageInpaintEdit),
"video_generation" => Ok(Self::VideoGeneration),
"object_generation" => Ok(Self::ObjectGeneration),
"background_removal" => Ok(Self::BackgroundRemoval),
@@ -47,6 +50,7 @@ impl TaskType {
// NB: BTreeSet::from() isn't const, but not worth using LazyStatic, etc.
BTreeSet::from([
Self::ImageGeneration,
Self::ImageInpaintEdit,
Self::VideoGeneration,
Self::ObjectGeneration,
Self::BackgroundRemoval,
@@ -65,6 +69,7 @@ mod tests {
#[test]
fn test_serialization() {
assert_serialization(TaskType::ImageGeneration, "image_generation");
assert_serialization(TaskType::ImageInpaintEdit, "image_inpaint_edit");
assert_serialization(TaskType::VideoGeneration, "video_generation");
assert_serialization(TaskType::ObjectGeneration, "object_generation");
assert_serialization(TaskType::BackgroundRemoval, "background_removal");
@@ -73,6 +78,7 @@ mod tests {
#[test]
fn to_str() {
assert_eq!(TaskType::ImageGeneration.to_str(), "image_generation");
assert_eq!(TaskType::ImageInpaintEdit.to_str(), "image_inpaint_edit");
assert_eq!(TaskType::VideoGeneration.to_str(), "video_generation");
assert_eq!(TaskType::ObjectGeneration.to_str(), "object_generation");
assert_eq!(TaskType::BackgroundRemoval.to_str(), "background_removal");
@@ -81,6 +87,7 @@ mod tests {
#[test]
fn from_str() {
assert_eq!(TaskType::from_str("image_generation").unwrap(), TaskType::ImageGeneration);
assert_eq!(TaskType::from_str("image_inpaint_edit").unwrap(), TaskType::ImageInpaintEdit);
assert_eq!(TaskType::from_str("video_generation").unwrap(), TaskType::VideoGeneration);
assert_eq!(TaskType::from_str("object_generation").unwrap(), TaskType::ObjectGeneration);
assert_eq!(TaskType::from_str("background_removal").unwrap(), TaskType::BackgroundRemoval);
@@ -89,8 +96,9 @@ mod tests {
#[test]
fn all_variants() {
let mut variants = TaskType::all_variants();
assert_eq!(variants.len(), 4);
assert_eq!(variants.len(), 5);
assert_eq!(variants.pop_first(), Some(TaskType::ImageGeneration));
assert_eq!(variants.pop_first(), Some(TaskType::ImageInpaintEdit));
assert_eq!(variants.pop_first(), Some(TaskType::VideoGeneration));
assert_eq!(variants.pop_first(), Some(TaskType::ObjectGeneration));
assert_eq!(variants.pop_first(), Some(TaskType::BackgroundRemoval));
@@ -14,6 +14,8 @@ use utoipa::ToSchema;
pub enum TauriCommandCaller {
/// The 2D canvas
Canvas,
/// The inpainting editor
ImageEditor,
}
impl_enum_display_and_debug_using_to_str!(TauriCommandCaller);
@@ -26,12 +28,14 @@ impl TauriCommandCaller {
pub fn to_str(&self) -> &'static str {
match self {
Self::Canvas => "canvas",
Self::ImageEditor => "image_editor",
}
}
pub fn from_str(job_status: &str) -> Result<Self, String> {
match job_status {
"canvas" => Ok(Self::Canvas),
"image_editor" => Ok(Self::ImageEditor),
_ => Err(format!("invalid tauri_command_caller: {:?}", job_status)),
}
}
@@ -41,6 +45,7 @@ impl TauriCommandCaller {
// NB: BTreeSet::from() isn't const, but not worth using LazyStatic, etc.
BTreeSet::from([
Self::Canvas,
Self::ImageEditor,
])
}
}
@@ -56,23 +61,27 @@ mod tests {
#[test]
fn test_serialization() {
assert_serialization(TauriCommandCaller::Canvas, "canvas");
assert_serialization(TauriCommandCaller::ImageEditor, "image_editor");
}
#[test]
fn to_str() {
assert_eq!(TauriCommandCaller::Canvas.to_str(), "canvas");
assert_eq!(TauriCommandCaller::ImageEditor.to_str(), "image_editor");
}
#[test]
fn from_str() {
assert_eq!(TauriCommandCaller::from_str("canvas").unwrap(), TauriCommandCaller::Canvas);
assert_eq!(TauriCommandCaller::from_str("image_editor").unwrap(), TauriCommandCaller::ImageEditor);
}
#[test]
fn all_variants() {
let mut variants = TauriCommandCaller::all_variants();
assert_eq!(variants.len(), 1);
assert_eq!(variants.len(), 2);
assert_eq!(variants.pop_first(), Some(TauriCommandCaller::Canvas));
assert_eq!(variants.pop_first(), Some(TauriCommandCaller::ImageEditor));
assert_eq!(variants.pop_first(), None);
}
}
@@ -43,6 +43,11 @@ pub enum TauriEventName {
#[serde(rename = "canvas_bg_removed_event")]
CanvasBgRemovedEvent,
/// Special event:
/// Image edit is complete
#[serde(rename = "image_edit_complete_event")]
ImageEditCompleteEvent,
/// Special event:
/// Refresh account states
#[serde(rename = "refresh_account_state_event")]
@@ -73,6 +78,7 @@ impl TauriEventName {
Self::GenerationCompleteEvent => "generation-complete-event",
Self::GenerationFailedEvent => "generation-failed-event",
Self::CanvasBgRemovedEvent => "canvas_bg_removed_event",
Self::ImageEditCompleteEvent => "image_edit_complete_event",
Self::RefreshAccountStateEvent => "refresh_account_state_event",
Self::ShowProviderLoginModalEvent => "show_provider_login_modal_event",
Self::FlashUserInputErrorEvent => "flash_user_input_error_event",
@@ -86,6 +92,7 @@ impl TauriEventName {
"generation-complete-event" => Ok(Self::GenerationCompleteEvent),
"generation-failed-event" => Ok(Self::GenerationFailedEvent),
"canvas_bg_removed_event" => Ok(Self::CanvasBgRemovedEvent),
"image_edit_complete_event" => Ok(Self::ImageEditCompleteEvent),
"refresh_account_state_event" => Ok(Self::RefreshAccountStateEvent),
"show_provider_login_modal_event" => Ok(Self::ShowProviderLoginModalEvent),
"flash_user_input_error_event" => Ok(Self::FlashUserInputErrorEvent),
@@ -102,6 +109,7 @@ impl TauriEventName {
Self::GenerationCompleteEvent,
Self::GenerationFailedEvent,
Self::CanvasBgRemovedEvent,
Self::ImageEditCompleteEvent,
Self::RefreshAccountStateEvent,
Self::ShowProviderLoginModalEvent,
Self::FlashUserInputErrorEvent,
@@ -124,6 +132,7 @@ mod tests {
assert_serialization(TauriEventName::GenerationCompleteEvent, "generation-complete-event");
assert_serialization(TauriEventName::GenerationFailedEvent, "generation-failed-event");
assert_serialization(TauriEventName::CanvasBgRemovedEvent, "canvas_bg_removed_event");
assert_serialization(TauriEventName::ImageEditCompleteEvent, "image_edit_complete_event");
assert_serialization(TauriEventName::RefreshAccountStateEvent, "refresh_account_state_event");
assert_serialization(TauriEventName::ShowProviderLoginModalEvent, "show_provider_login_modal_event");
assert_serialization(TauriEventName::FlashUserInputErrorEvent, "flash_user_input_error_event");
@@ -136,6 +145,7 @@ mod tests {
assert_eq!(TauriEventName::GenerationCompleteEvent.to_str(), "generation-complete-event");
assert_eq!(TauriEventName::GenerationFailedEvent.to_str(), "generation-failed-event");
assert_eq!(TauriEventName::CanvasBgRemovedEvent.to_str(), "canvas_bg_removed_event");
assert_eq!(TauriEventName::ImageEditCompleteEvent.to_str(), "image_edit_complete_event");
assert_eq!(TauriEventName::RefreshAccountStateEvent.to_str(), "refresh_account_state_event");
assert_eq!(TauriEventName::ShowProviderLoginModalEvent.to_str(), "show_provider_login_modal_event");
assert_eq!(TauriEventName::FlashUserInputErrorEvent.to_str(), "flash_user_input_error_event");
@@ -148,6 +158,7 @@ mod tests {
assert_eq!(TauriEventName::from_str("generation-complete-event").unwrap(), TauriEventName::GenerationCompleteEvent);
assert_eq!(TauriEventName::from_str("generation-failed-event").unwrap(), TauriEventName::GenerationFailedEvent);
assert_eq!(TauriEventName::from_str("canvas_bg_removed_event").unwrap(), TauriEventName::CanvasBgRemovedEvent);
assert_eq!(TauriEventName::from_str("image_edit_complete_event").unwrap(), TauriEventName::ImageEditCompleteEvent);
assert_eq!(TauriEventName::from_str("refresh_account_state_event").unwrap(), TauriEventName::RefreshAccountStateEvent);
assert_eq!(TauriEventName::from_str("show_provider_login_modal_event").unwrap(), TauriEventName::ShowProviderLoginModalEvent);
assert_eq!(TauriEventName::from_str("flash_user_input_error_event").unwrap(), TauriEventName::FlashUserInputErrorEvent);
@@ -156,12 +167,13 @@ mod tests {
#[test]
fn all_variants() {
let mut variants = TauriEventName::all_variants();
assert_eq!(variants.len(), 8);
assert_eq!(variants.len(), 9);
assert_eq!(variants.pop_first(), Some(TauriEventName::GenerationEnqueueSuccessEvent));
assert_eq!(variants.pop_first(), Some(TauriEventName::GenerationEnqueueFailureEvent));
assert_eq!(variants.pop_first(), Some(TauriEventName::GenerationCompleteEvent));
assert_eq!(variants.pop_first(), Some(TauriEventName::GenerationFailedEvent));
assert_eq!(variants.pop_first(), Some(TauriEventName::CanvasBgRemovedEvent));
assert_eq!(variants.pop_first(), Some(TauriEventName::ImageEditCompleteEvent));
assert_eq!(variants.pop_first(), Some(TauriEventName::RefreshAccountStateEvent));
assert_eq!(variants.pop_first(), Some(TauriEventName::ShowProviderLoginModalEvent));
assert_eq!(variants.pop_first(), Some(TauriEventName::FlashUserInputErrorEvent));
@@ -0,0 +1,72 @@
use crate::http_server::common_responses::media::media_domain::MediaDomain;
use artcraft_api_defs::common::responses::cover_image_links::CoverImageLinks;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
// TODO(bt,2024-09-18): Consolidate thumbnail builder code and vars
// TODO(bt,2024-09-05): Worth reducing the quality at all?
const QUALITY : u8 = 95;
pub struct CoverImageLinksBuilder {}
impl CoverImageLinksBuilder {
pub fn from_media_path(
domain: MediaDomain,
bucket_path: &MediaFileBucketPath,
) -> Option<CoverImageLinks> {
let rooted_path = bucket_path.get_full_object_path_str();
Self::from_rooted_path(domain, rooted_path)
}
pub fn from_maybe_media_path(
domain: MediaDomain,
bucket_path: Option<&MediaFileBucketPath>,
) -> Option<CoverImageLinks> {
match bucket_path {
None => None,
Some(bucket_path) => Self::from_media_path(domain, bucket_path),
}
}
pub fn from_parts(
domain: MediaDomain,
maybe_cover_image_public_bucket_path: Option<&str>,
maybe_cover_image_public_bucket_prefix: Option<&str>,
maybe_cover_image_public_bucket_extension: Option<&str>,
) -> Option<CoverImageLinks> {
let bucket_path= match maybe_cover_image_public_bucket_path {
None => return None,
Some(path) => path,
};
let bucket_path = MediaFileBucketPath::from_object_hash(
bucket_path,
maybe_cover_image_public_bucket_prefix,
maybe_cover_image_public_bucket_extension
);
Self::from_media_path(domain, &bucket_path)
}
pub fn from_rooted_path(
domain: MediaDomain,
rooted_path: &str,
) -> Option<CoverImageLinks> {
if !rooted_path.ends_with(".jpg")
&& !rooted_path.ends_with(".png")
&& !rooted_path.ends_with(".gif") {
return None;
}
let mut cdn_url = domain.new_cdn_url();
cdn_url.set_path(rooted_path);
Some(CoverImageLinks {
cdn_url,
thumbnail_template: thumbnail_template(domain, rooted_path),
})
}
}
fn thumbnail_template(media_domain: MediaDomain, rooted_path: &str) -> String {
let host = media_domain.cdn_url_str();
format!("{host}/cdn-cgi/image/width={{WIDTH}},quality={QUALITY}{rooted_path}")
}
@@ -0,0 +1,151 @@
use crate::http_server::common_responses::media::cover_image_links_builder::CoverImageLinksBuilder;
use crate::http_server::common_responses::media::media_domain::MediaDomain;
use crate::http_server::web_utils::bucket_urls::bucket_url_from_media_path::bucket_url_from_media_path;
use artcraft_api_defs::common::responses::media_file_cover_image_details::{MediaFileCoverImageDetails, MediaFileDefaultCover};
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
/// There are currently 25 cover images numbered 0 to 24 (0-indexed).
/// The original dataset was numbered 1 - 25, but I renamed 25 to 0.
const NUMBER_OF_IMAGES : u64 = 25;
const NUMBER_OF_IMAGES_SALT_OFFSET : u8 = 5;
const NUMBER_OF_COLORS : u64 = 8;
const NUMBER_OF_COLORS_SALT_OFFSET : u8 = 1;
pub struct MediaFileCoverImageDetailsBuilder {}
pub struct MediaFileDefaultCoverBuilder {}
impl MediaFileCoverImageDetailsBuilder {
/// Typical constructor
pub fn from_token(token: &MediaFileToken) -> MediaFileCoverImageDetails {
Self::from_token_str(token.as_str())
}
/// For non-media file tokens (eg. emulated TTS results)
pub fn from_token_str(token: &str) -> MediaFileCoverImageDetails {
MediaFileCoverImageDetails {
// NB: The code using this builder does not populate or read these legacy fields:
// maybe_cover_image_public_bucket_path: None,
// maybe_cover_image_public_bucket_url: None,
// maybe_media_links: None,
maybe_links: None,
default_cover: MediaFileDefaultCoverBuilder::from_token_str(token),
}
}
pub fn from_optional_db_fields(
token: &MediaFileToken,
domain: MediaDomain,
maybe_cover_image_public_bucket_path: Option<&str>,
maybe_cover_image_public_bucket_prefix: Option<&str>,
maybe_cover_image_public_bucket_extension: Option<&str>,
) -> MediaFileCoverImageDetails {
Self::from_optional_db_str_fields(
token.as_str(),
domain,
maybe_cover_image_public_bucket_path,
maybe_cover_image_public_bucket_prefix,
maybe_cover_image_public_bucket_extension
)
}
pub fn from_optional_db_str_fields(
token: &str,
domain: MediaDomain,
maybe_cover_image_public_bucket_path: Option<&str>,
maybe_cover_image_public_bucket_prefix: Option<&str>,
maybe_cover_image_public_bucket_extension: Option<&str>,
) -> MediaFileCoverImageDetails {
let maybe_bucket_path = maybe_cover_image_public_bucket_path
.map(|hash| MediaFileBucketPath::from_object_hash(
hash,
maybe_cover_image_public_bucket_prefix,
maybe_cover_image_public_bucket_extension
));
let maybe_cover_image_public_bucket_path = maybe_bucket_path
.as_ref()
.map(|bucket_path| bucket_path
.get_full_object_path_str()
.to_string());
// NB: Fail construction open.
let maybe_cover_image_public_bucket_url = maybe_bucket_path
.as_ref()
.map(|bucket_path| bucket_url_from_media_path(bucket_path).ok())
.flatten();
let maybe_links = CoverImageLinksBuilder::from_maybe_media_path(
domain, maybe_bucket_path.as_ref());
// let maybe_media_links = maybe_bucket_path
// .map(|path| MediaLinks::from_media_path(domain, &path));
MediaFileCoverImageDetails {
// NB: The code using this builder does not populate or read these legacy fields:
// maybe_cover_image_public_bucket_path,
// maybe_cover_image_public_bucket_url,
// maybe_media_links,
maybe_links,
default_cover: MediaFileDefaultCoverBuilder::from_token_str(token),
}
}
}
impl MediaFileDefaultCoverBuilder {
/// Typical constructor
pub fn from_token(token: &MediaFileToken) -> MediaFileDefaultCover {
Self::from_token_str(token.as_str())
}
/// For non-media file tokens (eg. emulated TTS results)
pub fn from_token_str(token: &str) -> MediaFileDefaultCover {
MediaFileDefaultCover {
image_index: hash(token, NUMBER_OF_IMAGES, NUMBER_OF_IMAGES_SALT_OFFSET),
color_index: hash(token, NUMBER_OF_COLORS, NUMBER_OF_COLORS_SALT_OFFSET),
}
}
}
fn hash(token: &str, max_number: u64, salt: u8) -> u8 {
let mut hasher = DefaultHasher::new();
token.hash(&mut hasher);
salt.hash(&mut hasher);
let hash = hasher.finish();
let index= hash % max_number;
index as u8
}
#[cfg(test)]
mod tests {
use tokens::tokens::media_files::MediaFileToken;
use crate::http_server::common_responses::media::media_file_cover_image_details_builder::MediaFileDefaultCoverBuilder;
#[test]
fn test() {
let token = MediaFileToken::new_from_str("foo");
let cover = MediaFileDefaultCoverBuilder::from_token(&token);
assert_eq!(cover.color_index, 5);
assert_eq!(cover.image_index, 2);
let token = MediaFileToken::new_from_str("bar");
let cover = MediaFileDefaultCoverBuilder::from_token(&token);
assert_eq!(cover.color_index, 5);
assert_eq!(cover.image_index, 3);
let token = MediaFileToken::new_from_str("asdf");
let cover = MediaFileDefaultCoverBuilder::from_token(&token);
assert_eq!(cover.color_index, 0);
assert_eq!(cover.image_index, 23);
}
}
@@ -1,6 +1,8 @@
pub mod cdn_link;
pub mod cover_image_links;
pub mod cover_image_links_builder;
pub mod media_domain;
pub mod media_file_cover_image_details;
pub mod media_file_cover_image_details_builder;
pub mod media_links_builder;
pub mod weights_cover_image_details;
@@ -9,3 +9,4 @@ pub mod simple_response;
pub mod tag_info;
pub mod user_avatars;
pub mod user_details_lite;
pub mod user_details_lite_builder;
@@ -0,0 +1,107 @@
use artcraft_api_defs::common::responses::user_details_light::{UserDefaultAvatarInfo, UserDetailsLight};
use tokens::tokens::users::UserToken;
use utoipa::ToSchema;
use crate::http_server::common_responses::user_avatars::default_avatar_color_from_username::default_avatar_color_from_username;
use crate::http_server::common_responses::user_avatars::default_avatar_from_username::default_avatar_from_username;
pub struct UserDetailsLightBuilder {}
impl UserDetailsLightBuilder {
pub fn from_db_fields(
user_token: &UserToken,
username: &str,
display_name: &str,
gravatar_hash: &str,
) -> UserDetailsLight {
UserDetailsLight{
default_avatar: UserDefaultAvatarInfoBuilder::from_username(&username),
user_token: user_token.clone(),
username: username.to_string(),
display_name: display_name.to_string(),
gravatar_hash: gravatar_hash.to_string(),
}
}
pub fn from_optional_db_fields(
maybe_user_token: Option<&UserToken>,
maybe_username: Option<&str>,
maybe_display_name: Option<&str>,
maybe_gravatar_hash: Option<&str>,
) -> Option<UserDetailsLight> {
Self::from_optional_db_fields_owned(
maybe_user_token.map(|u| u.clone()),
maybe_username.map(|s|s.to_string()),
maybe_display_name.map(|s|s.to_string()),
maybe_gravatar_hash.map(|s|s.to_string()),
)
}
pub fn from_optional_db_fields_owned(
maybe_user_token: Option<UserToken>,
maybe_username: Option<String>,
maybe_display_name: Option<String>,
maybe_gravatar_hash: Option<String>,
) -> Option<UserDetailsLight> {
match (maybe_user_token, maybe_username, maybe_display_name, maybe_gravatar_hash) {
(Some(user_token), Some(username), Some(display_name), Some(gravatar_hash)) => {
Some(UserDetailsLight {
default_avatar: UserDefaultAvatarInfoBuilder::from_username(&username),
user_token,
username,
display_name,
gravatar_hash,
})
}
_ => {
None
}
}
}
}
pub struct UserDefaultAvatarInfoBuilder {}
impl UserDefaultAvatarInfoBuilder {
/// Default avatars are based on username, not user token.
/// NB(bt,2023-08-23): I think the thinking here was that we'd always have the
/// username on hand and that it was more entropic.
pub fn from_username(username: &str) -> UserDefaultAvatarInfo {
UserDefaultAvatarInfo {
image_index: default_avatar_from_username(username),
color_index: default_avatar_color_from_username(username),
}
}
}
#[cfg(test)]
mod tests {
use tokens::tokens::users::UserToken;
use crate::http_server::common_responses::user_details_lite_builder::UserDetailsLightBuilder;
#[test]
fn test_from_optional_db_fields() {
let user_token = UserToken::new_from_str("token");
let username = "username";
let display_name = "display_name";
let gravatar_hash= "adsf";
let user_details = UserDetailsLightBuilder::from_optional_db_fields(
Some(&user_token),
Some(username),
Some(display_name),
Some(gravatar_hash)
);
let user_details = user_details.expect("Should not be optional.");
assert_eq!(user_details.user_token, user_token);
assert_eq!(user_details.username, username);
assert_eq!(user_details.display_name, display_name);
assert_eq!(user_details.gravatar_hash, gravatar_hash);
assert_eq!(user_details.default_avatar.color_index, 5);
assert_eq!(user_details.default_avatar.image_index, 16);
}
}
@@ -358,6 +358,7 @@ fn db_record_to_response_payload(
ListSessionResultDetailsResponse {
entity_type: result_details.entity_type,
entity_token: result_details.entity_token,
maybe_batch_token: result_details.maybe_batch_token,
media_links: MediaLinksBuilder::from_rooted_path(media_domain, &public_bucket_media_path),
maybe_public_bucket_media_path: Some(public_bucket_media_path),
maybe_successfully_completed_at: result_details.maybe_successfully_completed_at,
@@ -0,0 +1,144 @@
use std::collections::HashSet;
use std::fmt;
use std::sync::Arc;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_file_cover_image_details_builder::MediaFileCoverImageDetailsBuilder;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::common_responses::user_details_lite_builder::UserDetailsLightBuilder;
use crate::http_server::endpoints::media_files::get::get_media_file_handler::GetMediaFilePathInfo;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::web_utils::bucket_urls::bucket_url_string_from_media_path::bucket_url_string_from_media_path;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::state::server_state::ServerState;
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::web::{Json, Path};
use actix_web::{web, HttpRequest, HttpResponse};
use actix_web_lab::extract::Query;
use artcraft_api_defs::common::responses::media_links::MediaLinks;
use artcraft_api_defs::common::responses::simple_entity_stats::SimpleEntityStats;
use artcraft_api_defs::media_file::list_batch_generated_media_files::{BatchGeneratedReduxMediaFileInfo, ListBatchGeneratedReduxMediaFilesPathInfo, ListBatchGeneratedReduxMediaFilesSuccessResponse};
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use chrono::{DateTime, Utc};
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_subtype::MediaFileSubtype;
use enums::by_table::media_files::media_file_type::MediaFileType;
use enums::by_table::model_weights::weights_category::WeightsCategory;
use enums::by_table::model_weights::weights_types::WeightsType;
use enums::common::visibility::Visibility;
use enums::no_table::style_transfer::style_transfer_name::StyleTransferName;
use enums_public::by_table::model_weights::public_weights_types::PublicWeightsType;
use log::warn;
use mysql_queries::queries::media_files::get::batch_get_media_files::batch_get_media_files;
use mysql_queries::queries::media_files::list::list_batch_generated_redux_media_files::list_batch_generated_redux_media_files_with_connection;
use tokens::tokens::batch_generations::BatchGenerationToken;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::model_weights::ModelWeightToken;
use tokens::tokens::prompts::PromptToken;
use utoipa::{IntoParams, ToSchema};
/// List media files generated as part of the same batch (redux endpoint)
#[utoipa::path(
get,
tag = "Media Files",
path = "/v1/media_files/batch_gen_redux/{token}",
responses(
(status = 200, description = "Success", body = ListBatchGeneratedReduxMediaFilesSuccessResponse),
),
params(
("request" = ListBatchGeneratedReduxMediaFilesPathInfo, description = "Payload for Request"),
)
)]
pub async fn list_batch_generated_redux_media_files_handler(
http_request: HttpRequest,
path: Path<ListBatchGeneratedReduxMediaFilesPathInfo>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<ListBatchGeneratedReduxMediaFilesSuccessResponse>, CommonWebError> {
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await?;
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
CommonWebError::ServerError
})?;
let mut show_deleted_results = false;
let mut is_moderator = false;
if let Some(user_session) = maybe_user_session {
// NB: Moderators can see deleted results.
// Original creators cannot see them (unless they're moderators!)
show_deleted_results = user_session.can_delete_other_users_tts_results;
// Moderators get to see all the fields.
is_moderator = user_session.can_delete_other_users_tts_results
|| user_session.can_edit_other_users_tts_models;
}
// NB(bt,2024-03-24): I'm sorry, this is gross. We're not respecting sorting, input ordering,
// de-duplication, (if we swap types), etc. Gotta go fast.
let result = list_batch_generated_redux_media_files_with_connection(
&path.token,
show_deleted_results,
&mut mysql_connection,
).await;
let media_files = match result {
Ok(batch) => batch.media_files,
Err(e) => {
warn!("query error: {:?}", e);
return Err(CommonWebError::ServerError);
}
};
let media_domain = get_media_domain(&http_request);
let media_files = media_files.into_iter()
.map(|result| {
let public_bucket_path = MediaFileBucketPath::from_object_hash(
&result.public_bucket_directory_hash,
result.maybe_public_bucket_prefix.as_deref(),
result.maybe_public_bucket_extension.as_deref());
BatchGeneratedReduxMediaFileInfo {
token: result.token.clone(),
media_class: result.media_class,
media_type: result.media_type,
maybe_batch_token: result.maybe_batch_token,
media_links: MediaLinksBuilder::from_media_path(media_domain, &public_bucket_path),
cover_image: MediaFileCoverImageDetailsBuilder::from_optional_db_fields(
&result.token,
media_domain,
result.maybe_file_cover_image_public_bucket_hash.as_deref(),
result.maybe_file_cover_image_public_bucket_prefix.as_deref(),
result.maybe_file_cover_image_public_bucket_extension.as_deref(),
),
maybe_title: result.maybe_title,
maybe_creator_user: UserDetailsLightBuilder::from_optional_db_fields_owned(
result.maybe_creator_user_token,
result.maybe_creator_username,
result.maybe_creator_display_name,
result.maybe_creator_gravatar_hash,
),
creator_set_visibility: result.creator_set_visibility,
maybe_prompt_token: result.maybe_prompt_token,
maybe_original_filename: result.maybe_origin_filename,
maybe_duration_millis: result.maybe_duration_millis,
created_at: result.created_at,
updated_at: result.updated_at,
}
})
.collect();
Ok(Json(ListBatchGeneratedReduxMediaFilesSuccessResponse {
success: true,
media_files,
}))
}
@@ -1,3 +1,4 @@
pub mod list_batch_generated_redux_media_files_handler;
pub mod list_featured_media_files_handler;
pub mod list_media_files_by_batch_token_handler;
pub mod list_media_files_for_user_handler;
@@ -15,7 +15,7 @@ use http_server_common::response::response_success_helpers::SimpleGenericJsonSuc
use http_server_common::response::serialize_as_json_error::serialize_as_json_error;
use log::{info, warn};
use mysql_queries::queries::generic_inference::fal::get_inference_job_by_fal_id::get_inference_job_by_fal_id;
use mysql_queries::queries::generic_inference::fal::mark_fal_generic_inference_job_successfully_done::mark_fal_generic_inference_job_successfully_done;
use mysql_queries::queries::generic_inference::fal::mark_fal_generic_inference_job_successfully_done::{mark_fal_generic_inference_job_successfully_done, MarkJobArgs};
use serde_json::Value;
use utoipa::ToSchema;
// 1. tauri --> hit endpoint to enqueue
@@ -95,7 +95,7 @@ impl From<anyhow::Error> for FalWebhookError {
/// Fal webhook
#[utoipa::path(
post,
tag = "Webooks",
tag = "Webhooks",
path = "/v1/webhooks/fal",
responses(
(status = 200, description = "Success", body = SimpleGenericJsonSuccess),
@@ -143,6 +143,7 @@ pub async fn fal_webhook_handler(
info!("Fal webhook job record: {:?}", job);
let mut maybe_media_token = None;
let mut maybe_batch_token = None;
if let Some(payload_obj) = payload.as_object() {
if payload_obj.contains_key("image") {
@@ -150,7 +151,7 @@ pub async fn fal_webhook_handler(
let token = handle_image_payload(payload_obj, &job, &server_state).await?;
maybe_media_token = Some(token);
} else if payload_obj.contains_key("images") {
maybe_media_token = handle_images_payload(payload_obj, &job, &server_state).await?;
(maybe_media_token, maybe_batch_token) = handle_images_payload(payload_obj, &job, &server_state).await?;
} else if payload_obj.contains_key("video") {
info!("Handling video payload for job: {:?}", job.job_token);
let token = handle_video_payload(payload_obj, &job, &server_state).await?;
@@ -165,11 +166,13 @@ pub async fn fal_webhook_handler(
if let Some(media_token) = maybe_media_token {
info!("Media file token: {:?}", media_token);
// TODO: Update job metadata.
mark_fal_generic_inference_job_successfully_done(
&server_state.mysql_pool,
&job.job_token,
media_token,
).await.map_err(|err| {
mark_fal_generic_inference_job_successfully_done(MarkJobArgs {
job_token: &job.job_token,
media_file_token: &media_token,
maybe_batch_token: maybe_batch_token.as_ref(),
mysql_executor: &server_state.mysql_pool,
phantom: Default::default(),
}).await.map_err(|err| {
warn!("Error marking job as successfully done: {:?}", err);
FalWebhookError::ServerError
})?;
@@ -15,6 +15,7 @@ use mimetypes::mimetype_info::mimetype_info::MimetypeInfo;
use mysql_queries::queries::generic_inference::fal::get_inference_job_by_fal_id::FalJobDetails;
use mysql_queries::queries::media_files::create::insert_builder::media_file_insert_builder::MediaFileInsertBuilder;
use serde_json::{Map, Value};
use tokens::tokens::batch_generations::BatchGenerationToken;
use tokens::tokens::media_files::MediaFileToken;
const PREFIX : Option<&str> = Some("artcraft_");
@@ -33,8 +34,8 @@ pub async fn handle_images_payload(
payload: &Map<String, Value>,
job: &FalJobDetails,
server_state: &ServerState,
) -> AnyhowResult<Option<MediaFileToken>> {
) -> AnyhowResult<(Option<MediaFileToken>, Option<BatchGenerationToken>)> {
let images_value = payload.get("images")
.ok_or_else(|| anyhow!("no `images` key in payload"))?;
@@ -43,22 +44,31 @@ pub async fn handle_images_payload(
let images: Vec<FalWebhookImage> = serde_json::from_value(images_value.clone())?;
let mut maybe_media_token = None;
// NB: We are not going to create `batch_generations` table records. We don't need them.
// The foreign key in `media_files` is enough to look up the rest of the batch.
let mut maybe_batch_token = None;
if images.len() > 1 {
maybe_batch_token = Some(BatchGenerationToken::generate());
}
for image in images {
let media_token = upload_image(job, server_state, &image).await?;
let media_token = upload_image(job, server_state, &image, maybe_batch_token.as_ref()).await?;
if maybe_media_token.is_none() {
maybe_media_token = Some(media_token); // Set the first media token
}
}
Ok(maybe_media_token)
Ok((maybe_media_token, maybe_batch_token))
}
async fn upload_image(
job: &FalJobDetails,
server_state: &ServerState,
image: &FalWebhookImage,
maybe_batch_token: Option<&BatchGenerationToken>,
) -> AnyhowResult<MediaFileToken> {
let image_url = image.url
.as_deref()
@@ -98,6 +108,7 @@ async fn upload_image(
image,
&file_bytes,
mimetype_info,
maybe_batch_token,
).await
}
_ => {
@@ -107,6 +118,7 @@ async fn upload_image(
image,
&file_bytes,
mimetype_info,
maybe_batch_token,
).await
}
}
@@ -118,6 +130,7 @@ async fn upload_image_bytes(
image: &FalWebhookImage,
file_bytes: &[u8],
mimetype_info: MimetypeInfo,
maybe_batch_token: Option<&BatchGenerationToken>,
) -> AnyhowResult<MediaFileToken> {
let mime_type = mimetype_info.mime_type();
@@ -145,7 +158,7 @@ async fn upload_image_bytes(
&mime_type)
.await?;
let media_token = MediaFileInsertBuilder::new()
let mut query_builder = MediaFileInsertBuilder::new()
.maybe_creator_user(job.maybe_creator_user_token.as_ref())
.maybe_creator_anonymous_visitor(job.maybe_creator_anonymous_visitor_token.as_ref())
.creator_ip_address(&job.creator_ip_address)
@@ -159,11 +172,18 @@ async fn upload_image_bytes(
.frame_width(image_info.width())
.frame_height(image_info.height())
.maybe_prompt_token(job.maybe_prompt_token.as_ref())
.checksum_sha2(&file_hash)
.checksum_sha2(&file_hash);
if let Some(batch_token) = maybe_batch_token {
query_builder = query_builder.maybe_batch_generation_token(Some(batch_token));
}
let media_token = query_builder
.insert_pool(&server_state.mysql_pool)
.await?;
info!("Image media file uploaded with token: {}", media_token);
info!("Image media file uploaded with token: {} ; possible batch token: {:?}",
media_token, maybe_batch_token);
Ok(media_token)
}
@@ -12,6 +12,7 @@ use crate::http_server::endpoints::media_files::edit::set_media_file_cover_image
use crate::http_server::endpoints::media_files::edit::update_media_file_handler::update_media_file_handler;
use crate::http_server::endpoints::media_files::get::batch_get_media_files_handler::batch_get_media_files_handler;
use crate::http_server::endpoints::media_files::get::get_media_file_handler::get_media_file_handler;
use crate::http_server::endpoints::media_files::list::list_batch_generated_redux_media_files_handler::list_batch_generated_redux_media_files_handler;
use crate::http_server::endpoints::media_files::list::list_featured_media_files_handler::list_featured_media_files_handler;
use crate::http_server::endpoints::media_files::list::list_media_files_by_batch_token_handler::list_media_files_by_batch_token_handler;
use crate::http_server::endpoints::media_files::list::list_media_files_for_user_handler::list_media_files_for_user_handler;
@@ -76,6 +77,10 @@ pub fn add_media_file_routes<T, B> (app: App<T>) -> App<T>
.route(web::get().to(batch_get_media_files_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
.service(web::resource("/batch_gen_redux/{token}")
.route(web::get().to(list_batch_generated_redux_media_files_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
.service(web::resource("/list")
.route(web::get().to(list_media_files_handler))
.route(web::head().to(|| HttpResponse::Ok()))
@@ -22,6 +22,8 @@ import {
useModelSelectorStore,
} from "@storyteller/ui-model-selector";
import { ModelInfo } from "@storyteller/model-list";
import { useImageEditCompleteEvent } from "@storyteller/tauri-events";
const PAGE_ID: ModelPage = ModelPage.ImageEditor;
@@ -54,6 +56,17 @@ const PageEdit = () => {
// Use the Zustand store
const store = useEditStore();
useImageEditCompleteEvent(async (event) => {
console.log("Image edit complete:", event);
// TODO: Logic to show edit history
for (const editedImage of event.edited_images) {
console.log("Edited image:", editedImage.media_token);
console.log("Edited image url:", editedImage.cdn_url);
console.log("Edited image thumbnail template:", editedImage.maybe_thumbnail_template);
}
});
// Pass store actions directly as callbacks
useDeleteHotkeys({ onDelete: store.deleteSelectedItems });
@@ -273,6 +286,7 @@ const PageEdit = () => {
mask_image_raw_bytes: arrayBuffer,
prompt: prompt,
image_count: generationCount,
frontend_caller: "image_editor",
});
setIsEnqueuing(false);
@@ -17,6 +17,12 @@ export interface EnqueueImageInpaintRequest {
// Number of images to generate.
image_count?: number;
// TODO: Actual enum.
frontend_caller?: string;
// Optional frontend state to return later.
frontend_subscriber_id?: string;
}
@@ -28,6 +34,8 @@ interface RawEnqueueImageInpaintRequest {
mask_image_raw_bytes?: Uint8Array;
prompt?: string;
image_count?: number;
frontend_caller?: string;
frontend_subscriber_id?: string;
}
export enum EnqueueImageInpaintErrorType {
@@ -88,6 +96,14 @@ export const EnqueueImageInpaint = async (request: EnqueueImageInpaintRequest) :
mutableRequest.image_count = request.image_count;
}
if (!!request.frontend_caller) {
mutableRequest.frontend_caller = request.frontend_caller;
}
if (!!request.frontend_subscriber_id) {
mutableRequest.frontend_subscriber_id = request.frontend_subscriber_id;
}
const result = await invoke("enqueue_image_inpaint_command", {
request: mutableRequest,
});
+1
View File
@@ -2,6 +2,7 @@ export * from './lib/common/BasicEventWrapper';
export * from './lib/events/FlashUserInputErrorEvent';
export * from './lib/events/RefreshAccountStateEvent';
export * from './lib/events/ShowProviderLoginModalEvent';
export * from './lib/events/functional/ImageEditCompleteEvent';
export * from './lib/events/generation/useGenerationCompleteEvent';
export * from './lib/events/generation/useGenerationEnqueueFailureEvent';
export * from './lib/events/generation/useGenerationEnqueueSuccessEvent';
@@ -0,0 +1,42 @@
import { listen, UnlistenFn } from '@tauri-apps/api/event';
import { BasicEventWrapper } from '../../common/BasicEventWrapper';
import { useEffect } from 'react';
const EVENT_NAME : string = 'image_edit_complete_event';
export interface ImageEditCompleteEvent {
edited_images: EditedImage[],
maybe_frontend_subscriber_id?: string,
maybe_frontend_subscriber_payload?: string,
}
export interface EditedImage {
media_token: string,
cdn_url: string,
maybe_thumbnail_template?: string,
}
export const useImageEditCompleteEvent = (asyncCallback: (event: ImageEditCompleteEvent) => Promise<void>) => {
useEffect(() => {
let isUnmounted = false;
let unlisten: Promise<UnlistenFn>;
const setup = async () => {
unlisten = listen<BasicEventWrapper<ImageEditCompleteEvent>>(EVENT_NAME, async (wrappedEvent) => {
await asyncCallback(wrappedEvent.payload.data);
});
if (isUnmounted) {
unlisten.then(f => f()); // Unsubscribe if unmounted early.
}
};
setup();
return () => {
isUnmounted = true;
unlisten.then(f => f());
};
}, []);
}