Merge pull request #20 from storytold/premium-tts

Premium tts
This commit is contained in:
Brandon Thomas authored and GitHub committed 2022-11-02 05:28:54 -04:00
commit 9a1df0cd8c
17 files changed
+676 -120

No files matched your search

@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE tts_inference_jobs DROP COLUMN max_duration_seconds;
@@ -0,0 +1,7 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE tts_inference_jobs
ADD COLUMN max_duration_seconds INTEGER NOT NULL DEFAULT 0
AFTER is_debug_request;
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE w2l_inference_jobs DROP COLUMN max_duration_seconds;
@@ -0,0 +1,7 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE w2l_inference_jobs
ADD COLUMN max_duration_seconds INTEGER NOT NULL DEFAULT 0
AFTER creator_set_visibility;
@@ -65,6 +65,13 @@ CREATE TABLE tts_inference_jobs (
-- If true, the request gets routed to a special "debug" worker.
is_debug_request BOOLEAN NOT NULL DEFAULT FALSE,
-- ========== PREMIUM FEATURES METADATA ==========
-- The maximum duration for generated audio in seconds.
-- Zero is implied to be the default value, which is typically 12 seconds.
-- A negative value implies "unlimited".
max_duration_seconds INTEGER NOT NULL DEFAULT 0,
-- ========== JOB SYSTEM DETAILS ==========
-- Jobs begin as "pending", then transition to other states.
@@ -1,3 +1,6 @@
-- NB: This is a manually squashed view of all the CREATE and ALTER statements,
-- with comments attached to the fields for centralized documentation.
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
@@ -0,0 +1,135 @@
-- NB: This is a manually squashed view of all the CREATE and ALTER statements,
-- with comments attached to the fields for centralized documentation.
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE w2l_inference_jobs (
id BIGINT(20) NOT NULL AUTO_INCREMENT,
-- Effective "primary key" (PUBLIC)
-- This is so the in-progress results can be looked up by the UI.
token VARCHAR(32) NOT NULL,
-- Idempotency token from client
-- This is so the frontend client doesn't submit duplicate jobs.
uuid_idempotency_token VARCHAR(36) NOT NULL,
-- ========== SUCCESS CASE ==========
-- If the job completes successfully, this is the result token.
-- This is only populated on a successful result.
on_success_result_token VARCHAR(32) DEFAULT NULL,
-- ========== INFERENCE DETAILS : FACE TEMPLATE ==========
-- The W2L template to use
-- Can be an image or video.
-- This is null if we're using a custom uploaded image.
maybe_w2l_template_token VARCHAR(32) DEFAULT NULL,
-- If we're using a custom uploaded image, this will be present.
-- NOTE: This may never be supported.
maybe_public_image_bucket_location VARCHAR(255) DEFAULT NULL,
-- ========== INFERENCE DETAILS : AUDIO SOURCE ==========
-- If we're using TTS results, this will be present
maybe_tts_inference_result_token VARCHAR(32) DEFAULT NULL,
-- If we're using custom uploaded audio, this will be present.
maybe_public_audio_bucket_hash VARCHAR(64) DEFAULT NULL,
maybe_public_audio_bucket_location VARCHAR(255) DEFAULT NULL,
-- The filename that was used at upload time (if available)
maybe_original_audio_filename CHAR(255) DEFAULT NULL,
-- Where the audio file was originally downloaded (if it was downloaded)
maybe_original_audio_download_url VARCHAR(512) DEFAULT NULL,
maybe_audio_mime_type VARCHAR(32) DEFAULT NULL,
-- ========== CREATOR DETAILS AND PREFERENCES ==========
-- Foreign key to user
-- If no user is logged in, this is null.
maybe_creator_user_token VARCHAR(32) DEFAULT NULL,
-- Based on a cookie sent by the frontend.
-- We'll save this even if the user is logged in.
maybe_creator_anonymous_visitor_token VARCHAR(32) DEFAULT NULL,
-- For abuse tracking.
-- Wide enough for IPv4/6
creator_ip_address VARCHAR(40) NOT NULL,
-- (THIS MIGHT NOT BE USED)
-- NB: DO NOT SORT!
-- THIS MUST MATCH THE RESPECTIVE JOBS TABLE.
creator_set_visibility ENUM(
'public',
'hidden',
'private'
) NOT NULL DEFAULT 'public',
-- ========== PREMIUM FEATURES METADATA ==========
-- The maximum duration for generated audio in seconds.
-- Zero is implied to be the default value, which is typically 12 seconds.
-- A negative value implies "unlimited".
max_duration_seconds INTEGER NOT NULL DEFAULT 0,
-- Flags to disable branding
disable_end_bump BOOL NOT NULL DEFAULT false,
disable_watermark BOOL NOT NULL DEFAULT false,
-- ========== JOB SYSTEM DETAILS ==========
-- Jobs begin as "pending", then transition to other states.
--
-- * Pending = job is ready to go
-- * Started = job is running
-- * Complete_Success = job is done (success)
-- * Complete_Failure = job is done (failure)
-- * Attempt_Failed = job failed but may retry.
-- * Dead = job failed permanently.
--
-- Pending -> Started -> Complete_Success
-- |-> Complete_Failure
-- \-> Attempt_Failed -> Started -> { Complete, Failed, Dead }
status ENUM(
'pending',
'started',
'complete_success',
'complete_failure',
'attempt_failed',
'dead') NOT NULL DEFAULT 'pending',
-- We can track this against a "max_attempt_count"
attempt_count INT(3) NOT NULL DEFAULT 0,
-- If there is a failure, tell the user why.
failure_reason VARCHAR(512) DEFAULT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- Failed jobs will set a next attempt time.
-- Subsequent tries can increase the timeout.
-- Failures because of permissions require human intervention => [retry_at=null].
-- Failures because of invalid files are dead => [status=dead].
retry_at TIMESTAMP NULL,
-- INDICES --
PRIMARY KEY (id),
UNIQUE KEY (token),
UNIQUE KEY (uuid_idempotency_token),
KEY fk_on_success_result_token (on_success_result_token),
KEY fk_maybe_w2l_template_token (maybe_w2l_template_token),
KEY fk_maybe_tts_inference_result_token (maybe_tts_inference_result_token),
KEY fk_maybe_creator_user_token (maybe_creator_user_token),
KEY index_status (status),
KEY index_creator_ip_address (creator_ip_address)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -1,7 +1,7 @@
use chrono::{DateTime, Utc};
use database_queries::column_types::record_visibility::RecordVisibility;
#[derive(Clone)]
#[derive(Clone, Default)]
pub struct UserSessionExtended {
pub user_token: String,
pub user: UserSessionUserDetails,
@@ -10,7 +10,7 @@ pub struct UserSessionExtended {
pub role: UserSessionRoleAndPermissions,
}
#[derive(Clone)]
#[derive(Clone, Default)]
pub struct UserSessionUserDetails {
pub username: String,
pub display_name: String,
@@ -19,7 +19,7 @@ pub struct UserSessionUserDetails {
pub email_gravatar_hash: String,
}
#[derive(Clone)]
#[derive(Clone, Default)]
pub struct UserSessionPremiumPlanInfo {
pub maybe_stripe_customer_id: Option<String>,
pub maybe_loyalty_program_key: Option<String>,
@@ -41,7 +41,16 @@ pub struct UserSessionSubscriptionPlan {
pub subscription_expires_at: DateTime<Utc>,
}
#[derive(Clone)]
impl Default for UserSessionSubscriptionPlan {
fn default() -> Self {
Self {
subscription_expires_at: chrono::MIN_DATETIME,
..Default::default()
}
}
}
#[derive(Clone, Default)]
pub struct UserSessionPreferences {
pub disable_gravatar: bool,
pub auto_play_audio_preference: Option<bool>,
@@ -50,7 +59,7 @@ pub struct UserSessionPreferences {
pub auto_play_video_preference: Option<bool>,
}
#[derive(Clone)]
#[derive(Clone, Default)]
pub struct UserSessionRoleAndPermissions {
// ===== ROLE ===== //
pub user_role_slug: String,
@@ -15,6 +15,10 @@ pub enum RecordVisibility {
Private,
}
impl Default for RecordVisibility {
fn default() -> Self { Self::Public }
}
impl RecordVisibility {
pub fn to_str(&self) -> &'static str {
match self {
@@ -21,7 +21,14 @@ pub struct TtsInferenceJobInsertBuilder {
creator_set_visibility: Option<String>,
is_from_api: bool,
is_for_twitch: bool,
is_debug_request: bool, // Route the request to a special host with this flag
priority_level: u8, // Priority is 0 by default and optionally increases
/// Premium feature controlling TTS max duration.
/// - Negative values imply unlimited duration
/// - 0 is "use default" (typically 12 seconds)
/// - Positive values are that value in seconds up to some max.
max_duration_seconds: i32,
}
impl TtsInferenceJobInsertBuilder {
@@ -38,7 +45,9 @@ impl TtsInferenceJobInsertBuilder {
creator_set_visibility: None,
is_from_api: false,
is_for_twitch: false,
is_debug_request: false,
priority_level: 0,
max_duration_seconds: 0,
}
}
@@ -57,7 +66,9 @@ impl TtsInferenceJobInsertBuilder {
creator_set_visibility: Some("hidden".to_string()),
is_from_api: false,
is_for_twitch: false,
is_debug_request: false,
priority_level: 0,
max_duration_seconds: 0,
}
}
@@ -106,11 +117,21 @@ impl TtsInferenceJobInsertBuilder {
self
}
pub fn set_is_debug_request(mut self, value: bool) -> Self {
self.is_debug_request = value;
self
}
pub fn set_priority_level(mut self, value: u8) -> Self {
self.priority_level = value;
self
}
pub fn set_max_duration_seconds(mut self, value: i32) -> Self {
self.max_duration_seconds = value;
self
}
pub async fn insert(&mut self, mysql_pool: &MySqlPool) -> AnyhowResult<()> {
// TODO: These should be custom error types with a custom macro to make this easy.
let job_token = self.job_token
@@ -150,7 +171,9 @@ SET
creator_set_visibility = ?,
is_from_api = ?,
is_for_twitch = ?,
is_debug_request = ?,
priority_level = ?,
max_duration_seconds = ?,
status = "pending"
"#,
job_token,
@@ -162,7 +185,9 @@ SET
creator_set_visibility,
self.is_from_api,
self.is_for_twitch,
self.is_debug_request,
self.priority_level,
self.max_duration_seconds,
);
let query_result = query.execute(mysql_pool)
@@ -173,7 +198,7 @@ SET
res.last_insert_id()
},
Err(err) => {
return Err(anyhow!("Twitch pubsub bits insert DB error: {:?}", err));
return Err(anyhow!("MySQL tts_inference_job record insert error: {:?}", err));
}
};
@@ -0,0 +1,254 @@
use std::ops::Deref;
use chrono::Utc;
use crate::configs::plans::plan::Plan;
use crate::configs::plans::plan_list::{DEVELOPMENT_PREMIUM_PLANS, DEVELOPMENT_PREMIUM_PLANS_BY_SLUG, FREE_LOGGED_IN_PLAN, FREE_LOGGED_OUT_PLAN, LOYALTY_PLANS_BY_SLUG, PRODUCTION_PREMIUM_PLANS, PRODUCTION_PREMIUM_PLANS_BY_SLUG};
use url_config::server_environment::ServerEnvironment;
use users_component::utils::user_session_extended::{UserSessionExtended, UserSessionSubscriptionPlan};
/// Look up the most appropriate plan for the session.
/// This will probably grow to include a lot of factors.
pub fn get_correct_plan_for_session(
server_environment: ServerEnvironment,
maybe_user_session: Option<&UserSessionExtended>,
) -> Plan {
let user_session = match maybe_user_session {
None => {
return FREE_LOGGED_OUT_PLAN.clone();
},
Some(user_session) => user_session,
};
premium_plan_if_available(server_environment, user_session)
.or_else(|| loyalty_plan_if_available(user_session))
.unwrap_or(FREE_LOGGED_IN_PLAN.clone())
}
fn premium_plan_if_available(
server_environment: ServerEnvironment,
user_session: &UserSessionExtended,
) -> Option<Plan> {
let now = Utc::now();
let plan_set = match server_environment {
ServerEnvironment::Development => &DEVELOPMENT_PREMIUM_PLANS_BY_SLUG,
ServerEnvironment::Production => &PRODUCTION_PREMIUM_PLANS_BY_SLUG,
};
let applicable_plans = user_session.premium.subscription_plans
.iter()
.filter(|plan| plan.subscription_expires_at.gt(&now))
.filter_map(|plan| {
plan_set.get(&plan.subscription_product_slug)
})
.collect::<Vec<&Plan>>();
// TODO: For now it's only possible for users to have one plan, so we return the first match.
// We may need to revisit this if we add additional plans in the future.
applicable_plans.get(0).map(|plan| (*plan).clone())
}
fn loyalty_plan_if_available(
user_session: &UserSessionExtended,
) -> Option<Plan> {
user_session.premium.maybe_loyalty_program_key
.as_deref()
.and_then(|loyalty_key| LOYALTY_PLANS_BY_SLUG.get(loyalty_key))
.map(|plan| plan.clone())
}
#[cfg(test)]
pub mod tests {
use std::ops::{Add, Sub};
use chrono::{Duration, Utc};
use crate::configs::plans::get_correct_plan_for_session::get_correct_plan_for_session;
use crate::configs::plans::plan_list::{ALL_PLANS_BY_SLUG, FREE_LOGGED_IN_PLAN, FREE_LOGGED_OUT_PLAN};
use url_config::server_environment::ServerEnvironment;
use users_component::utils::user_session_extended::{UserSessionExtended, UserSessionSubscriptionPlan};
#[test]
fn test_free_logged_out_plan() {
let plan = get_correct_plan_for_session(ServerEnvironment::Development, None);
assert_eq!(plan, *FREE_LOGGED_OUT_PLAN);
let plan = get_correct_plan_for_session(ServerEnvironment::Production, None);
assert_eq!(plan, *FREE_LOGGED_OUT_PLAN);
}
#[test]
fn test_free_logged_in_plan() {
let mut user_session = UserSessionExtended::default();
user_session.premium.subscription_plans = vec![];
user_session.premium.maybe_loyalty_program_key = None;
let plan = get_correct_plan_for_session(
ServerEnvironment::Development,
Some(&user_session));
assert_eq!(plan, *FREE_LOGGED_IN_PLAN);
let plan = get_correct_plan_for_session(
ServerEnvironment::Production,
Some(&user_session));
assert_eq!(plan, *FREE_LOGGED_IN_PLAN);
}
#[test]
fn test_loyalty_plan() {
let mut user_session = UserSessionExtended::default();
user_session.premium.subscription_plans = vec![];
user_session.premium.maybe_loyalty_program_key = Some("loyalty_plan".to_string());
let plan = get_correct_plan_for_session(
ServerEnvironment::Development,
Some(&user_session));
assert_eq!(&plan, ALL_PLANS_BY_SLUG.get("loyalty_plan").unwrap());
let plan = get_correct_plan_for_session(
ServerEnvironment::Production,
Some(&user_session));
assert_eq!(&plan, ALL_PLANS_BY_SLUG.get("loyalty_plan").unwrap());
}
#[test]
fn test_premium_plan_valid_for_production() {
let mut user_session = UserSessionExtended::default();
let future_expiry = Utc::now().add(Duration::days(30));
user_session.premium.maybe_loyalty_program_key = None;
user_session.premium.subscription_plans = vec![
UserSessionSubscriptionPlan {
subscription_namespace: "fakeyou".to_string(),
subscription_product_slug: "fakeyou_en_plus".to_string(),
subscription_expires_at: future_expiry,
}
];
let plan = get_correct_plan_for_session(
ServerEnvironment::Development,
Some(&user_session));
// NB: The user has a "production" plan, which won't show up in development
assert_eq!(&plan, ALL_PLANS_BY_SLUG.get("free_logged_in").unwrap());
let plan = get_correct_plan_for_session(
ServerEnvironment::Production,
Some(&user_session));
// In production, however, we see the correct plan
assert_eq!(&plan, ALL_PLANS_BY_SLUG.get("fakeyou_en_plus").unwrap());
}
#[test]
fn test_premium_plan_valid_for_development() {
let mut user_session = UserSessionExtended::default();
let future_expiry = Utc::now().add(Duration::days(30));
user_session.premium.maybe_loyalty_program_key = None;
user_session.premium.subscription_plans = vec![
UserSessionSubscriptionPlan {
subscription_namespace: "fakeyou".to_string(),
subscription_product_slug: "development_fakeyou_en_plus".to_string(),
subscription_expires_at: future_expiry,
}
];
let plan = get_correct_plan_for_session(
ServerEnvironment::Development,
Some(&user_session));
// NB: In development we see the correct plan
assert_eq!(&plan, ALL_PLANS_BY_SLUG.get("development_fakeyou_en_plus").unwrap());
let plan = get_correct_plan_for_session(
ServerEnvironment::Production,
Some(&user_session));
// NB: The user has a "development" plan, which won't show up in production
assert_eq!(&plan, ALL_PLANS_BY_SLUG.get("free_logged_in").unwrap());
}
#[test]
fn test_expired_plan_no_longer_returned() {
let mut user_session = UserSessionExtended::default();
let already_expired = Utc::now().sub(Duration::days(30));
user_session.premium.maybe_loyalty_program_key = None;
user_session.premium.subscription_plans = vec![
UserSessionSubscriptionPlan {
subscription_namespace: "fakeyou".to_string(),
subscription_product_slug: "fakeyou_en_plus".to_string(),
subscription_expires_at: already_expired,
}
];
let plan = get_correct_plan_for_session(
ServerEnvironment::Development,
Some(&user_session));
// Premium plan is already expired
assert_eq!(&plan, ALL_PLANS_BY_SLUG.get("free_logged_in").unwrap());
let plan = get_correct_plan_for_session(
ServerEnvironment::Production,
Some(&user_session));
// Premium plan is already expired
assert_eq!(&plan, ALL_PLANS_BY_SLUG.get("free_logged_in").unwrap());
// NB: Just to show that it goes back to premium with a expiry in the future
let future_expiry = Utc::now().add(Duration::days(30));
user_session.premium.subscription_plans.get_mut(0).unwrap().subscription_expires_at = future_expiry;
let plan = get_correct_plan_for_session(
ServerEnvironment::Production,
Some(&user_session));
// NB: Now it isn't expired!
assert_eq!(&plan, ALL_PLANS_BY_SLUG.get("fakeyou_en_plus").unwrap());
}
#[test]
fn test_premium_plan_preferred_over_loyalty_plan() {
let mut user_session = UserSessionExtended::default();
let future_expiry = Utc::now().add(Duration::days(30));
user_session.premium.maybe_loyalty_program_key = Some("loyalty_plan".to_string());
user_session.premium.subscription_plans = vec![
UserSessionSubscriptionPlan {
subscription_namespace: "fakeyou".to_string(),
subscription_product_slug: "fakeyou_en_plus".to_string(),
subscription_expires_at: future_expiry,
}
];
let plan = get_correct_plan_for_session(
ServerEnvironment::Production,
Some(&user_session));
// In production we see the premium plan
assert_eq!(&plan, ALL_PLANS_BY_SLUG.get("fakeyou_en_plus").unwrap());
let plan = get_correct_plan_for_session(
ServerEnvironment::Development,
Some(&user_session));
// NB: The user has a "production" plan, and since this is invalid in development,
// we see the loyalty plan instead.
assert_eq!(&plan, ALL_PLANS_BY_SLUG.get("loyalty_plan").unwrap());
}
}
@@ -1,3 +1,4 @@
pub mod get_correct_plan_for_session;
pub mod plan;
pub mod plan_category;
pub mod plan_list;
@@ -14,7 +14,7 @@ const W2L_DEFAULT_TIME_LIMIT_SECONDS : i64 = 20;
/// A Plan is either a free or premium plan.
/// Each plan corresponds to a certain level of service.
#[derive(Clone, Eq, PartialEq, Hash)]
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
pub struct Plan {
/// Name of the plan in URL-friendly "slug" form (human readable, no spaces, underscores),
/// eg. "plan_1", "en_basic", etc.
@@ -37,6 +37,9 @@ pub struct Plan {
/// Whether this plan is only active in development
is_development_plan: bool,
/// Whether this plan does not exist in Stripe and only exists as defined by this server.
is_synthetic_plan: bool,
// ========== Features for TTS ==========
tts_base_priority_level: u8,
@@ -70,6 +73,7 @@ impl Plan {
stripe_product_id: builder.stripe_product_id.clone(),
stripe_price_id: builder.stripe_price_id.clone(),
is_development_plan: builder.is_development_plan,
is_synthetic_plan: builder.is_synthetic_plan,
tts_base_priority_level: builder.tts_base_priority_level,
tts_max_duration: builder.tts_max_duration,
tts_can_generate_mp3: builder.tts_can_generate_mp3,
@@ -101,10 +105,22 @@ impl Plan {
self.stripe_price_id.as_deref()
}
/// The plan is only valid in "development" Stripe.
/// These plans should never be used on the production cluster.
pub fn is_development_plan(&self) -> bool {
self.is_development_plan
}
/// The plan is a valid Stripe plan in production (not test/development).
pub fn is_production_plan(&self) -> bool {
!self.is_development_plan
}
/// The plan is "synthetic", ie. does not exist in Stripe.
pub fn is_synthetic_plan(&self) -> bool {
self.is_synthetic_plan
}
pub fn tts_base_priority_level(&self) -> u8 {
self.tts_base_priority_level
}
@@ -112,6 +128,10 @@ impl Plan {
pub fn tts_max_duration(&self) -> Duration {
self.tts_max_duration
}
pub fn tts_max_duration_seconds(&self) -> i32 {
self.tts_max_duration.num_seconds() as i32
}
}
/// Builder pattern for Plans.
@@ -123,6 +143,7 @@ pub struct PlanBuilder {
stripe_product_id: Option<String>,
stripe_price_id: Option<String>,
is_development_plan: bool,
is_synthetic_plan: bool,
// ========== Features for TTS ==========
@@ -158,6 +179,7 @@ impl PlanBuilder {
stripe_product_id: None,
stripe_price_id: None,
is_development_plan: false,
is_synthetic_plan: false,
// TTS
tts_base_priority_level : TTS_DEFAULT_PRIORITY_LEVEL,
@@ -208,6 +230,11 @@ impl PlanBuilder {
self
}
pub fn is_synthetic_plan(mut self, value: bool) -> Self {
self.is_synthetic_plan = value;
self
}
pub fn tts_base_priority_level(mut self, value: u8) -> Self {
self.tts_base_priority_level = value;
self
@@ -9,6 +9,7 @@ use tts_common::priority::{FAKEYOU_ANONYMOUS_PRIORITY_LEVEL, FAKEYOU_LOGGED_IN_P
/// This is declared at the top level because it may need to be referenced directly.
pub static FREE_LOGGED_OUT_PLAN : Lazy<Plan> = Lazy::new(|| {
PlanBuilder::new("free_logged_out")
.is_synthetic_plan(true)
.plan_category(PlanCategory::Free)
.tts_base_priority_level(FAKEYOU_ANONYMOUS_PRIORITY_LEVEL)
.build()
@@ -18,6 +19,7 @@ pub static FREE_LOGGED_OUT_PLAN : Lazy<Plan> = Lazy::new(|| {
/// This is declared at the top level because it may need to be referenced directly.
pub static FREE_LOGGED_OUT_FIRST_TRY_PLAN : Lazy<Plan> = Lazy::new(|| {
PlanBuilder::new("free_logged_out_first_try")
.is_synthetic_plan(true)
.plan_category(PlanCategory::Free)
.tts_base_priority_level(FAKEYOU_LOGGED_IN_PRIORITY_LEVEL) // NB: Same as logged-in free users.
.build()
@@ -27,24 +29,28 @@ pub static FREE_LOGGED_OUT_FIRST_TRY_PLAN : Lazy<Plan> = Lazy::new(|| {
/// This is declared at the top level because it may need to be referenced directly.
pub static FREE_LOGGED_IN_PLAN : Lazy<Plan> = Lazy::new(|| {
PlanBuilder::new("free_logged_in")
.is_synthetic_plan(true)
.plan_category(PlanCategory::Free)
.tts_base_priority_level(FAKEYOU_LOGGED_IN_PRIORITY_LEVEL)
.build()
});
/// This plan is for users that create models for us but don't pay.
/// This is declared at the top level because it may need to be referenced directly.
pub static LOYALTY_PLAN : Lazy<Plan> = Lazy::new(|| {
PlanBuilder::new("loyalty_plan")
/// These plans are for users that create models for us but don't pay.
pub static LOYALTY_PLANS : Lazy<HashSet<Plan>> = Lazy::new(|| {
let mut plans = HashSet::new();
plans.insert(PlanBuilder::new("loyalty_plan")
.is_synthetic_plan(true)
.plan_category(PlanCategory::LoyaltyReward)
.tts_base_priority_level(2)
.tts_max_duration_seconds(30)
.build()
.build());
plans
});
// TODO: Add by-[stripe-product] lookup HashSets.
pub static DEVELOPMENT_PLANS : Lazy<HashSet<Plan>> = Lazy::new(|| {
/// These plans do not exist in production!
pub static DEVELOPMENT_PREMIUM_PLANS: Lazy<HashSet<Plan>> = Lazy::new(|| {
let mut plans = HashSet::new();
// ========== English plans ==========
@@ -72,8 +78,8 @@ pub static DEVELOPMENT_PLANS : Lazy<HashSet<Plan>> = Lazy::new(|| {
plans
});
/// All premium / paid-for plans in the system.
pub static PREMIUM_PLANS : Lazy<HashSet<Plan>> = Lazy::new(|| {
/// All paid-for production premium plans in the system.
pub static PRODUCTION_PREMIUM_PLANS: Lazy<HashSet<Plan>> = Lazy::new(|| {
let mut plans = HashSet::new();
// ========== English plans ==========
@@ -111,6 +117,45 @@ pub static PREMIUM_PLANS : Lazy<HashSet<Plan>> = Lazy::new(|| {
plans
});
/// Only loyalty premium plans (non-Stripe, non-PayPal, etc. - internally defined)
pub static LOYALTY_PLANS_BY_SLUG : Lazy<HashMap<String, Plan>> = Lazy::new(|| {
let mut plans = HashMap::new();
fn add_plan(plans: &mut HashMap<String, Plan>, plan: &Plan) {
plans.insert(plan.plan_slug().to_string(), plan.clone());
}
LOYALTY_PLANS.iter().for_each(|plan| add_plan(&mut plans, plan));
plans
});
/// Only development premium plans
pub static DEVELOPMENT_PREMIUM_PLANS_BY_SLUG : Lazy<HashMap<String, Plan>> = Lazy::new(|| {
let mut plans = HashMap::new();
fn add_plan(plans: &mut HashMap<String, Plan>, plan: &Plan) {
plans.insert(plan.plan_slug().to_string(), plan.clone());
}
DEVELOPMENT_PREMIUM_PLANS.iter().for_each(|plan| add_plan(&mut plans, plan));
plans
});
/// Only production premium plans
pub static PRODUCTION_PREMIUM_PLANS_BY_SLUG : Lazy<HashMap<String, Plan>> = Lazy::new(|| {
let mut plans = HashMap::new();
fn add_plan(plans: &mut HashMap<String, Plan>, plan: &Plan) {
plans.insert(plan.plan_slug().to_string(), plan.clone());
}
PRODUCTION_PREMIUM_PLANS.iter().for_each(|plan| add_plan(&mut plans, plan));
plans
});
/// Every single plan in the system: free, paid, loyalty-based, and Stripe development plans.
pub static ALL_PLANS_BY_SLUG : Lazy<HashMap<String, Plan>> = Lazy::new(|| {
let mut plans = HashMap::new();
@@ -122,10 +167,10 @@ pub static ALL_PLANS_BY_SLUG : Lazy<HashMap<String, Plan>> = Lazy::new(|| {
add_plan(&mut plans, &FREE_LOGGED_OUT_PLAN);
add_plan(&mut plans, &FREE_LOGGED_OUT_FIRST_TRY_PLAN);
add_plan(&mut plans, &FREE_LOGGED_IN_PLAN);
add_plan(&mut plans, &LOYALTY_PLAN);
PREMIUM_PLANS.iter().for_each(|plan| add_plan(&mut plans, plan));
DEVELOPMENT_PLANS .iter().for_each(|plan| add_plan(&mut plans, plan));
LOYALTY_PLANS.iter().for_each(|plan| add_plan(&mut plans, plan));
PRODUCTION_PREMIUM_PLANS.iter().for_each(|plan| add_plan(&mut plans, plan));
DEVELOPMENT_PREMIUM_PLANS.iter().for_each(|plan| add_plan(&mut plans, plan));
plans
});
@@ -158,13 +203,11 @@ pub static PLANS_BY_STRIPE_PRICE_ID : Lazy<HashMap<String, Plan>> = Lazy::new(||
plans
});
// TODO: Add tests to assert our expectations of each plan.
#[cfg(test)]
mod test {
use crate::configs::plans::plan::Plan;
use crate::configs::plans::plan_category::PlanCategory;
use crate::configs::plans::plan_list::{ALL_PLANS_BY_SLUG, DEVELOPMENT_PLANS, PLANS_BY_STRIPE_PRICE_ID, PLANS_BY_STRIPE_PRODUCT_ID, PREMIUM_PLANS};
use crate::configs::plans::plan_list::{ALL_PLANS_BY_SLUG, DEVELOPMENT_PREMIUM_PLANS, DEVELOPMENT_PREMIUM_PLANS_BY_SLUG, LOYALTY_PLANS, LOYALTY_PLANS_BY_SLUG, PLANS_BY_STRIPE_PRICE_ID, PLANS_BY_STRIPE_PRODUCT_ID, PRODUCTION_PREMIUM_PLANS, PRODUCTION_PREMIUM_PLANS_BY_SLUG};
use speculoos::prelude::*;
// NB: We're being extremely careful in this test and all those that follow, essentially
@@ -175,23 +218,41 @@ mod test {
}
#[test]
fn test_number_of_production_plans_is_expected() {
fn test_number_of_stripe_production_plans_is_expected() {
let production_plans = ALL_PLANS_BY_SLUG.values()
.filter(|plan| !plan.is_development_plan())
.filter(|plan| !plan.is_synthetic_plan()) // NB: Synthetic plans are production!
.collect::<Vec<&Plan>>();
assert_eq!(8, production_plans.len());
let expected = 4;
assert_eq!(expected, production_plans.len());
assert_eq!(expected, PRODUCTION_PREMIUM_PLANS.len());
assert_eq!(expected, PRODUCTION_PREMIUM_PLANS_BY_SLUG.len());
}
#[test]
fn test_number_of_development_plans_is_expected() {
fn test_number_of_stripe_development_plans_is_expected() {
let development_plans = ALL_PLANS_BY_SLUG.values()
.filter(|plan| plan.is_development_plan())
.collect::<Vec<&Plan>>();
assert_eq!(2, development_plans.len());
let expected = 2;
assert_eq!(expected, development_plans.len());
assert_eq!(expected, DEVELOPMENT_PREMIUM_PLANS.len());
assert_eq!(expected, DEVELOPMENT_PREMIUM_PLANS_BY_SLUG.len());
}
#[test]
fn test_number_of_loyalty_plans_is_expected() {
let expected = 1;
assert_eq!(expected, LOYALTY_PLANS.len());
assert_eq!(expected, LOYALTY_PLANS_BY_SLUG.len());
}
#[test]
fn test_plans_by_stripe_product_id_are_expected() {
let product_ids = PLANS_BY_STRIPE_PRODUCT_ID.keys()
@@ -256,65 +317,92 @@ mod test {
assert_eq!(120, ALL_PLANS_BY_SLUG.get("fakeyou_en_pro").unwrap().tts_max_duration().num_seconds());
}
// The following tests should not need to change much
// =================== THE FOLLOWING TESTS SHOULD NOT NEED TO CHANGE MUCH =================== //
#[test]
fn test_assert_all_premium_plans_are_paid() {
let mut test_count = 0;
PREMIUM_PLANS.iter().for_each(|plan| {
fn test_assert_all_production_stripe_plans_are_paid() {
PRODUCTION_PREMIUM_PLANS.iter().for_each(|plan| {
assert_eq!(plan.plan_category(), PlanCategory::Paid);
test_count += 1;
});
PRODUCTION_PREMIUM_PLANS_BY_SLUG.iter().for_each(|(_, plan)| {
assert_eq!(plan.plan_category(), PlanCategory::Paid);
});
}
assert!(test_count > 1);
#[test]
fn test_assert_all_production_stripe_plans_are_non_synthetic() {
PRODUCTION_PREMIUM_PLANS.iter().for_each(|plan| {
assert!(!plan.is_synthetic_plan());
});
PRODUCTION_PREMIUM_PLANS_BY_SLUG.iter().for_each(|(_, plan)| {
assert!(!plan.is_synthetic_plan());
});
}
#[test]
fn test_assert_all_development_stripe_plans_are_marked_development() {
let mut test_count = 0;
DEVELOPMENT_PLANS.iter().for_each(|plan| {
DEVELOPMENT_PREMIUM_PLANS.iter().for_each(|plan| {
assert!(plan.is_development_plan());
});
DEVELOPMENT_PREMIUM_PLANS_BY_SLUG.iter().for_each(|(_, plan)| {
assert!(plan.is_development_plan());
test_count += 1;
});
assert!(test_count > 1);
}
#[test]
fn test_assert_correct_key_for_plans_indexed_by_slug() {
let mut test_count = 0;
fn test_assert_all_development_stripe_plans_are_non_synthetic() {
DEVELOPMENT_PREMIUM_PLANS.iter().for_each(|plan| {
assert!(!plan.is_synthetic_plan());
});
DEVELOPMENT_PREMIUM_PLANS_BY_SLUG.iter().for_each(|(_, plan)| {
assert!(!plan.is_synthetic_plan());
});
}
#[test]
fn test_assert_all_loyalty_plans_are_synthetic() {
LOYALTY_PLANS.iter().for_each(|plan| {
assert!(plan.is_synthetic_plan());
});
}
#[test]
fn test_assert_correct_key_used_for_plans_indexed_by_slug() {
ALL_PLANS_BY_SLUG.iter().for_each(|(slug, plan)| {
assert_eq!(slug, plan.plan_slug());
test_count += 1;
});
assert!(test_count > 1);
PRODUCTION_PREMIUM_PLANS_BY_SLUG.iter().for_each(|(slug, plan)| {
assert_eq!(slug, plan.plan_slug());
});
DEVELOPMENT_PREMIUM_PLANS_BY_SLUG.iter().for_each(|(slug, plan)| {
assert_eq!(slug, plan.plan_slug());
});
LOYALTY_PLANS_BY_SLUG.iter().for_each(|(slug, plan)| {
assert_eq!(slug, plan.plan_slug());
});
}
#[test]
fn test_assert_correct_key_for_plans_indexed_by_product_id() {
let mut test_count = 0;
PLANS_BY_STRIPE_PRODUCT_ID.iter().for_each(|(product_id, plan)| {
assert_eq!(product_id, plan.stripe_product_id().unwrap());
test_count += 1;
});
assert!(test_count > 1);
}
#[test]
fn test_assert_correct_key_for_plans_indexed_by_price_id() {
let mut test_count = 0;
PLANS_BY_STRIPE_PRICE_ID.iter().for_each(|(price_id, plan)| {
assert_eq!(price_id, plan.stripe_price_id().unwrap());
test_count += 1;
});
}
assert!(test_count > 1);
#[test]
fn test_assert_stripe_plans_are_non_synthetic() {
PLANS_BY_STRIPE_PRICE_ID.iter().for_each(|(price_id, plan)| {
assert!(!plan.is_synthetic_plan());
});
PLANS_BY_STRIPE_PRODUCT_ID.iter().for_each(|(price_id, plan)| {
assert!(!plan.is_synthetic_plan());
});
}
}
@@ -10,6 +10,7 @@ use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::server_state::ServerState;
use crate::validations::model_uploads::validate_model_title;
use database_queries::column_types::record_visibility::RecordVisibility;
use database_queries::queries::tts::tts_inference_jobs::insert_tts_inference_job::TtsInferenceJobInsertBuilder;
use database_queries::tokens::Tokens;
use http_server_common::request::get_request_api_token::get_request_api_token;
use http_server_common::request::get_request_header_optional::get_request_header_optional;
@@ -18,13 +19,11 @@ use log::{info, warn, log};
use r2d2_redis::redis::Commands;
use redis_common::redis_keys::RedisKeys;
use regex::Regex;
use sqlx::error::DatabaseError;
use sqlx::error::Error::Database;
use sqlx::mysql::MySqlDatabaseError;
use std::fmt;
use std::sync::Arc;
use tts_common::priority::{FAKEYOU_LOGGED_IN_PRIORITY_LEVEL, FAKEYOU_ANONYMOUS_PRIORITY_LEVEL, FAKEYOU_INVESTOR_PRIORITY_LEVEL, FAKEYOU_DEFAULT_VALID_API_TOKEN_PRIORITY_LEVEL};
use user_input_common::check_for_slurs::contains_slurs;
use crate::configs::plans::get_correct_plan_for_session::get_correct_plan_for_session;
// TODO: Temporary for investor demo
const STORYTELLER_DEMO_COOKIE_NAME : &'static str = "storyteller_demo";
@@ -92,15 +91,23 @@ pub async fn infer_tts_handler(
{
let mut is_from_api = false;
let mut maybe_user_token : Option<String> = None;
let mut priority_level = FAKEYOU_ANONYMOUS_PRIORITY_LEVEL;
let mut priority_level ;
let mut use_high_priority_rate_limiter = false; // NB: Careful!
let mut disable_rate_limiter = false; // NB: Careful!
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await
.map_err(|err| {
warn!("MySql pool error: {:?}", err);
InferTtsError::ServerError
})?;
// ==================== USER SESSION ==================== //
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
.maybe_get_user_session_extended_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
@@ -109,9 +116,15 @@ pub async fn infer_tts_handler(
if let Some(user_session) = maybe_user_session.as_ref() {
maybe_user_token = Some(user_session.user_token.to_string());
priority_level = FAKEYOU_LOGGED_IN_PRIORITY_LEVEL; // Give logged in users execution priority.
}
// TODO: Plan should handle "first anonymous use" and "investor" cases.
let plan = get_correct_plan_for_session(
server_state.server_environment,
maybe_user_session.as_ref());
priority_level = plan.tts_base_priority_level();
// ==================== API TOKENS ==================== //
if let Some(api_token) = get_request_api_token(&http_request) {
@@ -171,7 +184,7 @@ pub async fn infer_tts_handler(
let mut rate_limiter = match maybe_user_session {
None => &server_state.redis_rate_limiters.logged_out,
Some(ref user) => {
if user.is_banned {
if user.role.is_banned {
return Err(InferTtsError::NotAuthorized);
}
&server_state.redis_rate_limiters.logged_in
@@ -227,7 +240,7 @@ pub async fn infer_tts_handler(
let maybe_user_preferred_visibility : Option<RecordVisibility> = maybe_user_session
.as_ref()
.map(|user_session| user_session.preferred_tts_result_visibility);
.map(|user_session| user_session.preferences.preferred_tts_result_visibility);
let set_visibility = request.creator_set_visibility
.or(maybe_user_preferred_visibility)
@@ -240,69 +253,30 @@ pub async fn infer_tts_handler(
InferTtsError::ServerError
})?;
info!("Creating w2l inference job record...");
info!("Creating tts inference job record...");
let query_result = sqlx::query!(
r#"
INSERT INTO tts_inference_jobs
SET
token = ?,
uuid_idempotency_token = ?,
let query_result = TtsInferenceJobInsertBuilder::new_for_fakeyou_request()
.set_job_token(&job_token)
.set_uuid_idempotency_token(&request.uuid_idempotency_token)
.set_model_token(&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(plan.tts_max_duration_seconds())
.set_is_from_api(is_from_api)
.set_is_debug_request(is_debug_request)
.insert(&server_state.mysql_pool)
.await;
model_token = ?,
raw_inference_text = ?,
maybe_creator_user_token = ?,
creator_ip_address = ?,
creator_set_visibility = ?,
priority_level = ?,
is_from_api = ?,
is_debug_request = ?,
status = "pending"
"#,
&job_token,
request.uuid_idempotency_token.clone(),
model_token,
inference_text,
maybe_user_token,
ip_address,
set_visibility.to_str(),
priority_level,
is_from_api,
is_debug_request,
)
.execute(&server_state.mysql_pool)
.await;
let record_id = match query_result {
Ok(res) => {
res.last_insert_id()
},
match query_result {
Ok(_) => {},
Err(err) => {
warn!("New w2l inference job creation DB error: {:?}", err);
// NB: SQLSTATE[23000]: Integrity constraint violation
// NB: MySQL Error Code 1062: Duplicate key insertion (this is harder to access)
match err {
Database(err) => {
let maybe_code = err.code().map(|c| c.into_owned());
/*match maybe_code.as_deref() {
Some("23000") => {
if err.message().contains("username") {
return Err(UsernameTaken);
} else if err.message().contains("email_address") {
return Err(EmailTaken);
}
}
_ => {},
}*/
},
_ => {},
}
warn!("New tts inference job creation DB error: {:?}", err);
return Err(InferTtsError::ServerError);
}
};
info!("new w2l inference job id: {}", record_id);
}
server_state.firehose_publisher.enqueue_tts_inference(maybe_user_token.as_deref(), &job_token, &model_token)
.await
@@ -349,6 +349,7 @@ async fn main() -> AnyhowResult<()> {
client: stripe_client,
},
hostname: server_hostname,
server_environment,
third_party_url_redirector,
health_check_status,
mysql_pool: pool,
@@ -14,6 +14,7 @@ use memory_caching::single_item_ttl_cache::SingleItemTtlCache;
use r2d2_redis::{r2d2, RedisConnectionManager};
use sqlx::MySqlPool;
use storage_buckets_common::bucket_client::BucketClient;
use url_config::server_environment::ServerEnvironment;
use url_config::third_party_url_redirector::ThirdPartyUrlRedirector;
use users_component::utils::session_checker::SessionChecker;
use users_component::utils::session_cookie_manager::SessionCookieManager;
@@ -27,6 +28,9 @@ pub struct ServerState {
pub hostname: String,
/// Knowing if we're in production will allow us to turn off development-only functionalities.
pub server_environment: ServerEnvironment,
pub third_party_url_redirector: ThirdPartyUrlRedirector,
pub health_check_status: HealthCheckStatus,