diff --git a/.sqlx/query-423109f2dcee23a558c43ecaab0eb2836714d5316620daaa1127b447cad59212.json b/.sqlx/query-1c32bdb79ed2bc1b8988bbb6ea3f2a1db4ed4552d53cb4f31ff4b08de01691f7.json similarity index 85% rename from .sqlx/query-423109f2dcee23a558c43ecaab0eb2836714d5316620daaa1127b447cad59212.json rename to .sqlx/query-1c32bdb79ed2bc1b8988bbb6ea3f2a1db4ed4552d53cb4f31ff4b08de01691f7.json index c8eac2bc0a..fb0c810712 100644 --- a/.sqlx/query-423109f2dcee23a558c43ecaab0eb2836714d5316620daaa1127b447cad59212.json +++ b/.sqlx/query-1c32bdb79ed2bc1b8988bbb6ea3f2a1db4ed4552d53cb4f31ff4b08de01691f7.json @@ -1,6 +1,6 @@ { "db_name": "MySQL", - "query": "\nSELECT\n users.token as user_token,\n users.username,\n users.display_name,\n\n users.email_address,\n users.email_confirmed,\n users.email_gravatar_hash,\n\n users.maybe_stripe_customer_id,\n users.maybe_loyalty_program_key,\n\n users.disable_gravatar,\n users.auto_play_audio_preference,\n users.auto_play_video_preference,\n users.preferred_tts_result_visibility as `preferred_tts_result_visibility: enums::common::visibility::Visibility`,\n users.preferred_w2l_result_visibility as `preferred_w2l_result_visibility: enums::common::visibility::Visibility`,\n\n users.user_role_slug,\n users.is_banned,\n\n users.can_access_studio,\n\n user_roles.can_use_tts,\n user_roles.can_use_w2l,\n user_roles.can_delete_own_tts_results,\n user_roles.can_delete_own_w2l_results,\n user_roles.can_delete_own_account,\n\n user_roles.can_upload_tts_models,\n user_roles.can_upload_w2l_templates,\n user_roles.can_delete_own_tts_models,\n user_roles.can_delete_own_w2l_templates,\n\n user_roles.can_approve_w2l_templates,\n user_roles.can_edit_other_users_profiles,\n user_roles.can_edit_other_users_tts_models,\n user_roles.can_edit_other_users_w2l_templates,\n user_roles.can_delete_other_users_tts_models,\n user_roles.can_delete_other_users_tts_results,\n user_roles.can_delete_other_users_w2l_templates,\n user_roles.can_delete_other_users_w2l_results,\n user_roles.can_ban_users,\n user_roles.can_delete_users\n\nFROM users\nLEFT OUTER JOIN user_sessions\n ON users.token = user_sessions.user_token\nLEFT OUTER JOIN user_roles\n ON users.user_role_slug = user_roles.slug\nWHERE user_sessions.token = ?\n AND user_sessions.deleted_at IS NULL\n AND users.user_deleted_at IS NULL\n AND users.mod_deleted_at IS NULL\n ", + "query": "\nSELECT\n users.token as user_token,\n users.username,\n users.display_name,\n\n users.email_address,\n users.email_confirmed,\n users.email_gravatar_hash,\n\n users.maybe_stripe_customer_id,\n users.maybe_loyalty_program_key,\n\n users.disable_gravatar,\n users.auto_play_audio_preference,\n users.auto_play_video_preference,\n users.preferred_tts_result_visibility as `preferred_tts_result_visibility: enums::common::visibility::Visibility`,\n users.preferred_w2l_result_visibility as `preferred_w2l_result_visibility: enums::common::visibility::Visibility`,\n\n users.user_role_slug,\n users.is_banned,\n\n users.can_access_studio,\n users.maybe_feature_flags,\n\n user_roles.can_use_tts,\n user_roles.can_use_w2l,\n user_roles.can_delete_own_tts_results,\n user_roles.can_delete_own_w2l_results,\n user_roles.can_delete_own_account,\n\n user_roles.can_upload_tts_models,\n user_roles.can_upload_w2l_templates,\n user_roles.can_delete_own_tts_models,\n user_roles.can_delete_own_w2l_templates,\n\n user_roles.can_approve_w2l_templates,\n user_roles.can_edit_other_users_profiles,\n user_roles.can_edit_other_users_tts_models,\n user_roles.can_edit_other_users_w2l_templates,\n user_roles.can_delete_other_users_tts_models,\n user_roles.can_delete_other_users_tts_results,\n user_roles.can_delete_other_users_w2l_templates,\n user_roles.can_delete_other_users_w2l_results,\n user_roles.can_ban_users,\n user_roles.can_delete_users\n\nFROM users\nLEFT OUTER JOIN user_sessions\n ON users.token = user_sessions.user_token\nLEFT OUTER JOIN user_roles\n ON users.user_role_slug = user_roles.slug\nWHERE user_sessions.token = ?\n AND user_sessions.deleted_at IS NULL\n AND users.user_deleted_at IS NULL\n AND users.mod_deleted_at IS NULL\n ", "describe": { "columns": [ { @@ -165,6 +165,16 @@ }, { "ordinal": 16, + "name": "maybe_feature_flags", + "type_info": { + "type": "VarString", + "flags": "BINARY", + "char_set": 224, + "max_size": 1020 + } + }, + { + "ordinal": 17, "name": "can_use_tts", "type_info": { "type": "Tiny", @@ -174,7 +184,7 @@ } }, { - "ordinal": 17, + "ordinal": 18, "name": "can_use_w2l", "type_info": { "type": "Tiny", @@ -184,7 +194,7 @@ } }, { - "ordinal": 18, + "ordinal": 19, "name": "can_delete_own_tts_results", "type_info": { "type": "Tiny", @@ -194,7 +204,7 @@ } }, { - "ordinal": 19, + "ordinal": 20, "name": "can_delete_own_w2l_results", "type_info": { "type": "Tiny", @@ -204,7 +214,7 @@ } }, { - "ordinal": 20, + "ordinal": 21, "name": "can_delete_own_account", "type_info": { "type": "Tiny", @@ -214,7 +224,7 @@ } }, { - "ordinal": 21, + "ordinal": 22, "name": "can_upload_tts_models", "type_info": { "type": "Tiny", @@ -224,7 +234,7 @@ } }, { - "ordinal": 22, + "ordinal": 23, "name": "can_upload_w2l_templates", "type_info": { "type": "Tiny", @@ -234,7 +244,7 @@ } }, { - "ordinal": 23, + "ordinal": 24, "name": "can_delete_own_tts_models", "type_info": { "type": "Tiny", @@ -244,7 +254,7 @@ } }, { - "ordinal": 24, + "ordinal": 25, "name": "can_delete_own_w2l_templates", "type_info": { "type": "Tiny", @@ -254,7 +264,7 @@ } }, { - "ordinal": 25, + "ordinal": 26, "name": "can_approve_w2l_templates", "type_info": { "type": "Tiny", @@ -264,7 +274,7 @@ } }, { - "ordinal": 26, + "ordinal": 27, "name": "can_edit_other_users_profiles", "type_info": { "type": "Tiny", @@ -274,7 +284,7 @@ } }, { - "ordinal": 27, + "ordinal": 28, "name": "can_edit_other_users_tts_models", "type_info": { "type": "Tiny", @@ -284,7 +294,7 @@ } }, { - "ordinal": 28, + "ordinal": 29, "name": "can_edit_other_users_w2l_templates", "type_info": { "type": "Tiny", @@ -294,7 +304,7 @@ } }, { - "ordinal": 29, + "ordinal": 30, "name": "can_delete_other_users_tts_models", "type_info": { "type": "Tiny", @@ -304,7 +314,7 @@ } }, { - "ordinal": 30, + "ordinal": 31, "name": "can_delete_other_users_tts_results", "type_info": { "type": "Tiny", @@ -314,7 +324,7 @@ } }, { - "ordinal": 31, + "ordinal": 32, "name": "can_delete_other_users_w2l_templates", "type_info": { "type": "Tiny", @@ -324,7 +334,7 @@ } }, { - "ordinal": 32, + "ordinal": 33, "name": "can_delete_other_users_w2l_results", "type_info": { "type": "Tiny", @@ -334,7 +344,7 @@ } }, { - "ordinal": 33, + "ordinal": 34, "name": "can_ban_users", "type_info": { "type": "Tiny", @@ -344,7 +354,7 @@ } }, { - "ordinal": 34, + "ordinal": 35, "name": "can_delete_users", "type_info": { "type": "Tiny", @@ -392,8 +402,9 @@ true, true, true, + true, true ] }, - "hash": "423109f2dcee23a558c43ecaab0eb2836714d5316620daaa1127b447cad59212" + "hash": "1c32bdb79ed2bc1b8988bbb6ea3f2a1db4ed4552d53cb4f31ff4b08de01691f7" } diff --git a/Cargo.lock b/Cargo.lock index 70f3cb9a12..d6f1270264 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3241,6 +3241,7 @@ dependencies = [ "tokens", "tokio", "tts_common", + "videos", "walkdir", ] diff --git a/_sql/actions/user_access/enroll_users_in_studio.sql b/_sql/actions/user_access/enroll_users_in_studio.sql deleted file mode 100644 index c066acf8f2..0000000000 --- a/_sql/actions/user_access/enroll_users_in_studio.sql +++ /dev/null @@ -1,36 +0,0 @@ --- noinspection SqlDialectInspectionForFile --- noinspection SqlNoDataSourceInspectionForFile --- noinspection SqlResolveForFile - --- Staff --- teddanson is victor's alt --- endtimes is @sugarbro (testing) --- el_cid_93 is for testing --- SELECT token, username FROM users WHERE username IN ( -UPDATE users SET can_access_studio = true WHERE username IN ( - 'bflat', - 'brandon', - 'crossproduct', - 'crossproduct1', - 'echelon', - 'el_cid_93', - 'endtimes', - 'fyscott', - 'gateway', - 'jags111', - 'mrvintage', - 'olivicmic', - 'rewin123', - 'saltacc', - 'teddanson', - 'vegito1089', - 'wilwong', - 'yae_ph', - 'zzz_last_item' -); - -UPDATE users SET can_access_studio = true WHERE username IN ( - 'endtimes', - 'tropicalfun', - 'zzz_last_item' -); diff --git a/_sql/actions/user_access/new_enroll.sql b/_sql/actions/user_access/new_enroll.sql new file mode 100644 index 0000000000..3c755913b7 --- /dev/null +++ b/_sql/actions/user_access/new_enroll.sql @@ -0,0 +1,62 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +-- Staff (1) +-- * teddanson is victor's alt +UPDATE users +SET + can_access_studio = false, + maybe_feature_flags = 'explore_media,studio,video_style_transfer' +WHERE username IN ( + 'bflat', + 'brandon', + 'crossproduct', + 'crossproduct1', + 'echelon', + 'olivicmic', + 'saltacc', + 'teddanson', + 'vegito1089', + 'wilwong', + 'zzz_last_item' +); + +-- Staff (2) +-- * el_cid_93 is for testing (who is this??) +-- * endtimes is @sugarbro (testing) +-- * tammieteller is Tammie (Pebblebed) +UPDATE users +SET + can_access_studio = false, + maybe_feature_flags = 'studio,video_style_transfer' +WHERE username IN ( + 'el_cid_93', + 'endtimes', + 'fyscott', + 'gateway', + 'jags111', + 'justinjohn0306', + 'mrvintage', + 'rewin123', + 'tammieteller', + 'tropicalfun', + 'yae_ph', + 'zzz_last_item' +); + +-- Early access to VST only +UPDATE users +SET + can_access_studio = false, + maybe_feature_flags = 'video_style_transfer' +WHERE username IN ( + 'kenjoplays', + 'ofccccccc', + 'sonicgt2', + 'stewiegroffin', + 'tanooki426', + 'wawoul', + 'zzz_last_item' +); + diff --git a/crates/lib/files/videos/src/get_mp4_info.rs b/crates/lib/files/videos/src/get_mp4_info.rs index 2a0327bf6c..11bd2f60fc 100644 --- a/crates/lib/files/videos/src/get_mp4_info.rs +++ b/crates/lib/files/videos/src/get_mp4_info.rs @@ -6,30 +6,47 @@ use mp4::TrackType; use errors::AnyhowResult; +#[derive(Debug, Clone)] pub struct Mp4Info { /// Framerate of the longest video track. pub framerate: f64, + /// Duration of the entire mp4. pub duration_millis: u128, + + /// Width of the longest video track. + pub width: u16, + + /// Height of the longest video track. + pub height: u16, } -pub fn get_mp4_info_for_bytes(file_bytes: &[u8], file_size: u64) -> AnyhowResult { +pub fn get_mp4_info_for_bytes(file_bytes: &[u8]) -> AnyhowResult { + get_mp4_info_for_bytes_and_len(file_bytes, file_bytes.len()) +} + +pub fn get_mp4_info_for_bytes_and_len(file_bytes: &[u8], file_size: usize) -> AnyhowResult { let reader = BufReader::new(Cursor::new(file_bytes)); - get_mp4_info(reader, file_size) + get_mp4_info(reader, file_size as u64) } pub fn get_mp4_info(reader: T, file_size: u64) -> AnyhowResult { let mp4 = mp4::Mp4Reader::read_header(reader, file_size)?; - let mut framerate = 0.0; let mut longest_duration = Duration::from_secs(0); + let mut framerate = 0.0; + let mut width = 0; + let mut height = 0; + for track in mp4.tracks().values() { match track.track_type() { Ok(TrackType::Video) => { if track.duration() > longest_duration { longest_duration = track.duration(); framerate = track.frame_rate(); + width = track.width(); + height = track.height(); } } Err(err) => { @@ -43,6 +60,8 @@ pub fn get_mp4_info(reader: T, file_size: u64) -> AnyhowResult, + pub can_access_studio: bool, // ===== ROLE ===== // @@ -121,6 +124,7 @@ SELECT users.is_banned, users.can_access_studio, + users.maybe_feature_flags, user_roles.can_use_tts, user_roles.can_use_w2l, @@ -183,6 +187,7 @@ WHERE user_sessions.token = ? is_banned: i8_to_bool(raw_user_record.is_banned), can_access_studio: i8_to_bool(raw_user_record.can_access_studio), + maybe_feature_flags: raw_user_record.maybe_feature_flags, // Usage can_use_tts: nullable_i8_to_bool_default_false(raw_user_record.can_use_tts), @@ -250,6 +255,8 @@ struct SessionUserRawDbRecord { // Feature / Rollout Flags can_access_studio: i8, + maybe_feature_flags: Option, + // NB: These are `Option` due to the JOIN not being compile-time assured. // Usage can_use_tts: Option, diff --git a/crates/service/job/inference_job/Cargo.toml b/crates/service/job/inference_job/Cargo.toml index f8cbc47307..bacb76e971 100644 --- a/crates/service/job/inference_job/Cargo.toml +++ b/crates/service/job/inference_job/Cargo.toml @@ -19,8 +19,6 @@ actix_helpers = { path = "../../../lib/actix_helpers" } bootstrap = { path = "../../../lib/bootstrap" } buckets = { path = "../../../_types/buckets" } cloud_storage = { path = "../../../lib/api_clients/cloud_storage" } -google_drive_common = { path = "../../../lib/api_clients/google_drive_common" } -thumbnail_generator = { path = "../../../lib/api_clients/thumbnail_generator"} collections = { path = "../../../lib/collections" } composite_identifiers = { path = "../../../_types/composite_identifiers" } concurrency = { path = "../../../lib/concurrency" } @@ -29,6 +27,7 @@ easyenv = { path = "../../../lib/easyenv" } enums = { path = "../../../_types/enums" } errors = { path = "../../../lib/errors" } filesys = { path = "../../../lib/files/filesys" } +google_drive_common = { path = "../../../lib/api_clients/google_drive_common" } hashing = { path = "../../../lib/files/hashing" } images = { path = "../../../lib/files/images" } jobs_common = { path = "../../../lib/jobs_common" } @@ -39,8 +38,10 @@ mimetypes = { path = "../../../lib/files/mimetypes" } mysql_queries = { path = "../../../lib/mysql_queries" } redis_common = { path = "../../../lib/redis_common" } subprocess_common = { path = "../../../lib/subprocess_common" } +thumbnail_generator = { path = "../../../lib/api_clients/thumbnail_generator"} tokens = { path = "../../../_types/tokens" } tts_common = { path = "../../../lib/tts_common" } +videos = { path = "../../../lib/files/videos" } # External anyhow = "1.0.75" diff --git a/crates/service/job/inference_job/src/job/job_types/mocap/mocap_net/process_job.rs b/crates/service/job/inference_job/src/job/job_types/mocap/mocap_net/process_job.rs index aa20750272..9fe398efef 100644 --- a/crates/service/job/inference_job/src/job/job_types/mocap/mocap_net/process_job.rs +++ b/crates/service/job/inference_job/src/job/job_types/mocap/mocap_net/process_job.rs @@ -1,4 +1,6 @@ -use std::fs::read_to_string; +use std::fs::{File, read_to_string}; +use std::io::{BufReader, Read}; +use std::path::PathBuf; use std::time::Instant; use anyhow::anyhow; @@ -14,6 +16,7 @@ use hashing::sha256::sha256_hash_file::sha256_hash_file; use mysql_queries::payloads::generic_inference_args::generic_inference_args::PolymorphicInferenceArgs::Mc; use mysql_queries::queries::generic_inference::job::list_available_generic_inference_jobs::AvailableInferenceJob; use mysql_queries::queries::media_files::create::insert_media_file_from_mocapnet::{insert_media_file_from_mocapnet, InsertArgs}; +use videos::get_mp4_info::{get_mp4_info, get_mp4_info_for_bytes, Mp4Info}; use crate::job::job_loop::job_success_result::{JobSuccessResult, ResultEntity}; use crate::job::job_loop::process_single_job_error::ProcessSingleJobError; @@ -53,6 +56,23 @@ pub async fn process_job(args: MocapNetProcessJobArgs<'_>) -> Result return Err(ProcessSingleJobError::Other(anyhow!("Mocap args not found"))), + Some(args) => args, + }; + + let mc_args = match poly_args { + Mc(args) => args, + _ => return Err(ProcessSingleJobError::Other(anyhow!("Mocap args not found"))), + }; + // ==================== TEMP DIR ==================== // let work_temp_dir = format!("temp_mocapnet_inference_{}", job.id.0); @@ -86,6 +106,22 @@ pub async fn process_job(args: MocapNetProcessJobArgs<'_>) -> Result) -> Result return Err(ProcessSingleJobError::Other(anyhow!("Mocap args not found"))), - Some(args) => args, - }; - - let mc_args = match poly_args { - Mc(args) => args, - _ => return Err(ProcessSingleJobError::Other(anyhow!("Mocap args not found"))), - }; - // ==================== RUN INFERENCE SCRIPT ==================== // let stderr_output_file = work_temp_dir.path().join("stderr.txt"); let inference_start_time = Instant::now(); @@ -130,8 +151,8 @@ pub async fn process_job(args: MocapNetProcessJobArgs<'_>) -> Result) -> Result Option { + let video_file = File::open(video_filename).ok()?; + let file_length = video_file.metadata().ok()?.len(); + let reader = BufReader::new(video_file); + let info = get_mp4_info(reader, file_length).ok()?; + Some(info) +} \ No newline at end of file diff --git a/crates/service/plugins/users/src/endpoints/session_info_handler.rs b/crates/service/plugins/users/src/endpoints/session_info_handler.rs index 0ee0a5dc49..498705cae4 100644 --- a/crates/service/plugins/users/src/endpoints/session_info_handler.rs +++ b/crates/service/plugins/users/src/endpoints/session_info_handler.rs @@ -3,6 +3,7 @@ #![forbid(unused_mut)] #![forbid(unused_variables)] +use std::collections::HashSet; use std::fmt; use actix_web::{HttpRequest, HttpResponse, web}; @@ -12,11 +13,13 @@ use log::warn; use sqlx::MySqlPool; use utoipa::ToSchema; +use enums::by_table::users::user_feature_flag::UserFeatureFlag; use http_server_common::response::response_error_helpers::to_simple_json_error; use crate::common_responses::user_details_lite::UserDetailsLight; use crate::cookies::anonymous_visitor_tracking::avt_cookie_manager::AvtCookieManager; use crate::utils::session_checker::SessionChecker; +use crate::utils::user_session_feature_flags::UserSessionFeatureFlags; #[derive(Serialize, Copy, Clone, ToSchema)] #[serde(rename_all = "snake_case")] @@ -47,6 +50,7 @@ pub struct SessionUserInfo { // Feature / rollout flags: pub can_access_studio: bool, + pub maybe_feature_flags: HashSet, // Premium plans: pub fakeyou_plan: FakeYouPlan, @@ -149,6 +153,9 @@ pub async fn session_info_handler( match maybe_user_session { None => {} Some(session_data) => { + let feature_flags = + UserSessionFeatureFlags::new(session_data.maybe_feature_flags.as_deref()); + if !session_data.is_banned { // NB: Banned users can't be logged in logged_in = true; @@ -166,6 +173,7 @@ pub async fn session_info_handler( // Rollout / feature flags: can_access_studio: session_data.can_access_studio, + maybe_feature_flags: feature_flags.get_flags(), // Premium plans: fakeyou_plan: FakeYouPlan::Free, diff --git a/crates/service/plugins/users/src/utils/mod.rs b/crates/service/plugins/users/src/utils/mod.rs index d49f148287..c22814e9d2 100644 --- a/crates/service/plugins/users/src/utils/mod.rs +++ b/crates/service/plugins/users/src/utils/mod.rs @@ -1,3 +1,4 @@ pub mod email_to_gravatar; pub mod session_checker; -pub mod user_session_extended; \ No newline at end of file +pub mod user_session_extended; +pub mod user_session_feature_flags; \ No newline at end of file diff --git a/crates/service/plugins/users/src/utils/session_checker.rs b/crates/service/plugins/users/src/utils/session_checker.rs index b9bcab82b7..d5ece432e1 100644 --- a/crates/service/plugins/users/src/utils/session_checker.rs +++ b/crates/service/plugins/users/src/utils/session_checker.rs @@ -17,6 +17,7 @@ use redis_common::redis_cache_keys::RedisCacheKeys; use crate::cookies::session::session_cookie_manager::SessionCookieManager; use crate::utils::user_session_extended::{UserSessionExtended, UserSessionPreferences, UserSessionPremiumPlanInfo, UserSessionRoleAndPermissions, UserSessionSubscriptionPlan, UserSessionUserDetails}; +use crate::utils::user_session_feature_flags::UserSessionFeatureFlags; #[derive(Clone)] pub struct SessionChecker { @@ -168,7 +169,6 @@ impl SessionChecker { } - // ==================== UserSessionExtended ==================== //#[deprecated = "Use the PoolConnection method instead of the MySqlPool one."] @@ -258,6 +258,9 @@ impl SessionChecker { can_ban_users: user_session.can_ban_users, can_delete_users: user_session.can_delete_users, }, + feature_flags: UserSessionFeatureFlags { + maybe_feature_flags: user_session.maybe_feature_flags, + } })) } diff --git a/crates/service/plugins/users/src/utils/user_session_extended.rs b/crates/service/plugins/users/src/utils/user_session_extended.rs index fe8f2bb2f6..00e655e25a 100644 --- a/crates/service/plugins/users/src/utils/user_session_extended.rs +++ b/crates/service/plugins/users/src/utils/user_session_extended.rs @@ -2,6 +2,8 @@ use chrono::{DateTime, Utc}; use enums::common::visibility::Visibility; +use crate::utils::user_session_feature_flags::UserSessionFeatureFlags; + #[derive(Clone, Default)] pub struct UserSessionExtended { pub user_token: String, @@ -9,6 +11,7 @@ pub struct UserSessionExtended { pub premium: UserSessionPremiumPlanInfo, pub preferences: UserSessionPreferences, pub role: UserSessionRoleAndPermissions, + pub feature_flags: UserSessionFeatureFlags, } #[derive(Clone, Default)] @@ -96,3 +99,4 @@ pub struct UserSessionRoleAndPermissions { pub can_ban_users: bool, pub can_delete_users: bool, } + diff --git a/crates/service/plugins/users/src/utils/user_session_feature_flags.rs b/crates/service/plugins/users/src/utils/user_session_feature_flags.rs new file mode 100644 index 0000000000..8ac9ade6a2 --- /dev/null +++ b/crates/service/plugins/users/src/utils/user_session_feature_flags.rs @@ -0,0 +1,117 @@ + +use std::collections::HashSet; +use enums::by_table::users::user_feature_flag::UserFeatureFlag; + +#[derive(Default, Clone)] +pub struct UserSessionFeatureFlags { + // Optional comma-separated list of parseable `UserFeatureFlag` enum features + pub maybe_feature_flags: Option, +} + +impl UserSessionFeatureFlags { + + pub fn new(maybe_feature_flags: Option<&str>) -> Self { + Self { + maybe_feature_flags: maybe_feature_flags.map(|s| s.to_string()), + } + } + + pub fn get_flags(&self) -> HashSet { + match self.maybe_feature_flags.as_deref() { + None => HashSet::new(), + Some(feature_flags) => { + feature_flags + .split(",") + .map(|flag| flag.trim()) + .filter_map(|flag| UserFeatureFlag::from_str(flag).ok()) + .collect() + } + } + } + + // TODO(bt,2024-03-05): Caching + pub fn has_permission_unoptimized(&self, permission: UserFeatureFlag) -> bool { + self.get_flags().contains(&permission) + } +} + +#[cfg(test)] +mod tests { + use enums::by_table::users::user_feature_flag::UserFeatureFlag; + use crate::utils::user_session_feature_flags::UserSessionFeatureFlags; + + #[test] + fn test_no_flags() { + let flags = UserSessionFeatureFlags::default(); + assert_eq!(flags.get_flags().len(), 0); + + for flag in UserFeatureFlag::all_variants() { + assert_eq!(flags.has_permission_unoptimized(flag), false); + } + + // TODO(bt,2024-03-05): Expose strum to callers in test packages. + //for flag in UserFeatureFlag::iter() { + // assert_eq!(flags.has_permission_unoptimized(flag), false); + //} + } + + #[test] + fn test_single_feature() { + let flags = UserSessionFeatureFlags { + maybe_feature_flags: Some("studio".to_string()), + }; + + assert_eq!(flags.get_flags().len(), 1); + assert_eq!(flags.has_permission_unoptimized(UserFeatureFlag::Studio), true); + assert_eq!(flags.has_permission_unoptimized(UserFeatureFlag::VideoStyleTransfer), false); + assert_eq!(flags.has_permission_unoptimized(UserFeatureFlag::ExploreMedia), false); + } + + #[test] + fn test_all_features() { + let flags = UserSessionFeatureFlags { + maybe_feature_flags: Some("studio,video_style_transfer,explore_media".to_string()), + }; + + assert_eq!(flags.get_flags().len(), 3); + assert_eq!(flags.has_permission_unoptimized(UserFeatureFlag::Studio), true); + assert_eq!(flags.has_permission_unoptimized(UserFeatureFlag::VideoStyleTransfer), true); + assert_eq!(flags.has_permission_unoptimized(UserFeatureFlag::ExploreMedia), true); + } + + #[test] + fn test_duplication() { + let flags = UserSessionFeatureFlags { + maybe_feature_flags: Some("studio,studio,studio,studio".to_string()), + }; + + assert_eq!(flags.get_flags().len(), 1); + assert_eq!(flags.has_permission_unoptimized(UserFeatureFlag::Studio), true); + assert_eq!(flags.has_permission_unoptimized(UserFeatureFlag::VideoStyleTransfer), false); + assert_eq!(flags.has_permission_unoptimized(UserFeatureFlag::ExploreMedia), false); + } + + #[test] + fn test_spacing() { + let flags = UserSessionFeatureFlags { + maybe_feature_flags: Some(" studio, video_style_transfer , , , ".to_string()), + }; + + assert_eq!(flags.get_flags().len(), 2); + assert_eq!(flags.has_permission_unoptimized(UserFeatureFlag::Studio), true); + assert_eq!(flags.has_permission_unoptimized(UserFeatureFlag::VideoStyleTransfer), true); + assert_eq!(flags.has_permission_unoptimized(UserFeatureFlag::ExploreMedia), false); + } + + #[test] + fn test_invalid_features_and_typos() { + let flags = UserSessionFeatureFlags { + maybe_feature_flags: Some("invalid,,typo,stdo,STUDIO".to_string()), + }; + + assert_eq!(flags.get_flags().len(), 0); + assert_eq!(flags.has_permission_unoptimized(UserFeatureFlag::Studio), false); + assert_eq!(flags.has_permission_unoptimized(UserFeatureFlag::VideoStyleTransfer), false); + assert_eq!(flags.has_permission_unoptimized(UserFeatureFlag::ExploreMedia), false); + } +} \ No newline at end of file diff --git a/crates/service/web/storyteller_web/src/api_doc.rs b/crates/service/web/storyteller_web/src/api_doc.rs index cecaf6c8cd..e476f35eeb 100644 --- a/crates/service/web/storyteller_web/src/api_doc.rs +++ b/crates/service/web/storyteller_web/src/api_doc.rs @@ -44,6 +44,7 @@ use crate::http_server::endpoints::media_files::upload::upload_media_file_handle use crate::http_server::endpoints::media_files::upload_video::upload_error::VideoMediaFileUploadError; use crate::http_server::endpoints::media_files::upload_video::upload_video_media_file_handler::*; use crate::http_server::endpoints::prompts::get_prompt::*; +use crate::http_server::endpoints::service::status_alert_handler::*; use crate::http_server::endpoints::user_bookmarks::batch_get_user_bookmarks_handler::*; use crate::http_server::endpoints::user_bookmarks::create_user_bookmark_handler::*; use crate::http_server::endpoints::user_bookmarks::delete_user_bookmark_handler::*; @@ -79,6 +80,7 @@ use crate::http_server::web_utils::response_success_helpers::*; crate::http_server::endpoints::media_files::upload::upload_media_file_handler::upload_media_file_handler, crate::http_server::endpoints::media_files::upload_video::upload_video_media_file_handler::upload_video_media_file_handler, crate::http_server::endpoints::prompts::get_prompt::get_prompt_handler, + crate::http_server::endpoints::service::status_alert_handler::status_alert_handler, crate::http_server::endpoints::user_bookmarks::batch_get_user_bookmarks_handler::batch_get_user_bookmarks_handler, crate::http_server::endpoints::user_bookmarks::create_user_bookmark_handler::create_user_bookmark_handler, crate::http_server::endpoints::user_bookmarks::delete_user_bookmark_handler::delete_user_bookmark_handler, @@ -233,10 +235,12 @@ use crate::http_server::web_utils::response_success_helpers::*; SetUserRatingError, SetUserRatingRequest, SetUserRatingResponse, + StatusAlertCategory, + StatusAlertError, + StatusAlertInfo, + StatusAlertResponse, StatusDetailsResponse, StorytellerStreamPlan, - VideoMediaFileUploadError, - UploadVideoMediaSuccessResponse, TerminateInferenceJobError, TerminateInferenceJobPathInfo, TerminateInferenceJobSuccessResponse, @@ -244,6 +248,7 @@ use crate::http_server::web_utils::response_success_helpers::*; UpdateWeightPathInfo, UpdateWeightRequest, UploadMediaSuccessResponse, + UploadVideoMediaSuccessResponse, UserBookmarkDetailsForUserList, UserBookmarkEntityType, UserBookmarkForEntityListItem, @@ -253,6 +258,7 @@ use crate::http_server::web_utils::response_success_helpers::*; UserProfileUserBadge, UserRatingEntityType, UserRatingValue, + VideoMediaFileUploadError, Weight, WeightsData, ZsDatasetRecord, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/list/list_media_files.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/list/list_media_files.rs index 833b7d26ae..ccd87d5a3d 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/list/list_media_files.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/list/list_media_files.rs @@ -24,6 +24,7 @@ use crate::http_server::common_responses::pagination_cursors::PaginationCursors; use crate::http_server::common_responses::simple_entity_stats::SimpleEntityStats; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::server_state::ServerState; +use crate::util::allowed_explore_media_access::allowed_explore_media_access; use crate::util::allowed_studio_access::allowed_studio_access; #[derive(Deserialize, ToSchema, IntoParams)] @@ -136,8 +137,8 @@ pub async fn list_media_files_handler( // ==================== FEATURE FLAG CHECK ==================== // - if !allowed_studio_access(maybe_user_session.as_ref(), &server_state.flags) { - warn!("Storyteller Studio access is not permitted for user"); + if !allowed_explore_media_access(maybe_user_session.as_ref()) { + warn!("Explore media access is not permitted for user"); return Err(ListMediaFilesError::NotAuthorized); } diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload_video/upload_video_media_file_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload_video/upload_video_media_file_handler.rs index 3a01c23f09..6eac332b77 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload_video/upload_video_media_file_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload_video/upload_video_media_file_handler.rs @@ -18,7 +18,7 @@ use mimetypes::mimetype_to_extension::mimetype_to_extension; use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token; use mysql_queries::queries::media_files::create::insert_media_file_from_file_upload::{insert_media_file_from_file_upload, InsertMediaFileFromUploadArgs, UploadType}; use tokens::tokens::media_files::MediaFileToken; -use videos::get_mp4_info::{get_mp4_info, get_mp4_info_for_bytes}; +use videos::get_mp4_info::{get_mp4_info, get_mp4_info_for_bytes, get_mp4_info_for_bytes_and_len}; use crate::http_server::endpoints::engine::create_scene_handler::{CreateSceneError, CreateSceneSuccessResponse}; use crate::http_server::endpoints::media_files::upload_video::drain_multipart_request::drain_multipart_request; @@ -176,7 +176,7 @@ pub async fn upload_video_media_file_handler( // ==================== FRAME RATE ==================== // - let mp4_info = get_mp4_info_for_bytes(file_bytes.as_ref(), file_size_bytes as u64) + let mp4_info = get_mp4_info_for_bytes(file_bytes.as_ref()) .map_err(|err| { warn!("Error reading mp4 info: {:?}", err); VideoMediaFileUploadError::ServerError diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/service/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/service/mod.rs index a8069876cf..c53223a497 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/service/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/service/mod.rs @@ -1,2 +1,3 @@ pub mod health_check_handler; -pub mod public_info_handler; \ No newline at end of file +pub mod public_info_handler; +pub mod status_alert_handler; \ No newline at end of file diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/service/status_alert_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/service/status_alert_handler.rs new file mode 100644 index 0000000000..e0bf03036f --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/service/status_alert_handler.rs @@ -0,0 +1,127 @@ +use std::sync::Arc; + +use actix_web::{HttpRequest, HttpResponse, ResponseError, web}; +use actix_web::http::StatusCode; +use utoipa::ToSchema; + +use http_server_common::response::serialize_as_json_error::serialize_as_json_error; + +use crate::server_state::ServerState; + +#[derive(Serialize, ToSchema)] +pub struct StatusAlertResponse { + pub success: bool, + + /// If there's an alert, this will be set. + /// The sub keys are optional, but at least one of them will be set. + /// i.e. we can have an alert with no message or no predefined category. + pub maybe_alert: Option, +} + +#[derive(Serialize, ToSchema)] +pub struct StatusAlertInfo { + /// If an alert is set, this might be a key for a common i18n message to show. + pub maybe_category: Option, + + /// We can set an optional message to show to the frontend. + /// + /// This field may or may not be set. If it's set, this is custom text that should + /// be displayed to the user. For example, we might tell the user when we'll be + /// back online, etc. Both this field and `maybe_category` are optional. + /// + /// If `maybe_category` is present, it should be a key for a predefined and + /// internationalized (i18n) message stored on the frontend. + /// + /// Either or both of these fields may be set. + pub maybe_message: Option, +} + +#[derive(Serialize, ToSchema, Copy, Clone)] +#[serde(rename_all = "snake_case")] +pub enum StatusAlertCategory { + /// System is down for maintenance + DownForMaintenance, +} + +#[derive(Debug, Serialize, ToSchema)] +pub enum StatusAlertError { + ServerError, +} + +impl ResponseError for StatusAlertError { + fn status_code(&self) -> StatusCode { + match *self { + StatusAlertError::ServerError => StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn error_response(&self) -> HttpResponse { + serialize_as_json_error(self) + } +} + +// NB: Not using derive_more::Display since Clion doesn't understand it. +impl std::fmt::Display for StatusAlertError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +#[utoipa::path( + get, + path = "/v1/status_alert_check", + responses( + ( + status = 200, + description = "Check service status for frontend alert messages", + body = StatusAlertResponse, + ), + (status = 500, description = "Server error", body = StatusAlertError) + ), +)] +pub async fn status_alert_handler( + _http_request: HttpRequest, + server_state: web::Data> +) -> Result { + + let maybe_category = server_state + .flags + .maybe_status_alert_category + .as_deref() + .map(|category| category_to_enum(&category)) + .flatten(); + + let maybe_message = server_state + .flags + .maybe_status_alert_custom_message + .as_deref() + .map(|message| message.trim().to_string()); + + let maybe_alert = match (maybe_category, maybe_message) { + (None, None) => None, + (category, message) => Some(StatusAlertInfo { + maybe_category: category, + maybe_message: message, + }), + }; + + let response = StatusAlertResponse { + success: true, + maybe_alert, + }; + + let body = serde_json::to_string(&response) + .map_err(|e| StatusAlertError::ServerError)?; + + Ok(HttpResponse::Ok() + .content_type("application/json") + .body(body)) +} + +fn category_to_enum(category: &str) -> Option { + let key = category.trim(); + match key { + "down_for_maintenance" => Some(StatusAlertCategory::DownForMaintenance), + _ => None, + } +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/tts/enqueue_infer_tts_handler/enqueue_infer_tts_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/enqueue_infer_tts_handler/enqueue_infer_tts_handler.rs index d287528d28..3f7a00cfac 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/tts/enqueue_infer_tts_handler/enqueue_infer_tts_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/enqueue_infer_tts_handler/enqueue_infer_tts_handler.rs @@ -24,9 +24,7 @@ use http_server_common::request::get_request_ip::get_request_ip; use migration::text_to_speech::get_tts_model_for_enqueue_inference_migration::TtsModelForEnqueueInferenceMigrationWrapper; use mysql_queries::payloads::generic_inference_args::generic_inference_args::{GenericInferenceArgs, InferenceCategoryAbbreviated}; use mysql_queries::queries::generic_inference::web::insert_generic_inference_job::{insert_generic_inference_job, InsertGenericInferenceArgs}; -use mysql_queries::queries::tts::tts_inference_jobs::insert_tts_inference_job::TtsInferenceJobInsertBuilder; use redis_common::redis_keys::RedisKeys; -use tokens::tokens::tts_inference_jobs::TtsInferenceJobToken; use tokens::tokens::users::UserToken; use tts_common::priority::{FAKEYOU_DEFAULT_VALID_API_TOKEN_PRIORITY_LEVEL, FAKEYOU_INVESTOR_PRIORITY_LEVEL}; use user_input_common::check_for_slurs::contains_slurs; @@ -55,10 +53,6 @@ const STORYTELLER_DEMO_COOKIE_NAME : &str = "storyteller_demo"; /// const DEBUG_HEADER_NAME : &str = "enable-debug-mode"; -/// Requests with this header can be forced onto the new jobs system. -/// It'll be used to help test TT2 on the new system before it is rolled out for everyone. -const NEW_JOB_SYSTEM_HEADER_NAME : &str = "new-job-system"; - /// The routing tag header can send workloads to particular k8s hosts. /// This is useful for catching the live logs or intercepting the job. /// NB: This is only for the new job system. @@ -234,9 +228,6 @@ pub async fn enqueue_infer_tts_handler( let is_debug_request = get_request_header_optional(&http_request, DEBUG_HEADER_NAME) .is_some(); - let use_new_job_system_for_request = get_request_header_optional(&http_request, NEW_JOB_SYSTEM_HEADER_NAME) - .is_some(); - let maybe_routing_tag= get_request_header_optional(&http_request, ROUTING_TAG_HEADER_NAME) .map(|routing_tag| routing_tag.trim().to_string()); @@ -377,95 +368,55 @@ pub async fn enqueue_infer_tts_handler( // ==================== ENQUEUE APPROPRIATE TTS TYPE ==================== // - let use_new_job_system = use_new_job_system_for_request || server_state.flags.enable_enqueue_generic_tts_job; - - let job_token; - let job_token_type; - let model_type = InferenceModelType::Tacotron2; let max_duration_seconds = plan.tts_max_duration_seconds(); - if use_new_job_system { - // This branch uses the `generic-inference-job` service and tables. - info!("Creating tts inference job record (new generic job system)..."); + // This branch uses the `generic-inference-job` service and tables. + info!("Creating tts inference job record (new generic job system)..."); - let maybe_creator_user_token_typed = maybe_user_token - .as_deref() - .map(|token| UserToken::new_from_str(token)); + let maybe_creator_user_token_typed = maybe_user_token + .as_deref() + .map(|token| UserToken::new_from_str(token)); - let maybe_avt_token = server_state.avt_cookie_manager.get_avt_token_from_request(&http_request); + let maybe_avt_token = server_state.avt_cookie_manager.get_avt_token_from_request(&http_request); - let query_result = insert_generic_inference_job(InsertGenericInferenceArgs { - uuid_idempotency_token: &request.uuid_idempotency_token, - job_type: InferenceJobType::Tacotron2, - inference_category: InferenceCategory::TextToSpeech, - maybe_model_type: Some(model_type), - maybe_model_token: Some(request.tts_model_token.as_str()), - maybe_input_source_token: None, // NB: TTS doesn't have input media - maybe_input_source_token_type: None, // NB: TTS doesn't have input media - maybe_download_url: None, - maybe_cover_image_media_file_token: None, - maybe_raw_inference_text: Some(inference_text.as_str()), - maybe_max_duration_seconds: Some(max_duration_seconds), - maybe_inference_args: Some(GenericInferenceArgs { - inference_category: Some(InferenceCategoryAbbreviated::TextToSpeech), - args: None, // NB: We don't need to encode any args yet. - }), - maybe_creator_user_token: maybe_creator_user_token_typed.as_ref(), - maybe_avt_token: maybe_avt_token.as_ref(), - creator_ip_address: &ip_address, - creator_set_visibility: set_visibility, - priority_level, - requires_keepalive: false, // TODO: We may want this to be the case in the future. - is_debug_request, - maybe_routing_tag: maybe_routing_tag.as_deref(), - mysql_pool: &server_state.mysql_pool, - }).await; + let query_result = insert_generic_inference_job(InsertGenericInferenceArgs { + uuid_idempotency_token: &request.uuid_idempotency_token, + job_type: InferenceJobType::Tacotron2, + inference_category: InferenceCategory::TextToSpeech, + maybe_model_type: Some(model_type), + maybe_model_token: Some(request.tts_model_token.as_str()), + maybe_input_source_token: None, // NB: TTS doesn't have input media + maybe_input_source_token_type: None, // NB: TTS doesn't have input media + maybe_download_url: None, + maybe_cover_image_media_file_token: None, + maybe_raw_inference_text: Some(inference_text.as_str()), + maybe_max_duration_seconds: Some(max_duration_seconds), + maybe_inference_args: Some(GenericInferenceArgs { + inference_category: Some(InferenceCategoryAbbreviated::TextToSpeech), + args: None, // NB: We don't need to encode any args yet. + }), + maybe_creator_user_token: maybe_creator_user_token_typed.as_ref(), + maybe_avt_token: maybe_avt_token.as_ref(), + creator_ip_address: &ip_address, + creator_set_visibility: set_visibility, + priority_level, + requires_keepalive: false, // TODO: We may want this to be the case in the future. + is_debug_request, + maybe_routing_tag: maybe_routing_tag.as_deref(), + mysql_pool: &server_state.mysql_pool, + }).await; - let inference_job_token = match query_result { - Ok((inference_job_token, _id)) => inference_job_token, - Err(err) => { - warn!("New (generic) tts inference job creation DB error: {:?}", err); - return Err(InferTtsError::ServerError); - } - }; - - job_token = inference_job_token.to_string(); - job_token_type = InferenceJobTokenType::Generic; - - } else { - // This branch uses the `tts-inference-job` service and tables. - info!("Creating tts inference job record (legacy job system)..."); - - // This token is returned to the client. - job_token = TtsInferenceJobToken::generate().to_string(); - - let query_result = TtsInferenceJobInsertBuilder::new_for_fakeyou_request() - .set_job_token(&job_token) - .set_uuid_idempotency_token(&request.uuid_idempotency_token) - .set_model_token(&request.tts_model_token) - .set_raw_inference_text(&inference_text) - .set_maybe_creator_user_token(maybe_user_token.as_deref()) - .set_creator_ip_address(&ip_address) - .set_creator_set_visibility(set_visibility.to_str()) - .set_priority_level(priority_level) - .set_max_duration_seconds(max_duration_seconds) - .set_is_from_api(is_from_api) - .set_is_debug_request(is_debug_request) - .insert(&server_state.mysql_pool) - .await; - - match query_result { - Ok(_) => {}, - Err(err) => { - warn!("New tts inference job creation DB error: {:?}", err); - return Err(InferTtsError::ServerError); - } + let inference_job_token = match query_result { + Ok((inference_job_token, _id)) => inference_job_token, + Err(err) => { + warn!("New (generic) tts inference job creation DB error: {:?}", err); + return Err(InferTtsError::ServerError); } + }; - job_token_type = InferenceJobTokenType::LegacyTts; - } + let job_token = inference_job_token.to_string(); server_state.firehose_publisher.enqueue_tts_inference(maybe_user_token.as_deref(), &job_token, &request.tts_model_token) .await @@ -476,8 +427,8 @@ pub async fn enqueue_infer_tts_handler( let response = InferTtsSuccessResponse { success: true, - inference_job_token: job_token.to_string(), - inference_job_token_type: job_token_type, + inference_job_token: job_token, + inference_job_token_type: InferenceJobTokenType::Generic, }; let body = serde_json::to_string(&response) diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/voice_conversion/models/list_voice_conversion_models.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_conversion/models/list_voice_conversion_models.rs index 68b206f143..aeaf7cd6f9 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/voice_conversion/models/list_voice_conversion_models.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_conversion/models/list_voice_conversion_models.rs @@ -134,7 +134,7 @@ pub async fn list_voice_conversion_models_handler( })?; // TODO: Fail open in case the DB is down. Pull from expired cache if query fails. - let models = get_all_models(&mut mysql_connection, server_state.flags.switch_voice_conversion_to_model_weights) + let models = get_all_models(&mut mysql_connection, true) .await .map_err(|e| { error!("Error querying database: {:?}", e); diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/workflows/enqueue_comfy_ui.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/workflows/enqueue_comfy_ui.rs index 3457b9cf63..3eb1bf8099 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/workflows/enqueue_comfy_ui.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/workflows/enqueue_comfy_ui.rs @@ -27,10 +27,10 @@ use tokens::tokens::model_weights::ModelWeightToken; use tokens::tokens::users::UserToken; use crate::configs::plans::get_correct_plan_for_session::get_correct_plan_for_session; -use crate::configs::plans::plan_category::{PlanCategory}; +use crate::configs::plans::plan_category::PlanCategory; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::server_state::ServerState; -use crate::util::allowed_studio_access::allowed_studio_access; +use crate::util::allowed_video_style_transfer_access::allowed_video_style_transfer_access; use crate::validations::validate_idempotency_token_format::validate_idempotency_token_format; /// Debug requests can get routed to special "debug-only" workers, which can @@ -144,8 +144,8 @@ pub async fn enqueue_comfy_ui_handler( // ==================== FEATURE FLAG CHECK ==================== // - if !allowed_studio_access(maybe_user_session.as_ref(), &server_state.flags) { - warn!("Storyteller Studio access is not permitted for user"); + if !allowed_video_style_transfer_access(maybe_user_session.as_ref(), &server_state.flags) { + warn!("Video style transfer access is not permitted for user"); return Err(EnqueueComfyError::NotAuthorized); } diff --git a/crates/service/web/storyteller_web/src/main.rs b/crates/service/web/storyteller_web/src/main.rs index 88597e2f3e..6940c36c94 100644 --- a/crates/service/web/storyteller_web/src/main.rs +++ b/crates/service/web/storyteller_web/src/main.rs @@ -340,7 +340,11 @@ async fn main() -> AnyhowResult<()> { .ok_or(anyhow!("invalid server environment"))?; let service_feature_flags = StaticFeatureFlags { - // Permanent (control plane / safety) flags + // Permanent (control plane / safety) flags : messaging + maybe_status_alert_category: easyenv::get_env_string_optional("FF_STATUS_ALERT_CATEGORY"), + maybe_status_alert_custom_message: easyenv::get_env_string_optional("FF_STATUS_ALERT_CUSTOM_MESSAGE"), + + // Permanent (control plane / safety) flags : disabling features global_429_pushback_filter_enabled: easyenv::get_env_bool_or_default("FF_GLOBAL_429_PUSHBACK_FILTER_ENABLED", false), disable_unified_queue_stats_endpoint: easyenv::get_env_bool_or_default("FF_DISABLE_QUEUE_STATS_ENDPOINT", false), disable_inference_queue_length_endpoint: easyenv::get_env_bool_or_default("FF_DISABLE_INFERENCE_QUEUE_LENGTH_ENDPOINT", false), @@ -357,10 +361,9 @@ async fn main() -> AnyhowResult<()> { troll_ban_user_percent: easyenv::get_env_num("FF_TROLL_BANNED_USER_PERCENT", 0)?, // Temporary flags - enable_enqueue_generic_tts_job: easyenv::get_env_bool_or_default("FF_ENABLE_ENQUEUE_GENERIC_TTS_JOB", false), - switch_voice_conversion_to_model_weights: easyenv::get_env_bool_or_default("FF_SWITCH_VOICE_CONVERSION_TO_MODEL_WEIGHTS", false), switch_tts_to_model_weights: easyenv::get_env_bool_or_default("FF_SWITCH_TTS_TO_MODEL_WEIGHTS", false), force_session_studio_flags: easyenv::get_env_bool_or_default("FF_FORCE_SESSION_STUDIO_FLAG", false), + force_session_video_style_transfer_flags: easyenv::get_env_bool_or_default("FF_FORCE_SESSION_VST_FLAG", false), }; let third_party_url_redirector = ThirdPartyUrlRedirector::new(server_environment); diff --git a/crates/service/web/storyteller_web/src/routes.rs b/crates/service/web/storyteller_web/src/routes.rs index d2d4965f9e..8406e3fb81 100644 --- a/crates/service/web/storyteller_web/src/routes.rs +++ b/crates/service/web/storyteller_web/src/routes.rs @@ -85,6 +85,7 @@ use crate::http_server::endpoints::moderation::users::list_users::list_users_han use crate::http_server::endpoints::prompts::get_prompt::get_prompt_handler; use crate::http_server::endpoints::service::health_check_handler::get_health_check_handler; use crate::http_server::endpoints::service::public_info_handler::get_public_info_handler; +use crate::http_server::endpoints::service::status_alert_handler::status_alert_handler; use crate::http_server::endpoints::stats::get_unified_queue_stats::get_unified_queue_stats_handler; use crate::http_server::endpoints::stubs::app_model_downloads::get_app_model_downloads_handler; use crate::http_server::endpoints::stubs::app_news::get_app_news_handler; @@ -270,6 +271,7 @@ pub fn add_routes (app: App, server_environment: ServerEnvironment) -> .into_app(); // =================== BVH from Workflow ==================== + let mut app = RouteBuilder::from_app(app) .add_post("/v1/conversion/enqueue_bvh_to_workflow", enqueue_bvh_to_workflow_handler) .into_app(); @@ -280,6 +282,12 @@ pub fn add_routes (app: App, server_environment: ServerEnvironment) -> .add_get("/v1/stats/queues", get_unified_queue_stats_handler) .into_app(); + // ==================== Service Status ==================== + + let mut app = RouteBuilder::from_app(app) + .add_get("/v1/status_alert_check", status_alert_handler) + .into_app(); + // ==================== COMPONENTS ==================== app = add_suggested_api_v1_account_creation_and_session_routes(app); // /create_account, /session, /login, /logout diff --git a/crates/service/web/storyteller_web/src/server_state.rs b/crates/service/web/storyteller_web/src/server_state.rs index d4f7ed9249..b7789edb6f 100644 --- a/crates/service/web/storyteller_web/src/server_state.rs +++ b/crates/service/web/storyteller_web/src/server_state.rs @@ -223,6 +223,12 @@ pub struct StaticFeatureFlags { /// Used to bring the service back online slowly. pub global_429_pushback_filter_enabled: bool, + /// If we're suffering an outage, set the alert category to display a predefined message to users. + pub maybe_status_alert_category: Option, + + /// If we're suffering an outage, set custom text for the alert message. + pub maybe_status_alert_custom_message: Option, + /// Disable the live `/v1/stats/queues` endpoint for all users and serve a static value instead. pub disable_unified_queue_stats_endpoint: bool, @@ -254,16 +260,6 @@ pub struct StaticFeatureFlags { /// This should be a number over 100. pub troll_ban_user_percent: u8, - // TODO(2023-03-20): Remove temporary flag when done. - // NB(bt,2023-12-18): This is rolled out (=="TRUE") - /// TEMPORARY: Control enqueuing TTS jobs to the generic job worker. - pub enable_enqueue_generic_tts_job: bool, - - // TODO(2023-12-18): Remove temporary flag when done. - /// TEMPORARY: Move voice control model listing over to `model_weights` from `voice_conversion_models` - /// This will control all downstream enqueuing, jobs, etc. - pub switch_voice_conversion_to_model_weights: bool, - // TODO(2024-01-13): Remove temporary flag when done. /// TEMPORARY: Move voice control model listing over to `model_weights` from `tts_models` /// This will control all downstream enqueuing, jobs, etc. @@ -273,6 +269,11 @@ pub struct StaticFeatureFlags { /// TEMPORARY: Require users to have a studio flag on their `users` table record to use studio features. /// This will also prevent anonymous users from using the service APIs marked as studio-only. pub force_session_studio_flags: bool, + + // TODO(2024-03-05): Remove when done. + /// TEMPORARY: Require users to have a flag on their `users` table record to use video style transfer. + /// This will also prevent anonymous users from using the service APIs marked as VST-only. + pub force_session_video_style_transfer_flags: bool, } /// Instead of top level service denial, these are bans against entities that instead return diff --git a/crates/service/web/storyteller_web/src/util/allowed_explore_media_access.rs b/crates/service/web/storyteller_web/src/util/allowed_explore_media_access.rs new file mode 100644 index 0000000000..b9d801edc4 --- /dev/null +++ b/crates/service/web/storyteller_web/src/util/allowed_explore_media_access.rs @@ -0,0 +1,52 @@ +use enums::by_table::users::user_feature_flag::UserFeatureFlag; +use mysql_queries::queries::users::user_sessions::get_user_session_by_token::SessionUserRecord; +use users_component::utils::user_session_extended::UserSessionExtended; +use users_component::utils::user_session_feature_flags::UserSessionFeatureFlags; + +use crate::server_state::StaticFeatureFlags; + +/// Check whether we should allow the request access to explore media. +pub fn allowed_explore_media_access(maybe_session: Option) -> bool { + // NB: We're more careful here since this is sensitive until we have NLP checks. + maybe_session + .map(|ref session| session.can_access_explore_media()) + .unwrap_or(false) +} + +pub trait UserSessionExploreMediaFlag { + fn can_access_explore_media(&self) -> bool; +} + +impl UserSessionExploreMediaFlag for UserSessionExtended { + fn can_access_explore_media(&self) -> bool { + self.feature_flags.has_permission_unoptimized(UserFeatureFlag::ExploreMedia) + } +} + +impl UserSessionExploreMediaFlag for &UserSessionExtended { + fn can_access_explore_media(&self) -> bool { + self.feature_flags.has_permission_unoptimized(UserFeatureFlag::ExploreMedia) + } +} + +impl UserSessionExploreMediaFlag for SessionUserRecord { + fn can_access_explore_media(&self) -> bool { + user_session_has_feature(self) + } +} + +impl UserSessionExploreMediaFlag for &SessionUserRecord { + fn can_access_explore_media(&self) -> bool { + user_session_has_feature(self) + } +} + +fn user_session_has_feature(user_session: &SessionUserRecord) -> bool { + // TODO(bt, 2024-03-05): this is horrible. + // There should be a wrapper class between the query and the caller. + let flags = UserSessionFeatureFlags { + maybe_feature_flags: user_session.maybe_feature_flags.clone(), + }; + + flags.has_permission_unoptimized(UserFeatureFlag::ExploreMedia) +} diff --git a/crates/service/web/storyteller_web/src/util/allowed_studio_access.rs b/crates/service/web/storyteller_web/src/util/allowed_studio_access.rs index d30449d272..47b62e6bac 100644 --- a/crates/service/web/storyteller_web/src/util/allowed_studio_access.rs +++ b/crates/service/web/storyteller_web/src/util/allowed_studio_access.rs @@ -1,5 +1,7 @@ +use enums::by_table::users::user_feature_flag::UserFeatureFlag; use mysql_queries::queries::users::user_sessions::get_user_session_by_token::SessionUserRecord; use users_component::utils::user_session_extended::UserSessionExtended; +use users_component::utils::user_session_feature_flags::UserSessionFeatureFlags; use crate::server_state::StaticFeatureFlags; @@ -20,24 +22,44 @@ pub trait UserSessionStudioFlag { impl UserSessionStudioFlag for UserSessionExtended { fn can_access_studio(&self) -> bool { - self.role.can_access_studio + self.role.can_access_studio || self.feature_flags.has_permission_unoptimized(UserFeatureFlag::Studio) } } impl UserSessionStudioFlag for &UserSessionExtended { fn can_access_studio(&self) -> bool { - self.role.can_access_studio + self.role.can_access_studio || self.feature_flags.has_permission_unoptimized(UserFeatureFlag::Studio) } } impl UserSessionStudioFlag for SessionUserRecord { fn can_access_studio(&self) -> bool { - self.can_access_studio + if self.can_access_studio { + return true + } + + // TODO(bt, 2024-03-05): this is horrible. + // There should be a wrapper class between the query and the caller. + let flags = UserSessionFeatureFlags { + maybe_feature_flags: self.maybe_feature_flags.clone(), + }; + + flags.has_permission_unoptimized(UserFeatureFlag::Studio) } } impl UserSessionStudioFlag for &SessionUserRecord { fn can_access_studio(&self) -> bool { - self.can_access_studio + if self.can_access_studio { + return true + } + + // TODO(bt, 2024-03-05): this is horrible. + // There should be a wrapper class between the query and the caller. + let flags = UserSessionFeatureFlags { + maybe_feature_flags: self.maybe_feature_flags.clone(), + }; + + flags.has_permission_unoptimized(UserFeatureFlag::Studio) } } diff --git a/crates/service/web/storyteller_web/src/util/allowed_video_style_transfer_access.rs b/crates/service/web/storyteller_web/src/util/allowed_video_style_transfer_access.rs new file mode 100644 index 0000000000..2203d9a327 --- /dev/null +++ b/crates/service/web/storyteller_web/src/util/allowed_video_style_transfer_access.rs @@ -0,0 +1,55 @@ +use enums::by_table::users::user_feature_flag::UserFeatureFlag; +use mysql_queries::queries::users::user_sessions::get_user_session_by_token::SessionUserRecord; +use users_component::utils::user_session_extended::UserSessionExtended; +use users_component::utils::user_session_feature_flags::UserSessionFeatureFlags; + +use crate::server_state::StaticFeatureFlags; + +/// Check whether we should allow the request access to video style transfer +pub fn allowed_video_style_transfer_access(maybe_session: Option, flags: &StaticFeatureFlags) -> bool { + if !flags.force_session_video_style_transfer_flags { + return true; + } + + maybe_session + .map(|ref session| session.can_access_video_style_transfer()) + .unwrap_or(false) +} + +pub trait UserSessionVideoStyleTransferFlag { + fn can_access_video_style_transfer(&self) -> bool; +} + +impl UserSessionVideoStyleTransferFlag for UserSessionExtended { + fn can_access_video_style_transfer(&self) -> bool { + self.feature_flags.has_permission_unoptimized(UserFeatureFlag::VideoStyleTransfer) + } +} + +impl UserSessionVideoStyleTransferFlag for &UserSessionExtended { + fn can_access_video_style_transfer(&self) -> bool { + self.feature_flags.has_permission_unoptimized(UserFeatureFlag::VideoStyleTransfer) + } +} + +impl UserSessionVideoStyleTransferFlag for SessionUserRecord { + fn can_access_video_style_transfer(&self) -> bool { + user_session_has_feature(self) + } +} + +impl UserSessionVideoStyleTransferFlag for &SessionUserRecord { + fn can_access_video_style_transfer(&self) -> bool { + user_session_has_feature(self) + } +} + +fn user_session_has_feature(user_session: &SessionUserRecord) -> bool { + // TODO(bt, 2024-03-05): this is horrible. + // There should be a wrapper class between the query and the caller. + let flags = UserSessionFeatureFlags { + maybe_feature_flags: user_session.maybe_feature_flags.clone(), + }; + + flags.has_permission_unoptimized(UserFeatureFlag::VideoStyleTransfer) +} diff --git a/crates/service/web/storyteller_web/src/util/mod.rs b/crates/service/web/storyteller_web/src/util/mod.rs index 13b83e2474..1b4f55b6ed 100644 --- a/crates/service/web/storyteller_web/src/util/mod.rs +++ b/crates/service/web/storyteller_web/src/util/mod.rs @@ -1,5 +1,7 @@ +pub mod allowed_explore_media_access; pub mod allowed_studio_access; +pub mod allowed_video_style_transfer_access; pub mod delete_role_disambiguation; pub mod encrypted_sort_id; pub mod placeholder_images; -pub mod troll_user_bans; +pub mod troll_user_bans; \ No newline at end of file