filter featured media by product category

This commit is contained in:
Brandon Thomas committed 2024-08-29 01:06:34 -04:00
1 parent 6a30d0c0d2
commit 65f0718fb2
7 files changed
+269 -4

No files matched your search

@@ -97,6 +97,7 @@ pub struct ListFeaturedMediaFilesArgs<'a> {
pub maybe_filter_media_types: Option<&'a HashSet<MediaFileType>>,
pub maybe_filter_media_classes: Option<&'a HashSet<MediaFileClass>>,
pub maybe_filter_engine_categories: Option<&'a HashSet<MediaFileEngineCategory>>,
pub maybe_filter_product_categories: Option<&'a HashSet<MediaFileOriginProductCategory>>,
pub maybe_offset: Option<usize>,
pub cursor_is_reversed: bool,
pub sort_ascending: bool,
@@ -110,6 +111,7 @@ pub async fn list_featured_media_files(args: ListFeaturedMediaFilesArgs<'_>) ->
args.maybe_filter_media_types,
args.maybe_filter_media_classes,
args.maybe_filter_engine_categories,
args.maybe_filter_product_categories,
args.limit,
args.maybe_offset,
args.cursor_is_reversed,
@@ -185,6 +187,7 @@ fn query_builder<'a>(
maybe_filter_media_types: Option<&HashSet<MediaFileType>>,
maybe_filter_media_classes: Option<&HashSet<MediaFileClass>>,
maybe_filter_engine_categories: Option<&HashSet<MediaFileEngineCategory>>,
maybe_filter_product_categories: Option<&HashSet<MediaFileOriginProductCategory>>,
limit: usize,
maybe_offset: Option<usize>,
cursor_is_reversed: bool,
@@ -340,6 +343,28 @@ LEFT OUTER JOIN prompts
}
}
if let Some(product_categories) = maybe_filter_product_categories {
// NB: `WHERE IN` comma separated syntax will be wrong if list has zero length
// We'll skip the predicate if the list isn't empty.
if !product_categories.is_empty() {
if !first_predicate_added {
query_builder.push(" WHERE ");
first_predicate_added = true;
} else {
query_builder.push(" AND ");
}
query_builder.push(" m.origin_product_category IN ( ");
let mut separated = query_builder.separated(", ");
for product_category in product_categories.iter() {
separated.push_bind(product_category.to_str());
}
separated.push_unseparated(") ");
}
}
// TODO: Argument to control `is_intermediate_system_file`
{
if !first_predicate_added {
@@ -38,6 +38,7 @@ use tokens::tokens::user_bookmarks::*;
use tokens::tokens::users::*;
use tokens::tokens::zs_voice_datasets::*;
use crate::http_server::common_requests::auto_product_category::AutoProductCategory;
use crate::http_server::common_requests::media_file_token_path_info::MediaFileTokenPathInfo;
use crate::http_server::common_responses::media_file_cover_image_details::MediaFileCoverImageDetails;
use crate::http_server::common_responses::media_file_cover_image_details::MediaFileDefaultCover;
@@ -89,7 +90,6 @@ use crate::http_server::endpoints::media_files::upload::upload_audio_media_file_
use crate::http_server::endpoints::media_files::upload::upload_engine_asset::upload_engine_asset_media_file_handler::*;
use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError;
use crate::http_server::endpoints::media_files::upload::upload_generic::upload_media_file_handler::*;
use crate::http_server::endpoints::media_files::upload::upload_studio_shot::upload_studio_shot_media_file_handler::*;
use crate::http_server::endpoints::media_files::upload::upload_image_media_file_handler::*;
use crate::http_server::endpoints::media_files::upload::upload_new_engine_asset_media_file_handler::*;
use crate::http_server::endpoints::media_files::upload::upload_new_scene_media_file_handler::*;
@@ -97,6 +97,7 @@ use crate::http_server::endpoints::media_files::upload::upload_new_video_media_f
use crate::http_server::endpoints::media_files::upload::upload_pmx::upload_pmx_media_file_handler::*;
use crate::http_server::endpoints::media_files::upload::upload_saved_scene_media_file_handler::*;
use crate::http_server::endpoints::media_files::upload::upload_scene_snapshot_media_file_handler::*;
use crate::http_server::endpoints::media_files::upload::upload_studio_shot::upload_studio_shot_media_file_handler::*;
use crate::http_server::endpoints::media_files::upload::upload_video::upload_video_media_file_handler::*;
use crate::http_server::endpoints::media_files::upsert_upload::write_engine_asset::write_engine_asset_media_file_handler::*;
use crate::http_server::endpoints::media_files::upsert_upload::write_error::MediaFileWriteError;
@@ -129,8 +130,6 @@ use crate::http_server::endpoints::weights::list::list_available_weights_handler
use crate::http_server::endpoints::weights::list::list_featured_weights_handler::*;
use crate::http_server::endpoints::weights::list::list_pinned_weights_handler::*;
use crate::http_server::endpoints::weights::list::list_weights_by_user_handler::*;
use crate::http_server::endpoints::weights::search::search_model_weights_http_get_handler::*;
use crate::http_server::endpoints::weights::search::search_model_weights_http_post_handler::*;
use crate::http_server::endpoints::weights::search::search_model_weights_impl::*;
use crate::http_server::endpoints::weights::update::set_model_weight_cover_image_handler::*;
use crate::http_server::endpoints::weights::update::update_weight_handler::*;
@@ -267,6 +266,9 @@ use crate::http_server::web_utils::response_success_helpers::*;
WeightsCategory,
WeightsType,
// Other common enums
AutoProductCategory,
// Common path info
MediaFileTokenPathInfo,
@@ -0,0 +1,121 @@
use std::collections::HashSet;
use utoipa::ToSchema;
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
/// AutoProductCategory can be used in APIs to auto-expand to multiple product categories,
/// deprecated product categories, etc.
///
/// For example, "voice" expands to ["tts", "zs_voice", "voice_conversion"]
///
/// These can also alias bad product names, eg "live_portrait" is an alias for "face_mirror"
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Hash, PartialOrd, Ord, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum AutoProductCategory {
// ===== Synthetic product categories that alias or auto-expand to multiple ===== //
/// Synthetic category that expands to ["tts", "zs_voice", "voice_conversion"]
Voice,
// ===== Synthetic product categories that alias others ===== //
/// Synthetic category that expands to ["face_animator"]
Lipsync,
/// This is an alias for "face_mirror"
LivePortrait,
// ===== Current Database Product Categories ===== //
FaceAnimator,
FaceMirror,
#[serde(rename = "vst")]
VideoStyleTransfer,
#[serde(rename = "studio")]
StorytellerStudio,
#[serde(rename = "tts")]
TextToSpeech,
VoiceConversion,
#[serde(rename = "zs_voice")]
ZeroShotVoice,
Mocap,
#[serde(rename = "image_gen")]
ImageGeneration,
// ===== Deprecated Database Product Categories ===== //
#[deprecated]
VideoFilter,
#[deprecated]
Workflow,
}
impl AutoProductCategory {
pub fn expand_to_db_product_categories(&self) -> HashSet<MediaFileOriginProductCategory> {
match self {
// Synthetic product categories that auto-expand to multiple
AutoProductCategory::Voice => HashSet::from([
MediaFileOriginProductCategory::TextToSpeech,
MediaFileOriginProductCategory::ZeroShotVoice,
MediaFileOriginProductCategory::VoiceConversion,
]),
// Synthetic product categories that alias other product categories
AutoProductCategory::Lipsync => HashSet::from([MediaFileOriginProductCategory::FaceAnimator]),
AutoProductCategory::LivePortrait => HashSet::from([MediaFileOriginProductCategory::FaceMirror]),
// Current database product categories
AutoProductCategory::FaceAnimator => HashSet::from([MediaFileOriginProductCategory::FaceAnimator]),
AutoProductCategory::FaceMirror => HashSet::from([MediaFileOriginProductCategory::FaceMirror]),
AutoProductCategory::VideoStyleTransfer => HashSet::from([MediaFileOriginProductCategory::VideoStyleTransfer]),
AutoProductCategory::StorytellerStudio => HashSet::from([MediaFileOriginProductCategory::StorytellerStudio]),
AutoProductCategory::TextToSpeech => HashSet::from([MediaFileOriginProductCategory::TextToSpeech]),
AutoProductCategory::VoiceConversion => HashSet::from([MediaFileOriginProductCategory::VoiceConversion]),
AutoProductCategory::ZeroShotVoice => HashSet::from([MediaFileOriginProductCategory::ZeroShotVoice]),
AutoProductCategory::Mocap => HashSet::from([MediaFileOriginProductCategory::Mocap]),
AutoProductCategory::ImageGeneration => HashSet::from([MediaFileOriginProductCategory::ImageGeneration]),
// Deprecated database product categories
AutoProductCategory::VideoFilter => HashSet::from([MediaFileOriginProductCategory::VideoFilter]),
AutoProductCategory::Workflow => HashSet::from([MediaFileOriginProductCategory::Workflow]),
}
}
pub fn from_str(value: &str) -> Result<Self, String> {
match value {
// Synthetic product categories that auto-expand to multiple
"voice" => Ok(Self::Voice),
// Synthetic product categories that alias other product categories
"lipsync" => Ok(Self::Lipsync),
"live_portrait" => Ok(Self::LivePortrait),
// Current database product categories
"face_animator" => Ok(Self::FaceAnimator),
"face_mirror" => Ok(Self::FaceMirror),
"vst" => Ok(Self::VideoStyleTransfer),
"studio" => Ok(Self::StorytellerStudio),
"tts" => Ok(Self::TextToSpeech),
"voice_conversion" => Ok(Self::VoiceConversion),
"zs_voice" => Ok(Self::ZeroShotVoice),
"mocap" => Ok(Self::Mocap),
"image_gen" => Ok(Self::ImageGeneration),
// Deprecated database product categories
"video_filter" => Ok(Self::VideoFilter),
"workflow" => Ok(Self::Workflow),
_ => Err(format!("Invalid AutoProductCategory: {}", value)),
}
}
}
@@ -1 +1,2 @@
pub mod auto_product_category;
pub mod media_file_token_path_info;
@@ -0,0 +1,99 @@
use std::collections::HashSet;
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
use crate::http_server::common_requests::auto_product_category::AutoProductCategory;
pub fn get_scoped_product_categories(
maybe_query_param: Option<&str>
) -> Option<HashSet<MediaFileOriginProductCategory>> {
let categories = match maybe_query_param {
None => return None,
Some(categories) => categories,
};
// NB: This silently fails on invalid values. Probably not the best tactic.
let categories= categories.split(",")
.map(|c| AutoProductCategory::from_str(c))
.flatten()
.fold(HashSet::new(), |mut acc, cat| {
acc.extend(cat.expand_to_db_product_categories());
acc
});
if categories.is_empty() {
return None;
}
Some(categories)
}
#[cfg(test)]
mod test {
use std::collections::HashSet;
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
use crate::http_server::endpoints::media_files::helpers::get_scoped_product_categories::get_scoped_product_categories;
#[test]
fn none() {
assert_eq!(get_scoped_product_categories(None), None)
}
#[test]
fn empty() {
assert_eq!(get_scoped_product_categories(Some("")), None)
}
#[test]
fn garbage() {
assert_eq!(get_scoped_product_categories(Some("foo,bar,baz")), None)
}
#[test]
fn direct_map() {
assert_eq!(
get_scoped_product_categories(Some("mocap")),
Some(HashSet::from([
MediaFileOriginProductCategory::Mocap,
]))
);
assert_eq!(
get_scoped_product_categories(Some("face_animator,face_mirror")),
Some(HashSet::from([
MediaFileOriginProductCategory::FaceAnimator,
MediaFileOriginProductCategory::FaceMirror
]))
);
}
#[test]
fn alias_map() {
assert_eq!(
get_scoped_product_categories(Some("lipsync")),
Some(HashSet::from([
MediaFileOriginProductCategory::FaceAnimator,
]))
);
assert_eq!(
get_scoped_product_categories(Some("live_portrait")),
Some(HashSet::from([
MediaFileOriginProductCategory::FaceMirror,
]))
);
}
#[test]
fn multi_map() {
assert_eq!(
get_scoped_product_categories(Some("voice")),
Some(HashSet::from([
MediaFileOriginProductCategory::TextToSpeech,
MediaFileOriginProductCategory::VoiceConversion,
MediaFileOriginProductCategory::ZeroShotVoice,
]))
);
}
}
@@ -1,3 +1,4 @@
pub mod get_scoped_engine_categories;
pub mod get_scoped_media_classes;
pub mod get_scoped_media_types;
pub mod get_scoped_media_types;
pub mod get_scoped_product_categories;
@@ -31,6 +31,7 @@ use crate::http_server::common_responses::user_details_lite::UserDetailsLight;
use crate::http_server::endpoints::media_files::helpers::get_scoped_engine_categories::get_scoped_engine_categories;
use crate::http_server::endpoints::media_files::helpers::get_scoped_media_classes::get_scoped_media_classes;
use crate::http_server::endpoints::media_files::helpers::get_scoped_media_types::get_scoped_media_types;
use crate::http_server::endpoints::media_files::helpers::get_scoped_product_categories::get_scoped_product_categories;
use crate::http_server::web_utils::bucket_urls::bucket_url_string_from_media_path::bucket_url_string_from_media_path;
use crate::state::server_state::ServerState;
use crate::util::allowed_explore_media_access::allowed_explore_media_access;
@@ -73,6 +74,19 @@ pub struct ListFeaturedMediaFilesQueryParams {
/// - `?filter_engine_categories=animation,character,object`
/// - etc.
pub filter_engine_categories: Option<String>,
/// NB: This can be one (or more comma-separated values) from `AutoProductCategory`.
/// AutoProductCategory is not a db enum directly and can expand to multiple categories for some values.
///
/// Usage:
/// - `?filter_products=voice`
/// - `?filter_products=tts,voice_conversion,zs_voice`
/// - `?filter_products=live_portrait`
/// - `?filter_products=lipsync`
/// - `?filter_products=vst`
/// - `?filter_products=studio`
/// - etc.
pub filter_products: Option<String>,
}
#[derive(Serialize, ToSchema)]
@@ -353,6 +367,7 @@ async fn database_lookup(
let maybe_filter_media_types = get_scoped_media_types(query.filter_media_type.as_deref());
let maybe_filter_media_classes = get_scoped_media_classes(query.filter_media_classes.as_deref());
let maybe_filter_engine_categories = get_scoped_engine_categories(query.filter_engine_categories.as_deref());
let maybe_filter_product_categories = get_scoped_product_categories(query.filter_engine_categories.as_deref());
// NB: No reason to show deleted or non-public featured items to mods or authors.
// That's just confusing and wastes an extra query.
@@ -367,6 +382,7 @@ async fn database_lookup(
maybe_filter_media_types: maybe_filter_media_types.as_ref(),
maybe_filter_media_classes: maybe_filter_media_classes.as_ref(),
maybe_filter_engine_categories: maybe_filter_engine_categories.as_ref(),
maybe_filter_product_categories: maybe_filter_product_categories.as_ref(),
mysql_pool: &server_state.mysql_pool,
}).await.map_err(|err| {
error!("DB error: {:?}", err);