Compare commits

..
28 changed files with 454 additions and 227 deletions
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO analytics_app_sessions\nSET\n session_token = ?,\n app_namespace = ?,\n user_token = ?,\n app_version = ?,\n os_platform = ?,\n os_version = ?,\n session_duration_seconds = ?,\n ip_address = ?,\n measurement_count = measurement_count + 1,\n first_active_at = NOW(),\n last_active_at = NOW()\nON DUPLICATE KEY UPDATE\n app_namespace = ?,\n user_token = ?,\n app_version = ?,\n os_platform = ?,\n os_version = ?,\n session_duration_seconds = ?,\n ip_address = ?,\n measurement_count = measurement_count + 1,\n last_active_at = NOW()\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 15
},
"nullable": []
},
"hash": "ff39576a5ec6a23c67f6f61e7eba8392e70b05c42b540bea260a9a4cdc478a5b"
}
@@ -2,6 +2,8 @@
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- Track user activity over time
-- User clients ping the endpoint that writes this data.
CREATE TABLE analytics_app_active_users (
-- Not used for anything except replication.
id BIGINT(20) NOT NULL AUTO_INCREMENT,
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
drop table analytics_app_sessions;
@@ -0,0 +1,65 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- Track user session durations
-- User clients ping the endpoint that writes this data.
CREATE TABLE analytics_app_sessions (
-- Not used for anything except replication.
id BIGINT(20) NOT NULL AUTO_INCREMENT,
-- Generated client-side
-- Wide enough for a UUID, but typically one of our internal tokens.
session_token VARCHAR(36) NOT NULL,
-- If we host multiple sites with distinct categories, this will enable us to
-- segregate analytics.
app_namespace VARCHAR(32) NOT NULL,
-- Who sent the event.
user_token VARCHAR(32) NOT NULL,
-- Updated on every write.
-- The "user agent" or app version string of the client.
app_version VARCHAR(255) DEFAULT NULL,
-- Updated on every write.
-- The OS platform for the client.
os_platform VARCHAR(16) DEFAULT NULL,
-- Updated on every write.
-- The OS platform version for the client.
os_version VARCHAR(64) DEFAULT NULL,
-- Updated on every write.
-- How long the current user's session has been open
session_duration_seconds INT(10) UNSIGNED DEFAULT NULL,
-- Updated on every write.
-- The user's last known IP address.
ip_address VARCHAR(40) NOT NULL,
-- Incrementing count of events.
-- NB: This is kind of an "hours spent" measure, but will lose information if we ever
-- change the ping cadence (eg. 1 minute pings --> 5 minute pings).
measurement_count BIGINT UNSIGNED NOT NULL DEFAULT 0,
-- ========== RECORD TIMESTAMPS ==========
-- Written at create only.
first_active_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- Updated on every ping.
last_active_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- ========== INDICES ==========
PRIMARY KEY (id),
UNIQUE KEY (session_token),
INDEX idx_app_namespace (app_namespace),
INDEX idx_user_token (user_token),
INDEX idx_os_platform (os_platform),
INDEX idx_os_version (os_version),
INDEX idx_app_version (app_version),
INDEX idx_last_active_at (last_active_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,65 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- Track user session durations
-- User clients ping the endpoint that writes this data.
CREATE TABLE analytics_app_sessions (
-- Not used for anything except replication.
id BIGINT(20) NOT NULL AUTO_INCREMENT,
-- Generated client-side
-- Wide enough for a UUID, but typically one of our internal tokens.
session_token VARCHAR(36) NOT NULL,
-- If we host multiple sites with distinct categories, this will enable us to
-- segregate analytics.
app_namespace VARCHAR(32) NOT NULL,
-- Who sent the event.
user_token VARCHAR(32) NOT NULL,
-- Updated on every write.
-- The "user agent" or app version string of the client.
app_version VARCHAR(255) DEFAULT NULL,
-- Updated on every write.
-- The OS platform for the client.
os_platform VARCHAR(16) DEFAULT NULL,
-- Updated on every write.
-- The OS platform version for the client.
os_version VARCHAR(64) DEFAULT NULL,
-- Updated on every write.
-- How long the current user's session has been open
session_duration_seconds INT(10) UNSIGNED DEFAULT NULL,
-- Updated on every write.
-- The user's last known IP address.
ip_address VARCHAR(40) NOT NULL,
-- Incrementing count of events.
-- NB: This is kind of an "hours spent" measure, but will lose information if we ever
-- change the ping cadence (eg. 1 minute pings --> 5 minute pings).
measurement_count BIGINT UNSIGNED NOT NULL DEFAULT 0,
-- ========== RECORD TIMESTAMPS ==========
-- Written at create only.
first_active_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- Updated on every ping.
last_active_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- ========== INDICES ==========
PRIMARY KEY (id),
UNIQUE KEY (session_token),
INDEX idx_app_namespace (app_namespace),
INDEX idx_user_token (user_token),
INDEX idx_os_platform (os_platform),
INDEX idx_os_version (os_version),
INDEX idx_app_version (app_version),
INDEX idx_last_active_at (last_active_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -1,10 +1,15 @@
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::app_session::AppSessionToken;
use utoipa::ToSchema;
pub const LOG_ACTIVE_USER_PATH: &str = "/v1/analytics/active_user";
#[derive(Serialize, Deserialize, ToSchema, Debug)]
pub struct LogAppActiveUserRequest {
/// Clientside-generated session token.
/// If present, it'll be validated against an expected format.
pub maybe_app_session_token: Option<AppSessionToken>,
/// An override for the application platform/OS (windows, mac, linux).
pub maybe_os_platform: Option<String>,
@@ -15,6 +15,10 @@ pub async fn log_active_user(
let mut query = HashMap::new();
if let Some(token) = request.maybe_app_session_token {
query.insert("maybe_app_session_token".to_string(), token.to_string());
}
if let Some(os_platform) = request.maybe_os_platform {
query.insert("maybe_os_platform".to_string(), os_platform);
}
@@ -0,0 +1,18 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::video::generate_veo_3_fast_image_to_video::{GenerateVeo3FastImageToVideoRequest, GenerateVeo3FastImageToVideoResponse, GENERATE_VEO_3_FAST_IMAGE_TO_VIDEO_URL_PATH};
pub async fn generate_veo_3_fast_image_to_video(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: GenerateVeo3FastImageToVideoRequest,
) -> Result<GenerateVeo3FastImageToVideoResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
GENERATE_VEO_3_FAST_IMAGE_TO_VIDEO_URL_PATH,
maybe_creds,
request,
).await?)
}
@@ -0,0 +1,18 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::video::generate_veo_3_image_to_video::{GenerateVeo3ImageToVideoRequest, GenerateVeo3ImageToVideoResponse, GENERATE_VEO_3_IMAGE_TO_VIDEO_URL_PATH};
pub async fn generate_veo_3_image_to_video(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: GenerateVeo3ImageToVideoRequest,
) -> Result<GenerateVeo3ImageToVideoResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
GENERATE_VEO_3_IMAGE_TO_VIDEO_URL_PATH,
maybe_creds,
request,
).await?)
}
@@ -2,4 +2,6 @@ pub mod generate_kling_16_pro_image_to_video;
pub mod generate_kling_21_master_image_to_video;
pub mod generate_kling_21_pro_image_to_video;
pub mod generate_seedance_1_0_lite_image_to_video;
pub mod generate_veo_2_image_to_video;
pub mod generate_veo_2_image_to_video;
pub mod generate_veo_3_fast_image_to_video;
pub mod generate_veo_3_image_to_video;
@@ -25,7 +25,7 @@ pub async fn basic_query_string_post_request<Res: DeserializeOwned>(
// TODO: Please stop using this URL Builder library. It's not very safe or intuitive.
let url = get_route(api_host, route_path, query_params);
info!("Requesting {:?}", &url);
debug!("Requesting {:?}", &url);
let client = Client::builder()
.gzip(true)
@@ -41,6 +41,12 @@ pub enum VideoModel {
#[serde(rename = "veo_2")]
Veo2,
#[serde(rename = "veo_3")]
Veo3,
#[serde(rename = "veo_3_fast")]
Veo3Fast,
}
#[derive(Deserialize)]
@@ -13,6 +13,8 @@ use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video
use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::{GenerateKling21ProAspectRatio, GenerateKling21ProImageToVideoRequest};
use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteImageToVideoRequest;
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::{GenerateVeo2AspectRatio, GenerateVeo2ImageToVideoRequest};
use artcraft_api_defs::generate::video::generate_veo_3_fast_image_to_video::GenerateVeo3FastImageToVideoRequest;
use artcraft_api_defs::generate::video::generate_veo_3_image_to_video::{GenerateVeo3AspectRatio, GenerateVeo3ImageToVideoRequest};
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
@@ -25,6 +27,8 @@ use storyteller_client::endpoints::generate::video::generate_kling_21_master_ima
use storyteller_client::endpoints::generate::video::generate_kling_21_pro_image_to_video::generate_kling_21_pro_image_to_video;
use storyteller_client::endpoints::generate::video::generate_seedance_1_0_lite_image_to_video::generate_seedance_1_0_lite_image_to_video;
use storyteller_client::endpoints::generate::video::generate_veo_2_image_to_video::generate_veo_2_image_to_video;
use storyteller_client::endpoints::generate::video::generate_veo_3_fast_image_to_video::generate_veo_3_fast_image_to_video;
use storyteller_client::endpoints::generate::video::generate_veo_3_image_to_video::generate_veo_3_image_to_video;
use storyteller_client::utils::api_host::ApiHost;
use tauri::AppHandle;
@@ -202,6 +206,61 @@ pub async fn handle_video_artcraft(
}
}
}
Some(VideoModel::Veo3) => {
info!("enqueue Veo 3 with Artcraft API");
selected_model = Some(GenerationModel::Veo3);
let request = GenerateVeo3ImageToVideoRequest {
uuid_idempotency_token,
media_file_token: request.image_media_token,
aspect_ratio: Some(GenerateVeo3AspectRatio::WideSixteenNine),
prompt: request.prompt,
duration: None,
resolution: None,
generate_audio: None,
};
let result = generate_veo_3_image_to_video(
&app_env_configs.storyteller_host,
Some(&creds),
request,
).await;
match result {
Ok(enqueued) => {
info!("Successfully enqueued Artcraft Veo 3 video generation");
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft Veo 3 video generation: {:?}", err);
return Err(GenerateError::from(err));
}
}
}
Some(VideoModel::Veo3Fast) => {
info!("enqueue Veo 3 Fast with Artcraft API");
selected_model = Some(GenerationModel::Veo3Fast);
let request = GenerateVeo3FastImageToVideoRequest {
uuid_idempotency_token,
media_file_token: request.image_media_token,
prompt: request.prompt,
duration: None,
resolution: None,
generate_audio: None,
};
let result = generate_veo_3_fast_image_to_video(
&app_env_configs.storyteller_host,
Some(&creds),
request,
).await;
match result {
Ok(enqueued) => {
info!("Successfully enqueued Artcraft Veo 3 video generation");
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft Veo 3 video generation: {:?}", err);
return Err(GenerateError::from(err));
}
}
}
};
info!("Successfully enqueued video generation");
@@ -71,6 +71,8 @@ impl TaskEnqueueSuccess{
Some(GenerationModel::Kling21Master) => Some(TaskModelType::Kling21Master),
Some(GenerationModel::Seedance10Lite) => Some(TaskModelType::Seedance10Lite),
Some(GenerationModel::Veo2) => Some(TaskModelType::Veo2),
Some(GenerationModel::Veo3) => Some(TaskModelType::Veo3),
Some(GenerationModel::Veo3Fast) => Some(TaskModelType::Veo3Fast),
Some(GenerationModel::Hunyuan3d2_0) => Some(TaskModelType::Hunyuan3d2_0),
Some(GenerationModel::Hunyuan3d2_1) => Some(TaskModelType::Hunyuan3d2_1),
Some(GenerationModel::Midjourney) => Some(TaskModelType::Midjourney), // NB: This is a generic Midjourney model, version unknown.
@@ -59,6 +59,10 @@ pub enum GenerationModel {
Seedance10Lite,
#[serde(rename = "veo_2")]
Veo2,
#[serde(rename = "veo_3")]
Veo3,
#[serde(rename = "veo_3_fast")]
Veo3Fast,
}
#[derive(Debug, Copy, Clone, Serialize)]
@@ -1,6 +1,7 @@
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::GenerationServiceProvider;
use crate::core::events::generation_events::generation_complete_event::GenerationCompleteEvent;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::utils::download_url_to_temp_dir::download_url_to_temp_dir;
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
@@ -15,7 +16,6 @@ use storyteller_client::recipes::upload_media_file_from_file::upload_media_file_
use storyteller_client::utils::api_host::ApiHost;
use tauri::AppHandle;
use tokens::tokens::media_files::MediaFileToken;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
pub async fn fal_task_polling_thread(
app_handle: AppHandle,
@@ -14,7 +14,7 @@ use enums::common::job_status_plus::JobStatusPlus;
use enums::tauri::tasks::task_status::TaskStatus;
use enums::tauri::tasks::task_type::TaskType;
use errors::AnyhowResult;
use log::{error, info};
use log::{debug, error, info};
use os_info::Info;
use reqwest::Url;
use sqlite_tasks::queries::list_tasks_by_provider_and_tokens::{list_tasks_by_provider_and_tokens, ListTasksArgs, Task};
@@ -27,6 +27,7 @@ use storyteller_client::error::storyteller_error::StorytellerError;
use storyteller_client::endpoints::jobs::list_session_jobs::{list_session_jobs, States};
use storyteller_client::endpoints::media_files::upload_image_media_file_from_file::upload_image_media_file_from_file;
use tauri::AppHandle;
use tokens::tokens::app_session::AppSessionToken;
// TODO: Configure this with the build and increment.
const CLIENT_NAME : &str = "artcraft";
@@ -40,12 +41,17 @@ pub async fn storyteller_activity_thread(
storyteller_creds_manager: StorytellerCredentialManager,
) -> ! {
let startup = Instant::now();
let app_session_token = AppSessionToken::generate();
info!("Session started at {:?} with token: {:?}", startup, app_session_token);
loop {
let res = polling_loop(
&app_env_configs,
&storyteller_creds_manager,
startup,
&artcraft_platform_info,
&app_session_token,
).await;
if let Err(err) = res {
error!("An error occurred: {:?}", err);
@@ -60,6 +66,7 @@ async fn polling_loop(
storyteller_creds_manager: &StorytellerCredentialManager,
startup: Instant,
artcraft_platform_info: &ArtcraftPlatformInfo,
app_session_token: &AppSessionToken,
) -> AnyhowResult<()> {
loop {
let creds = storyteller_creds_manager.get_credentials()?;
@@ -81,6 +88,7 @@ async fn polling_loop(
let time_since_startup = Instant::now().duration_since(startup);
let request = LogAppActiveUserRequest {
maybe_app_session_token: Some(app_session_token.clone()),
maybe_app_name: Some(CLIENT_NAME.to_string()),
maybe_app_version: Some(artcraft_platform_info.artcraft_version.clone()),
maybe_os_platform: Some(artcraft_platform_info.os_platform.as_str().to_owned()),
@@ -88,7 +96,7 @@ async fn polling_loop(
maybe_session_duration_seconds: Some(time_since_startup.as_secs()),
};
info!("Logging active user with storyteller.");
debug!("Logging active user with storyteller.");
let result = log_active_user(
&app_env_configs.storyteller_host,
@@ -1 +1,2 @@
pub mod upsert_analytics_app_active_user;
pub mod upsert_analytics_app_active_user;
pub mod upsert_analytics_app_session;
@@ -0,0 +1,80 @@
use crate::errors::mysql_error::MysqlError;
use crate::errors::subtypes::upsert_error::UpsertError;
use enums::common::payments_namespace::PaymentsNamespace;
use sqlx::mysql::MySqlArguments;
use sqlx::pool::PoolConnection;
use sqlx::query::Query;
use sqlx::MySql;
use tokens::tokens::app_session::AppSessionToken;
use tokens::tokens::users::UserToken;
pub struct UpsertAnalyticsAppSession<'a> {
pub app_session_token: &'a AppSessionToken,
// TODO: Swap PaymentsNamespace (artcraft,fakeyou,etc.) for an AnalyticsNamespace enum.
pub namespace: PaymentsNamespace,
pub user_token: &'a UserToken,
pub ip_address: &'a str,
pub app_version: Option<&'a str>,
pub os_platform: Option<&'a str>,
pub os_version: Option<&'a str>,
pub session_duration_seconds: Option<u64>,
}
impl <'a> UpsertAnalyticsAppSession<'a> {
fn query(&self) -> Query<MySql, MySqlArguments> {
sqlx::query!(
r#"
INSERT INTO analytics_app_sessions
SET
session_token = ?,
app_namespace = ?,
user_token = ?,
app_version = ?,
os_platform = ?,
os_version = ?,
session_duration_seconds = ?,
ip_address = ?,
measurement_count = measurement_count + 1,
first_active_at = NOW(),
last_active_at = NOW()
ON DUPLICATE KEY UPDATE
app_namespace = ?,
user_token = ?,
app_version = ?,
os_platform = ?,
os_version = ?,
session_duration_seconds = ?,
ip_address = ?,
measurement_count = measurement_count + 1,
last_active_at = NOW()
"#,
// Insert case
self.app_session_token.as_str(),
self.namespace.to_str(),
self.user_token.as_str(),
self.app_version,
self.os_platform,
self.os_version,
self.session_duration_seconds,
self.ip_address,
// Update case
self.namespace.to_str(),
self.user_token.as_str(),
self.app_version,
self.os_platform,
self.os_version,
self.session_duration_seconds,
self.ip_address,
)
}
pub async fn upsert_with_connection(
&self,
mysql_connection: &mut PoolConnection<MySql>
) -> Result<(), MysqlError<UpsertError>> {
let _query_result = self.query()
.execute(&mut **mysql_connection)
.await?;
Ok(())
}
}
@@ -48,6 +48,10 @@ pub enum TaskModelType {
Seedance10Lite,
#[serde(rename = "veo_2")]
Veo2,
#[serde(rename = "veo_3")]
Veo3,
#[serde(rename = "veo_3_fast")]
Veo3Fast,
// 3D Object generation models
#[serde(rename = "hunyuan_3d_2.0")]
@@ -83,6 +87,8 @@ impl TaskModelType {
Self::Kling21Master => "kling_2.1_master",
Self::Seedance10Lite => "seedance_1.0_lite",
Self::Veo2 => "veo_2",
Self::Veo3 => "veo_3",
Self::Veo3Fast => "veo_3_fast",
// 3D Object generation models
Self::Hunyuan3d2_0 => "hunyuan_3d_2.0",
Self::Hunyuan3d2_1 => "hunyuan_3d_2.1",
@@ -109,6 +115,8 @@ impl TaskModelType {
"kling_2.1_master" => Ok(Self::Kling21Master),
"seedance_1.0_lite" => Ok(Self::Seedance10Lite),
"veo_2" => Ok(Self::Veo2),
"veo_3" => Ok(Self::Veo3),
"veo_3_fast" => Ok(Self::Veo3Fast),
// 3D Object generation models
"hunyuan_3d_2.0" => Ok(Self::Hunyuan3d2_0),
"hunyuan_3d_2.1" => Ok(Self::Hunyuan3d2_1),
@@ -138,6 +146,8 @@ impl TaskModelType {
Self::Kling21Master,
Self::Seedance10Lite,
Self::Veo2,
Self::Veo3,
Self::Veo3Fast,
// 3D Object generation models
Self::Hunyuan3d2_0,
Self::Hunyuan3d2_1,
@@ -173,6 +183,8 @@ mod tests {
assert_serialization(TaskModelType::Kling21Master, "kling_2.1_master");
assert_serialization(TaskModelType::Seedance10Lite, "seedance_1.0_lite");
assert_serialization(TaskModelType::Veo2, "veo_2");
assert_serialization(TaskModelType::Veo3, "veo_3");
assert_serialization(TaskModelType::Veo3Fast, "veo_3_fast");
// 3D Object generation models
assert_serialization(TaskModelType::Hunyuan3d2_0, "hunyuan_3d_2.0");
assert_serialization(TaskModelType::Hunyuan3d2_1, "hunyuan_3d_2.1");
@@ -198,6 +210,8 @@ mod tests {
assert_eq!(TaskModelType::Kling21Master.to_str(), "kling_2.1_master");
assert_eq!(TaskModelType::Seedance10Lite.to_str(), "seedance_1.0_lite");
assert_eq!(TaskModelType::Veo2.to_str(), "veo_2");
assert_eq!(TaskModelType::Veo3.to_str(), "veo_3");
assert_eq!(TaskModelType::Veo3Fast.to_str(), "veo_3_fast");
// 3D Object generation models
assert_eq!(TaskModelType::Hunyuan3d2_0.to_str(), "hunyuan_3d_2.0");
assert_eq!(TaskModelType::Hunyuan3d2_1.to_str(), "hunyuan_3d_2.1");
@@ -223,6 +237,8 @@ mod tests {
assert_eq!(TaskModelType::from_str("kling_2.1_master").unwrap(), TaskModelType::Kling21Master);
assert_eq!(TaskModelType::from_str("seedance_1.0_lite").unwrap(), TaskModelType::Seedance10Lite);
assert_eq!(TaskModelType::from_str("veo_2").unwrap(), TaskModelType::Veo2);
assert_eq!(TaskModelType::from_str("veo_3").unwrap(), TaskModelType::Veo3);
assert_eq!(TaskModelType::from_str("veo_3_fast").unwrap(), TaskModelType::Veo3Fast);
// 3D Object generation models
assert_eq!(TaskModelType::from_str("hunyuan_3d_2.0").unwrap(), TaskModelType::Hunyuan3d2_0);
assert_eq!(TaskModelType::from_str("hunyuan_3d_2.1").unwrap(), TaskModelType::Hunyuan3d2_1);
@@ -231,7 +247,7 @@ mod tests {
#[test]
fn all_variants() {
let mut variants = TaskModelType::all_variants();
assert_eq!(variants.len(), 18);
assert_eq!(variants.len(), 20);
// Image models
assert_eq!(variants.pop_first(), Some(TaskModelType::Flux1Dev));
assert_eq!(variants.pop_first(), Some(TaskModelType::Flux1Schnell));
@@ -250,6 +266,8 @@ mod tests {
assert_eq!(variants.pop_first(), Some(TaskModelType::Kling21Master));
assert_eq!(variants.pop_first(), Some(TaskModelType::Seedance10Lite));
assert_eq!(variants.pop_first(), Some(TaskModelType::Veo2));
assert_eq!(variants.pop_first(), Some(TaskModelType::Veo3));
assert_eq!(variants.pop_first(), Some(TaskModelType::Veo3Fast));
// 3D Object generation models
assert_eq!(variants.pop_first(), Some(TaskModelType::Hunyuan3d2_0));
assert_eq!(variants.pop_first(), Some(TaskModelType::Hunyuan3d2_1));
@@ -23,6 +23,7 @@ use strum::EnumIter;
#[cfg_attr(test, derive(EnumIter, EnumCount))]
pub(crate) enum TokenPrefix {
AnonymousVisitorTracking, // AVTs are not stored as primary keys in any table, but an index in many tables.
AppSession, // NB: These are generated client-side (!)
AuditLog,
BatchGeneration,
BetaKey,
@@ -105,6 +106,7 @@ impl PrefixGenerator for TokenPrefix {
fn prefix(self) -> &'static str {
match self {
Self::AnonymousVisitorTracking => "avt_",
Self::AppSession => "app_session_",
Self::AuditLog => "audit_",
Self::BatchGeneration => "batch_g_",
Self::BetaKey => "beta_key_",
@@ -0,0 +1,17 @@
use std::fmt::Debug;
use serde::Deserialize;
use serde::Serialize;
use utoipa::ToSchema;
use crate::prefixes::TokenPrefix;
/// The primary key for users.
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize, Default, ToSchema)]
#[cfg_attr(feature = "database", derive(sqlx::Type))]
#[cfg_attr(feature = "database", sqlx(transparent))]
pub struct AppSessionToken(pub String);
impl_mysql_token_from_row!(AppSessionToken);
impl_string_token!(AppSessionToken);
impl_crockford_generator!(AppSessionToken, 32usize, TokenPrefix::AppSession, CrockfordMixed);
@@ -1,6 +1,7 @@
pub mod anonymous_visitor_tracking;
pub mod api_tokens_external;
pub mod api_tokens_internal;
pub mod app_session;
pub mod audit_logs;
pub mod batch_generations;
pub mod beta_keys;
@@ -5,11 +5,14 @@ use crate::state::server_state::ServerState;
use actix_helpers::extractors::get_request_user_agent::get_request_user_agent;
use actix_web::web::{Json, Query};
use actix_web::{web, HttpRequest};
use log::info;
use artcraft_api_defs::analytics::log_active_user::{LogAppActiveUserRequest, LogAppActiveUserResponse};
use enums::common::payments_namespace::PaymentsNamespace;
use errors::AnyhowResult;
use http_server_common::request::get_request_ip::get_request_ip;
use log::{info, warn};
use mysql_queries::queries::analytics_active_users::upsert_analytics_app_active_user::UpsertAnalyticsAppActiveUser;
use mysql_queries::queries::analytics_active_users::upsert_analytics_app_session::UpsertAnalyticsAppSession;
use tokens::tokens::app_session::AppSessionToken;
use utoipa::ToSchema;
const CLIENT_WAIT_FOR_RETRY_MILLIS: u64 = 1_000 * 60 * 1; // Every minute
@@ -100,6 +103,23 @@ pub async fn log_app_active_user_handler(
};
upsert.upsert_with_connection(&mut mysql_connection).await?;
if let Some(token) = request.maybe_app_session_token.as_ref() {
validate_app_session_token_format(token)?;
let upsert = UpsertAnalyticsAppSession{
app_session_token: token,
namespace: PaymentsNamespace::Artcraft,
user_token: &user_token,
ip_address: &ip_address,
app_version: app_version.as_deref(),
os_platform: request.maybe_os_platform.as_deref(),
os_version: request.maybe_os_version.as_deref(),
session_duration_seconds: request.maybe_session_duration_seconds,
};
upsert.upsert_with_connection(&mut mysql_connection).await?;
}
Ok(Json(LogAppActiveUserResponse {
success: true,
@@ -107,3 +127,11 @@ pub async fn log_app_active_user_handler(
}))
}
fn validate_app_session_token_format(app_session_token: &AppSessionToken) -> Result<(), CommonWebError> {
if !app_session_token.as_str().starts_with(AppSessionToken::token_prefix()) {
warn!("App session token has invalid prefix: {}", app_session_token.as_str());
return Err(CommonWebError::BadInputWithSimpleMessage("Invalid app session token format".to_string()));
}
Ok(())
}
@@ -11,7 +11,6 @@ import { useSignals } from "@preact/signals-react/runtime";
import { useEffect, useState } from "react";
import * as gpu from "detect-gpu";
import { UsersApi } from "~/Classes/ApiManager";
import { TurnOnGpu } from "~/pages/PageEnigma/TurnOnGpu";
import { PrecisionSelector } from "./comps/PrecisionSelector/PrecisionSelector";
import {
precisionSelectedValue,
@@ -22,7 +21,6 @@ import {
import { InstallSounds } from "~/pages/PageEnigma/InstallSounds";
import { PageEditor } from "~/pages/PageEnigma/PageEditor";
import { GalleryDragComponent } from "@storyteller/ui-gallery-modal";
import { LoadingDots } from "@storyteller/ui-loading";
import {
PricingModal,
CreditsModal,
@@ -105,13 +103,6 @@ export const PageEnigma = ({ sceneToken }: { sceneToken?: string }) => {
InstallSounds();
});
if (validGpu === "unknown") {
return <LoadingDots />;
}
if (validGpu === "error") {
return <TurnOnGpu />;
}
const currentReminderModalProps = actionReminderProps.value;
return (
@@ -1,210 +0,0 @@
import * as CopyToClipboard from "react-copy-to-clipboard";
import { useEffect, useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCopy } from "@fortawesome/pro-solid-svg-icons";
export const TurnOnGpu = () => {
const [copied1, setCopied1] = useState(false);
const [copied2, setCopied2] = useState(false);
useEffect(() => {
if (copied1) {
setTimeout(() => setCopied1(false), 2000);
}
}, [copied1]);
useEffect(() => {
if (copied2) {
setTimeout(() => setCopied2(false), 2000);
}
}, [copied2]);
return (
<div className="fixed inset-0 h-screen overflow-y-auto p-16">
<div>
Hardware acceleration is <b>required</b> for StoryTeller Studio to run
properly. To check if this is enabled, please follow the steps for your
browser below.
</div>
<div className="my-4">
<b>Google Chrome: </b>
</div>
<ol>
<li>
<div>
Click the three stacked dots in upper right of the browser window.
</div>
</li>
<li>
<div>
Click the arrow next to <em>&quot;Advanced&quot;</em> and select{" "}
<em>&quot;System.&quot;</em>
</div>
</li>
<li>
<div>
Make sure the box next to{" "}
<em>&quot;Use hardware acceleration when available&quot;</em> is
toggled on.
</div>
</li>
<li>
<div>
Close and restart the browser for the change to take effect.
</div>
</li>
<ul>
<li>
<div>
You can also copy and paste this link{" "}
<CopyToClipboard.CopyToClipboard
text="chrome://settings/system"
onCopy={() => setCopied1(true)}
>
<span className="text-blue">
chrome://settings/system&nbsp;
{copied1 ? (
<span style={{ color: "red" }}>Copied.</span>
) : (
<span className="text-sm">
<FontAwesomeIcon icon={faCopy} />
</span>
)}
&nbsp;
</span>
</CopyToClipboard.CopyToClipboard>
into the address bar. This will bring you right to the system
settings.
</div>
</li>
</ul>
</ol>
<div className="my-4">
<b>Firefox:</b>
</div>
<ol>
<li>
<div>
Click on <em>&quot;Firefox&quot;</em> then{" "}
<em>&quot;Preferences.&quot;</em>
</div>
</li>
<li>
<div>
In the&nbsp;<em>&quot;General&quot;</em>&nbsp;panel, go to the&nbsp;
<em>&quot;Performance&quot; </em>section.
</div>
</li>
<li>
<div>
Uncheck the box next to&nbsp;
<em>&quot;Use recommended performance settings&quot;</em> and make
sure the box next to{" "}
<em>&quot;Use hardware acceleration when available&quot;</em> is
selected.
</div>
</li>
<li>
<div>
Close and restart the browser for the change to take effect.
</div>
</li>
<ul>
<li>
<div>
If you need help, please follow this link:{" "}
<a
href="https://support.mozilla.org/en-US/kb/performance-settings?as=u&amp;utm_source=inproduct"
target="_blank"
rel="noreferrer"
className="text-blue underline"
>
https://support.mozilla.org/en-US/kb/performance-settings?as=u&amp;utm_source=inproduct
</a>
</div>
</li>
</ul>
</ol>
<div className="my-4">
<b>Microsoft Edge:</b>
</div>
<ol>
<li>
<div>
Click the &quot;3 dots&quot; menu icon in the upper right of the
browser window
</div>
</li>
<li>
<div>
Click on&nbsp;<em>&quot;Settings&quot;</em> then{" "}
<em>&quot;System&quot;</em> on the left side.
</div>
</li>
<li>
<div>
Toggle on{" "}
<em>&quot;Use hardware acceleration when available&quot;</em> if
this is not enabled.
</div>
</li>
<li>
<div>
Close and restart the browser for the change to take effect.
</div>
</li>
<ul>
<li>
<div>
You can also copy and paste this link{" "}
<CopyToClipboard.CopyToClipboard
text="edge://settings/system"
onCopy={() => setCopied2(true)}
>
<span className="text-blue">
edge://settings/system&nbsp;
{copied2 ? (
<span style={{ color: "red" }}>Copied.</span>
) : (
<span className="text-sm">
<FontAwesomeIcon icon={faCopy} />
</span>
)}
&nbsp;
</span>
</CopyToClipboard.CopyToClipboard>
into the address bar. This will bring you right to the system
settings.
</div>
</li>
</ul>
</ol>
<div className="my-4">
<b>Safari (Mac Only)</b>
</div>
<div>
As of macOS Catalina (version 10.15), Hardware acceleration is
automatically enabled, and there is no way turn this on and off. If you
are using an older version of MacOS, please use the steps below:
</div>
<ol>
<li>
<div>
Click on the &quot;Safari&quot; then &quot;Settings&quot; &gt;{" "}
</div>
</li>
<li>
<div>
Click &quot;Advanced&quot; on the top right of the Settings window
and ensure &quot;Use hardware acceleration&quot; is enabled
</div>
</li>
</ol>
<div>
If you are still having an issue with enabling hardware acceleration, we
recommend you update your graphics card drivers, if possible. If these
updates do not help, you may have to consider upgrading your graphics
card or playing on a different device.
</div>
</div>
);
};
@@ -62,4 +62,28 @@ export const VIDEO_MODELS: VideoModel[] = [
startFrame: true,
endFrame: false,
}),
new VideoModel({
id: "veo_3",
tauriId: "veo_3",
fullName: "Google Veo 3",
category: "video",
creator: ModelCreator.Google,
selectorName: "Google Veo 3",
selectorDescription: "Slow, high-quality model",
selectorBadges: ["2 min."],
startFrame: true,
endFrame: false,
}),
new VideoModel({
id: "veo_3_fast",
tauriId: "veo_3_fast",
fullName: "Google Veo 3 Fast",
category: "video",
creator: ModelCreator.Google,
selectorName: "Google Veo 3 Fast",
selectorDescription: "High-quality model (faster)",
selectorBadges: ["2 min."],
startFrame: true,
endFrame: false,
}),
];