Compare commits

..
Author SHA1 Message Date
Brandon Thomas eef1ed6cfb more columns 2026-01-21 00:50:54 -05:00
Brandon Thomas 4bee9d9715 add new analytics columns
need to know where to best spend our engineering effort
2026-01-21 00:50:53 -05:00
248 changed files with 1627 additions and 6691 deletions
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n token as `token: tokens::tokens::user_subscriptions::UserSubscriptionToken`,\n user_token as `user_token: tokens::tokens::users::UserToken`,\n subscription_namespace as `subscription_namespace: enums::common::payments_namespace::PaymentsNamespace`,\n subscription_product_slug,\n maybe_stripe_customer_id,\n maybe_stripe_product_id,\n maybe_stripe_subscription_id,\n maybe_stripe_subscription_status as `maybe_stripe_subscription_status: enums::common::stripe_subscription_status::StripeSubscriptionStatus`,\n maybe_stripe_invoice_is_paid,\n subscription_start_at,\n subscription_expires_at,\n current_billing_period_end_at,\n maybe_cancel_at,\n maybe_canceled_at\n\nFROM user_subscriptions\n\nWHERE\n user_token = ?\n AND subscription_namespace = ?\n AND maybe_stripe_customer_id IS NOT NULL\n AND maybe_stripe_subscription_id IS NOT NULL\n AND maybe_stripe_subscription_status IS NOT NULL\n AND maybe_stripe_subscription_status NOT IN ('canceled', 'incomplete_expired')\n AND subscription_expires_at > CURRENT_TIMESTAMP\n ORDER BY id ASC\n LIMIT 1\n ",
"query": "\nSELECT\n token as `token: tokens::tokens::user_subscriptions::UserSubscriptionToken`,\n user_token as `user_token: tokens::tokens::users::UserToken`,\n subscription_namespace as `subscription_namespace: enums::common::payments_namespace::PaymentsNamespace`,\n subscription_product_slug,\n maybe_stripe_customer_id,\n maybe_stripe_product_id,\n maybe_stripe_subscription_id,\n maybe_stripe_subscription_status,\n maybe_stripe_invoice_is_paid,\n subscription_start_at,\n subscription_expires_at,\n current_billing_period_end_at,\n maybe_cancel_at,\n maybe_canceled_at\n\nFROM user_subscriptions\n\nWHERE\n user_token = ?\n AND subscription_namespace = ?\n AND maybe_stripe_customer_id IS NOT NULL\n AND maybe_stripe_subscription_id IS NOT NULL\n AND maybe_stripe_subscription_status IS NOT NULL\n AND maybe_stripe_subscription_status NOT IN ('canceled', 'incomplete_expired')\n AND subscription_expires_at > CURRENT_TIMESTAMP\n ORDER BY id ASC\n LIMIT 1\n ",
"describe": {
"columns": [
{
@@ -75,7 +75,7 @@
},
{
"ordinal": 7,
"name": "maybe_stripe_subscription_status: enums::common::stripe_subscription_status::StripeSubscriptionStatus",
"name": "maybe_stripe_subscription_status",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
@@ -164,5 +164,5 @@
true
]
},
"hash": "dbf85f29c614d243a50e4259218f21c446c6ab17de35c8a8da08fe02822e8f58"
"hash": "1e08676857e999a58d3c85474c2a2acec38a413e53ca4207aade690e0bfffe25"
}
@@ -1,12 +0,0 @@
{
"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 total_generation_count = ?,\n image_generation_count = ?,\n video_generation_count = ?,\n object_generation_count = ?,\n text_to_image_count = ?,\n image_to_image_count = ?,\n text_to_video_count = ?,\n image_to_video_count = ?,\n text_to_object_count = ?,\n image_to_object_count = ?,\n image_page_prompt_count = ?,\n video_page_prompt_count = ?,\n edit_page_prompt_count = ?,\n stage_page_prompt_count = ?,\n object_page_prompt_count = ?,\n other_page_prompt_count = ?,\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 total_generation_count = ?,\n image_generation_count = ?,\n video_generation_count = ?,\n object_generation_count = ?,\n text_to_image_count = ?,\n image_to_image_count = ?,\n text_to_video_count = ?,\n image_to_video_count = ?,\n text_to_object_count = ?,\n image_to_object_count = ?,\n image_page_prompt_count = ?,\n video_page_prompt_count = ?,\n edit_page_prompt_count = ?,\n stage_page_prompt_count = ?,\n object_page_prompt_count = ?,\n other_page_prompt_count = ?,\n last_active_at = NOW()\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 47
},
"nullable": []
},
"hash": "4b3d363a4f5a088d2040cedd010e66964ea4d5e3b3a94aaad79bc122ea165715"
}
@@ -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"
}
Generated
+14 -1
View File
@@ -652,7 +652,7 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "artcraft"
version = "0.5.0"
version = "0.3.0"
dependencies = [
"anyhow",
"artcraft_api_defs",
@@ -10215,6 +10215,19 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "shared_service_components"
version = "0.0.1"
dependencies = [
"anyhow",
"errors",
"log",
"openai_sora_client",
"r2d2_redis",
"reqwest 0.12.23",
"workspace-build-acceleration",
]
[[package]]
name = "shlex"
version = "1.3.0"
+1
View File
@@ -80,6 +80,7 @@ members = [
"crates/lib/migration",
"crates/lib/password",
"crates/lib/server_environment",
"crates/lib/shared_service_components",
"crates/lib/sqlx_mysql_helpers",
"crates/lib/storyteller_root",
"crates/lib/subprocess_common",
+8 -15
View File
@@ -1,17 +1,3 @@
<p align="center">
<video src="https://github.com/user-attachments/assets/b4e24c27-d87d-4fd1-8599-dc0d0b8af48d" width="100%" autoplay="true" loop controls>
</p>
<p align="center">The IDE for artists.</p>
<p align="center">
<a href="https://discord.gg/artcraft"><img alt="Discord" src="https://img.shields.io/discord/1359579021108842617?style=for-the-badge&label=discord&color=ffffff&logo=discord&logoColor=ffffff" /></a>
<a href="https://www.youtube.com/@OfficialArtCraftStudios"><img alt="YouTube" src="https://img.shields.io/youtube/channel/subscribers/UCdjY4VG0ntoGwFsKZO4sVWA?style=for-the-badge&logo=YouTube" /></a>
<a href="https://x.com/intent/follow?screen_name=get_artcraft"><img alt="X" src="https://img.shields.io/twitter/follow/get_artcraft?style=for-the-badge&label=follow&logo=x&logoColor=ffffff&color=ffffff" /></a>
<a href="https://www.linkedin.com/company/artcraft-ai"><img alt="LinkedIn" src="https://img.shields.io/badge/linkedin--0A66C2?style=for-the-badge"></a>
</p>
---
ArtCraft
========
ArtCraft is the IDE for interactive AI image and video creation.
@@ -55,7 +41,7 @@ We haven't abandoned text-to-asset generation for quick prototyping and ideation
| Provider | Features |
|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Artcraft | Nano Banana, Nano Banana Pro, GPT-Image-1 / 1.5, Seedream 4 / 4.5, Flux 1.1 / Kontext, Veo 2 / 3 / 3.1, Kling 1.6 / 2.1 / 2.5 / 2.6, Seedance, Sora 2 / Pro, Hunyuan 3d 2 / 3 |
| Artcraft | Nano Banana, Nano Banana Pro, GPT-Image-1 / 1.5, Seedream 4 / 4.5, Flux 1.1 / Kontext, Veo 2 / 3 / 3.1, Kling 1.6 / 2.1 / 2.5 / 2.6, Seedance, Sora 2 / Pro, Hunyuan 3d |
| Grok | Grok Imagine, Grok Video |
| Midjourney | Image Gen (all versions) |
| Sora | Sora 1, Sora 2, GPT-Image-1 |
@@ -80,3 +66,10 @@ those providers, for example: OpenArt, FreePik, etc.
- [license](./LICENSE.md)
- [roadmap](./ROADMAP.md)
## Social
[![discord](https://raw.githubusercontent.com/CLorant/readme-social-icons/main/large/colored/discord.svg)](https://discord.gg/artcraft)
[![youtube](https://raw.githubusercontent.com/CLorant/readme-social-icons/main/large/colored/youtube.svg)](https://www.youtube.com/@OfficialArtCraftStudios)
[![x](https://raw.githubusercontent.com/CLorant/readme-social-icons/main/large/colored/linkedin.svg)](https://www.linkedin.com/company/artcraft-ai)
[![x](https://raw.githubusercontent.com/CLorant/readme-social-icons/main/large/colored/twitter-x.svg)](https://x.com/get_artcraft)
+6
View File
@@ -0,0 +1,6 @@
# Secrets
db_secrets.toml
# Python Virtual Env
python/
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env python
# FIXME: Needs packages 'toml' and 'mysql-connector-python'. Put in requirements.txt.
import time
import toml
import mysql.connector
DB_SECRETS_FILE = 'db_secrets.toml'
# TODO: Share db-related setup when more scripts are added.
def read_database_secrets(secrets_file = DB_SECRETS_FILE):
secrets = None
with open(secrets_file, "r") as f:
secrets = toml.load(f)
required_keys = ['database', 'hostname', 'port', 'password', 'username']
for key in required_keys:
if key not in secrets:
raise Exception(f"Required key '#{key}' not present")
return secrets
def connect_to_mysql():
secrets = read_database_secrets()
connection = mysql.connector.connect(
host = secrets['hostname'],
port = secrets['port'],
user = secrets['username'],
password = secrets['password'],
database = secrets['database'],
)
return connection
connection = connect_to_mysql()
cursor = connection.cursor()
SAFE_ID = 76000000 # Reasonably completed on 2022-06-24
LIMIT = 100000
#def get_record_count():
# query = f"SELECT COUNT(*) FROM tts_inference_jobs"
# cursor.execute(query)
# result = cursor.fetchall()
# print(result)
# return result[0]
#
##total_count = get_record_count()
#total_count = 75082057
total_count = 21925602
def get_first_record(cursor):
query = f"select * from tts_inference_jobs order by id limit 1"
cursor.execute(query)
result = cursor.fetchall()
print(result)
return result
total_deleted = 0
keep_deleting = True
while keep_deleting:
query = f"DELETE FROM tts_inference_jobs WHERE id < {SAFE_ID} ORDER BY ID ASC LIMIT {LIMIT}"
query = f"DELETE FROM tts_inference_jobs WHERE status = 'complete_success' ORDER BY ID ASC LIMIT {LIMIT}"
#print(query)
cursor.execute(query)
connection.commit() # NB: Python does not autocommit changes!
#result = cursor.fetchall()
count = cursor.rowcount
total_deleted += count
percent_deleted = (total_deleted / total_count) * 100.0
print(f'Deleted {count}; total deleted = {total_deleted} ({percent_deleted:.4f})')
if count < LIMIT - 1:
keep_deleting = False
time.sleep(0.5)
#record = get_first_record(cursor)
#time.sleep(2)
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env python
# FIXME: Needs packages 'toml' and 'mysql-connector-python'. Put in requirements.txt.
import time
import toml
import mysql.connector
DB_SECRETS_FILE = 'db_secrets.toml'
# TODO: Share db-related setup when more scripts are added.
def read_database_secrets(secrets_file = DB_SECRETS_FILE):
secrets = None
with open(secrets_file, "r") as f:
secrets = toml.load(f)
required_keys = ['database', 'hostname', 'port', 'password', 'username']
for key in required_keys:
if key not in secrets:
raise Exception(f"Required key '#{key}' not present")
return secrets
def connect_to_mysql():
secrets = read_database_secrets()
connection = mysql.connector.connect(
host = secrets['hostname'],
port = secrets['port'],
user = secrets['username'],
password = secrets['password'],
database = secrets['database'],
)
return connection
connection = connect_to_mysql()
cursor = connection.cursor()
SAFE_ID = 76000000 # Reasonably completed on 2022-06-24
LIMIT = 100000
#def get_record_count():
# query = f"SELECT COUNT(*) FROM tts_inference_jobs"
# cursor.execute(query)
# result = cursor.fetchall()
# print(result)
# return result[0]
#
##total_count = get_record_count()
#total_count = 75082057
total_count = 21925602
def get_user_tokens(cursor):
query = f"""
SELECT
r.maybe_creator_user_token, r.creator_ip_address
FROM
tts_results as r
LEFT OUTER JOIN
users as u
ON
r.maybe_creator_user_token = u.token
WHERE
r.raw_inference_text LIKE '%spidermant%'
AND r.created_at > ( CURDATE() - INTERVAL 1 HOUR )
"""
cursor.execute(query)
results = cursor.fetchall()
user_tokens = [row[0] for row in results]
user_tokens = [token for token in user_tokens if token is not None]
user_tokens = set(user_tokens)
ip_addresses = [row[1] for row in results]
ip_addresses = [ip for ip in ip_addresses if ip is not None]
ip_addresses = set(ip_addresses)
return (user_tokens, ip_addresses)
(user_tokens, ip_addresses) = get_user_tokens(cursor)
print(user_tokens)
print(len(user_tokens))
print(len(ip_addresses))
with open('101_user_tokens.txt', 'w') as f:
for user_token in user_tokens:
f.write(user_token)
f.write("\n")
with open('101_ip_addresses.txt', 'w') as f:
for ip in ip_addresses:
f.write(ip)
f.write("\n")
+3
View File
@@ -0,0 +1,3 @@
mysql-connector-python==8.0.32
protobuf==3.20.3
toml==0.10.2
@@ -2,109 +2,32 @@ use serde_derive::{Deserialize, Serialize};
use tokens::tokens::app_session::AppSessionToken;
use utoipa::ToSchema;
/// This is used in Artcraft versions 0.0.1 (several) through 0.3.0
/// Clients to this version do not send generation count data (even if the endpoint supports it).
pub const LOG_ACTIVE_USER_V1_PATH: &str = "/v1/analytics/active_user";
pub const LOG_ACTIVE_USER_PATH: &str = "/v1/analytics/active_user";
/// This is used in Artcraft versions 0.4.0 and later.
pub const LOG_ACTIVE_USER_V2_PATH: &str = "/v1/analytics/active_user_v2";
#[derive(Serialize, Deserialize, ToSchema, Debug, Default)]
#[derive(Serialize, Deserialize, ToSchema, Debug)]
pub struct LogAppActiveUserRequest {
/// Clientside-generated session token.
/// If present, it'll be validated against an expected format.
#[serde(skip_serializing_if = "Option::is_none")]
pub maybe_app_session_token: Option<AppSessionToken>,
/// An override for the application platform/OS (windows, mac, linux).
#[serde(skip_serializing_if = "Option::is_none")]
pub maybe_os_platform: Option<String>,
/// An override for the version of the OS (e.g. 10.15.7, 11, 22.04).
#[serde(skip_serializing_if = "Option::is_none")]
pub maybe_os_version: Option<String>,
/// An override for the application name.
/// If set together with `maybe_app_version`, the two will be
/// If set together with `maybe_app_version`, the two will be
/// concatenated as `{maybe_app_name}/{maybe_app_version}`.
#[serde(skip_serializing_if = "Option::is_none")]
pub maybe_app_name: Option<String>,
/// An override for the application version.
/// If set together with `maybe_app_name`, the two will be
/// If set together with `maybe_app_name`, the two will be
/// concatenated as `{maybe_app_name}/{maybe_app_version}`.
#[serde(skip_serializing_if = "Option::is_none")]
pub maybe_app_version: Option<String>,
/// How long the user has been active in the app, in seconds.
#[serde(skip_serializing_if = "Option::is_none")]
pub maybe_session_duration_seconds: Option<u64>,
// ========== GENERATION COUNTS ==========
/// Number of items generated across all types.
#[serde(skip_serializing_if = "Option::is_none")]
pub total_generation_count: Option<u16>,
/// Number of images generated.
#[serde(skip_serializing_if = "Option::is_none")]
pub image_generation_count: Option<u16>,
/// Number of videos generated.
#[serde(skip_serializing_if = "Option::is_none")]
pub video_generation_count: Option<u16>,
/// Number of objects generated (meshes, etc.).
#[serde(skip_serializing_if = "Option::is_none")]
pub object_generation_count: Option<u16>,
/// Number of images generated with text to image.
#[serde(skip_serializing_if = "Option::is_none")]
pub text_to_image_count: Option<u16>,
/// Number of images generated with image to image (image edits).
#[serde(skip_serializing_if = "Option::is_none")]
pub image_to_image_count: Option<u16>,
/// Number of videos generated with text to video.
#[serde(skip_serializing_if = "Option::is_none")]
pub text_to_video_count: Option<u16>,
/// Number of videos generated with image to video.
#[serde(skip_serializing_if = "Option::is_none")]
pub image_to_video_count: Option<u16>,
/// Number of objects generated with text to object.
#[serde(skip_serializing_if = "Option::is_none")]
pub text_to_object_count: Option<u16>,
/// Number of objects generated with image to object.
#[serde(skip_serializing_if = "Option::is_none")]
pub image_to_object_count: Option<u16>,
/// Number of prompts from the image page.
#[serde(skip_serializing_if = "Option::is_none")]
pub image_page_prompt_count: Option<u16>,
/// Number of prompts from the video page.
#[serde(skip_serializing_if = "Option::is_none")]
pub video_page_prompt_count: Option<u16>,
/// Number of prompts from the edit page.
#[serde(skip_serializing_if = "Option::is_none")]
pub edit_page_prompt_count: Option<u16>,
/// Number of prompts from the stage page.
#[serde(skip_serializing_if = "Option::is_none")]
pub stage_page_prompt_count: Option<u16>,
/// Number of prompts from the object page.
#[serde(skip_serializing_if = "Option::is_none")]
pub object_page_prompt_count: Option<u16>,
/// Number of prompts from other pages.
#[serde(skip_serializing_if = "Option::is_none")]
pub other_page_prompt_count: Option<u16>,
}
#[derive(Serialize, Deserialize, ToSchema)]
@@ -2,7 +2,7 @@ use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_query_string_post_request::basic_query_string_post_request;
use artcraft_api_defs::analytics::log_active_user::{LogAppActiveUserRequest, LogAppActiveUserResponse, LOG_ACTIVE_USER_V1_PATH};
use artcraft_api_defs::analytics::log_active_user::{LogAppActiveUserRequest, LogAppActiveUserResponse, LOG_ACTIVE_USER_PATH};
use std::collections::HashMap;
pub async fn log_active_user(
@@ -41,7 +41,7 @@ pub async fn log_active_user(
Ok(basic_query_string_post_request(
api_host,
LOG_ACTIVE_USER_V1_PATH,
LOG_ACTIVE_USER_PATH,
maybe_creds,
&query,
).await?)
@@ -1,18 +0,0 @@
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::analytics::log_active_user::{LogAppActiveUserRequest, LogAppActiveUserResponse, LOG_ACTIVE_USER_V2_PATH};
pub async fn log_active_user_v2(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: LogAppActiveUserRequest,
) -> Result<LogAppActiveUserResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
LOG_ACTIVE_USER_V2_PATH,
maybe_creds,
&request,
).await?)
}
@@ -1,2 +1 @@
pub mod log_active_user;
pub mod log_active_user_v2;
pub mod log_active_user;
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "artcraft"
version = "0.5.0"
version = "0.3.0"
description = "Artcraft"
authors = ["you"]
license = ""
@@ -1,8 +1,8 @@
use serde_derive::{Deserialize, Serialize};
/// This is a comprehensive list of common aspect ratios you can specify when enqueuing a generation.
/// This is a comprehensive list of common aspect ratios.
/// Not every model will support every aspect ratio.
/// In the case a model doesn't support the aspect ratio, gracefully pick the nearest option.
/// In the case a model doesn't support the aspect ratio, pick the nearest option.
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CommonAspectRatio {
@@ -1,2 +1 @@
pub mod aspect_ratio;
pub mod resolution;
pub mod aspect_ratio;
@@ -1,17 +0,0 @@
use serde_derive::{Deserialize, Serialize};
/// This is a comprehensive list of common resolutions you can specify when enqueuing a generation.
/// Not every model will support every resolution
/// In the case a model doesn't support the resolution, gracefully pick the nearest option.
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CommonResolution {
/// Models: Nano Banana Pro
OneK,
/// Models: Nano Banana Pro
TwoK,
/// Models: (None)
ThreeK,
/// Models: Nano Banana Pro
FourK,
}
@@ -1 +0,0 @@
pub mod resolution_to_artcraft_nano_banana_pro;
@@ -1,12 +0,0 @@
use crate::core::api_adapters::resolution::common_resolution::CommonResolution;
use artcraft_api_defs::generate::image::multi_function::nano_banana_pro_multi_function_image_gen::NanoBananaProMultiFunctionImageGenImageResolution;
pub fn resolution_to_artcraft_nano_banana_pro(resolution: CommonResolution) -> NanoBananaProMultiFunctionImageGenImageResolution {
match resolution {
CommonResolution::OneK => NanoBananaProMultiFunctionImageGenImageResolution::OneK,
CommonResolution::TwoK => NanoBananaProMultiFunctionImageGenImageResolution::TwoK,
// NB: ThreeK doesn't exist in Nano Banana Pro, use FourK as nearest higher resolution
CommonResolution::ThreeK => NanoBananaProMultiFunctionImageGenImageResolution::FourK,
CommonResolution::FourK => NanoBananaProMultiFunctionImageGenImageResolution::FourK,
}
}
@@ -1,2 +0,0 @@
pub mod common_resolution;
pub mod convert;
@@ -18,9 +18,6 @@ pub struct AppPreferencesPayload {
/// Play sounds on events.
pub play_sounds: bool,
/// Key pointing to file; defined in the frontend code.
pub delete_file_sound: Option<String>,
/// Key pointing to file; defined in the frontend code.
/// Defined for enqueue since image enqueue can be async
pub enqueue_success_sound: Option<String>,
@@ -64,7 +61,6 @@ async fn get_prefs(app_prefs: &AppPreferencesManager) -> AnyhowResult<AppPrefere
Ok(AppPreferencesPayload {
preferred_download_directory: prefs.preferred_download_directory,
play_sounds: prefs.play_sounds,
delete_file_sound: prefs.delete_file_sound,
enqueue_success_sound: prefs.enqueue_success_sound,
enqueue_failure_sound: prefs.enqueue_failure_sound,
generation_success_sound: prefs.generation_success_sound,
@@ -28,7 +28,6 @@ pub enum ValueType {
pub enum PreferenceName {
PreferredDownloadDirectory,
PlaySounds,
DeleteFileSound,
EnqueueSuccessSound,
EnqueueFailureSound,
GenerationSuccessSound,
@@ -86,11 +85,6 @@ async fn update_prefs(
return Err(anyhow!("Invalid value: {:?}", request.value)),
}
}
PreferenceName::DeleteFileSound => {
prefs.delete_file_sound = request.value
.map(|val| string_value(&val))
.transpose()?;
}
PreferenceName::EnqueueSuccessSound => {
prefs.enqueue_success_sound = request.value
.map(|val| string_value(&val))
@@ -12,8 +12,6 @@ use crate::core::events::generation_events::generation_enqueue_failure_event::Ge
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::core::events::sendable_event_trait::SendableEvent;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_type::{ArtcraftUsagePage, ArtcraftUsageType};
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
@@ -99,7 +97,6 @@ pub async fn enqueue_image_bg_removal_command(
request: EnqueueImageBgRemovalCommand,
app_data_root: State<'_, AppDataRoot>,
app_env_configs: State<'_, AppEnvConfigs>,
artcraft_usage_tracker: State<'_, ArtcraftUsageTracker>,
provider_priority_store: State<'_, ProviderPriorityStore>,
task_database: State<'_, TaskDatabase>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
@@ -112,7 +109,6 @@ pub async fn enqueue_image_bg_removal_command(
&app,
&app_data_root,
&app_env_configs,
&artcraft_usage_tracker,
&provider_priority_store,
&task_database,
&storyteller_creds_manager,
@@ -161,7 +157,6 @@ pub async fn handle_request(
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
artcraft_usage_tracker: &ArtcraftUsageTracker,
provider_priority_store: &ProviderPriorityStore,
task_database: &TaskDatabase,
storyteller_creds_manager: &StorytellerCredentialManager,
@@ -190,11 +185,6 @@ pub async fn handle_request(
error!("Failed to create task in database: {:?}", err);
// NB: Fail open, but find a way to flag this.
}
if let Err(err) = artcraft_usage_tracker.record_image_generation(1, ArtcraftUsageType::ImageToResult, ArtcraftUsagePage::EditPage) {
// NB: Fail open.
warn!("Failed to report usage: {:?}", err);
}
Ok(success_event)
}
@@ -43,8 +43,6 @@ use storyteller_client::error::storyteller_error::StorytellerError;
use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, Manager, State};
use tokens::tokens::media_files::MediaFileToken;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_type::{ArtcraftUsagePage, ArtcraftUsageType};
/// This is used in the Tauri command bridge.
/// Don't change the serializations without coordinating with the frontend.
@@ -206,7 +204,6 @@ pub async fn enqueue_edit_image_command(
request: EnqueueEditImageCommand,
app_data_root: State<'_, AppDataRoot>,
app_env_configs: State<'_, AppEnvConfigs>,
artcraft_usage_tracker: State<'_, ArtcraftUsageTracker>,
provider_priority_store: State<'_, ProviderPriorityStore>,
task_database: State<'_, TaskDatabase>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
@@ -222,7 +219,6 @@ pub async fn enqueue_edit_image_command(
&app,
&app_data_root,
&app_env_configs,
&artcraft_usage_tracker,
&provider_priority_store,
&task_database,
&storyteller_creds_manager,
@@ -273,7 +269,6 @@ pub async fn handle_request(
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
artcraft_usage_tracker: &ArtcraftUsageTracker,
provider_priority_store: &ProviderPriorityStore,
task_database: &TaskDatabase,
storyteller_creds_manager: &StorytellerCredentialManager,
@@ -347,13 +342,6 @@ pub async fn handle_request(
error!("Failed to create task in database: {:?}", err);
// NB: Fail open, but find a way to flag this.
}
// TODO: We don't know that this is strictly the edit page. It could be the stage. It might also be nice to distinguish inpainting.
let num_images = request.image_count.map(|count| count as u16).unwrap_or(1);
if let Err(err) = artcraft_usage_tracker.record_image_generation(num_images, ArtcraftUsageType::ImageToResult, ArtcraftUsagePage::EditPage) {
// NB: Fail open.
warn!("Failed to report usage: {:?}", err);
}
Ok(success_event)
}
@@ -41,8 +41,6 @@ use storyteller_client::error::storyteller_error::StorytellerError;
use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, Manager, State};
use tokens::tokens::media_files::MediaFileToken;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_type::{ArtcraftUsagePage, ArtcraftUsageType};
#[derive(Deserialize, Debug, Copy, Clone)]
#[serde(rename_all = "snake_case")]
@@ -157,7 +155,6 @@ pub async fn enqueue_image_inpaint_command(
request: EnqueueInpaintImageCommand,
app_data_root: State<'_, AppDataRoot>,
app_env_configs: State<'_, AppEnvConfigs>,
artcraft_usage_tracker: State<'_, ArtcraftUsageTracker>,
provider_priority_store: State<'_, ProviderPriorityStore>,
task_database: State<'_, TaskDatabase>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
@@ -173,7 +170,6 @@ pub async fn enqueue_image_inpaint_command(
&app,
&app_data_root,
&app_env_configs,
&artcraft_usage_tracker,
&provider_priority_store,
&task_database,
&storyteller_creds_manager,
@@ -224,7 +220,6 @@ pub async fn handle_request(
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
artcraft_usage_tracker: &ArtcraftUsageTracker,
provider_priority_store: &ProviderPriorityStore,
task_database: &TaskDatabase,
storyteller_creds_manager: &StorytellerCredentialManager,
@@ -277,13 +272,6 @@ pub async fn handle_request(
// NB: Fail open, but find a way to flag this.
}
// TODO: We don't know that this is strictly the edit page. It could be the stage. It might also be nice to distinguish inpainting.
let num_images = request.image_count.map(|count| count as u16).unwrap_or(1);
if let Err(err) = artcraft_usage_tracker.record_image_generation(num_images, ArtcraftUsageType::ImageToResult, ArtcraftUsagePage::EditPage) {
// NB: Fail open.
warn!("Failed to report usage: {:?}", err);
}
Ok(success_event)
}
@@ -30,8 +30,6 @@ use serde::{Deserialize, Serialize};
use sqlite_tasks::queries::create_task::{create_task, CreateTaskArgs};
use tauri::{AppHandle, State};
use tokens::tokens::media_files::MediaFileToken;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_type::{ArtcraftUsagePage, ArtcraftUsageType};
/// This is used in the Tauri command bridge.
/// Don't change the serializations without coordinating with the frontend.
@@ -99,7 +97,6 @@ pub async fn enqueue_image_to_gaussian_command(
app: AppHandle,
app_data_root: State<'_, AppDataRoot>,
app_env_configs: State<'_, AppEnvConfigs>,
artcraft_usage_tracker: State<'_, ArtcraftUsageTracker>,
task_database: State<'_, TaskDatabase>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
worldlabs_creds_manager: State<'_, WorldlabsCredentialManager>,
@@ -111,7 +108,6 @@ pub async fn enqueue_image_to_gaussian_command(
request,
&app,
&app_data_root,
&artcraft_usage_tracker,
&task_database,
&storyteller_creds_manager,
&worldlabs_creds_manager,
@@ -172,7 +168,6 @@ pub async fn handle_request(
request: EnqueueImageToGaussianRequest,
app: &AppHandle,
app_data_root: &AppDataRoot,
artcraft_usage_tracker: &ArtcraftUsageTracker,
task_database: &TaskDatabase,
storyteller_creds_manager: &StorytellerCredentialManager,
worldlabs_creds_manager: &WorldlabsCredentialManager,
@@ -206,12 +201,7 @@ pub async fn handle_request(
error!("Failed to create task in database: {:?}", err);
// NB: Fail open, but find a way to flag this.
}
if let Err(err) = artcraft_usage_tracker.record_object_generation(1, ArtcraftUsageType::ImageToResult, ArtcraftUsagePage::OtherPage) {
// NB: Fail open.
warn!("Failed to report usage: {:?}", err);
}
Ok(success_event)
}
@@ -19,8 +19,6 @@ use log::{error, info, warn};
use serde_derive::{Deserialize, Serialize};
use tauri::{AppHandle, State};
use tokens::tokens::media_files::MediaFileToken;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_type::{ArtcraftUsagePage, ArtcraftUsageType};
#[derive(Deserialize)]
pub struct EnqueueImageTo3dObjectRequest {
@@ -90,7 +88,6 @@ pub async fn enqueue_image_to_3d_object_command(
request: EnqueueImageTo3dObjectRequest,
app_env_configs: State<'_, AppEnvConfigs>,
app_data_root: State<'_, AppDataRoot>,
artcraft_usage_tracker: State<'_, ArtcraftUsageTracker>,
provider_priority_store: State<'_, ProviderPriorityStore>,
task_database: State<'_, TaskDatabase>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
@@ -104,7 +101,6 @@ pub async fn enqueue_image_to_3d_object_command(
&app,
&app_env_configs,
&app_data_root,
&artcraft_usage_tracker,
&provider_priority_store,
&task_database,
&storyteller_creds_manager,
@@ -175,7 +171,6 @@ pub async fn handle_request(
app: &AppHandle,
app_env_configs: &AppEnvConfigs,
app_data_root: &AppDataRoot,
artcraft_usage_tracker: &ArtcraftUsageTracker,
provider_priority_store: &ProviderPriorityStore,
task_database: &TaskDatabase,
storyteller_creds_manager: &StorytellerCredentialManager,
@@ -208,10 +203,6 @@ pub async fn handle_request(
// NB: Fail open, but find a way to flag this.
}
if let Err(err) = artcraft_usage_tracker.record_object_generation(1, ArtcraftUsageType::ImageToResult, ArtcraftUsagePage::ObjectPage) {
// NB: Fail open.
warn!("Failed to report usage: {:?}", err);
}
Ok(success_event)
}
@@ -14,7 +14,6 @@ use crate::core::events::generation_events::common::GenerationAction;
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
use crate::core::state::task_database::TaskDatabase;
@@ -28,7 +27,6 @@ use log::{error, info, warn};
use serde_derive::{Deserialize, Serialize};
use tauri::{AppHandle, State};
use tokens::tokens::media_files::MediaFileToken;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_type::{ArtcraftUsagePage, ArtcraftUsageType};
/// This is used in the Tauri command bridge.
/// Don't change the serializations without coordinating with the frontend.
@@ -177,7 +175,6 @@ pub async fn enqueue_image_to_video_command(
app: AppHandle,
app_env_configs: State<'_, AppEnvConfigs>,
app_data_root: State<'_, AppDataRoot>,
artcraft_usage_tracker: State<'_, ArtcraftUsageTracker>,
provider_priority_store: State<'_, ProviderPriorityStore>,
task_database: State<'_, TaskDatabase>,
grok_creds_manager: State<'_, GrokCredentialManager>,
@@ -193,7 +190,6 @@ pub async fn enqueue_image_to_video_command(
&app,
&app_env_configs,
&app_data_root,
&artcraft_usage_tracker,
&provider_priority_store,
&task_database,
&grok_creds_manager,
@@ -266,7 +262,6 @@ pub async fn handle_request(
app: &AppHandle,
app_env_configs: &AppEnvConfigs,
app_data_root: &AppDataRoot,
artcraft_usage_tracker: &ArtcraftUsageTracker,
provider_priority_store: &ProviderPriorityStore,
task_database: &TaskDatabase,
grok_creds_manager: &GrokCredentialManager,
@@ -336,10 +331,5 @@ pub async fn handle_request(
// NB: Fail open, but find a way to flag this.
}
if let Err(err) = artcraft_usage_tracker.record_video_generation(1, ArtcraftUsageType::ImageToResult, ArtcraftUsagePage::VideoPage) {
// NB: Fail open.
warn!("Failed to report usage: {:?}", err);
}
Ok(success_event)
}
@@ -62,7 +62,8 @@ pub async fn handle_artcraft_gpt_image_1p5_text_to_image(
prompt: request.prompt.clone(),
image_size: aspect_ratio,
num_images,
image_media_tokens: request.image_media_tokens.clone(),
// Not provided for text-to-image
image_media_tokens: None,
mask_image_token: None,
input_fidelity: None,
// Not provided
@@ -1,7 +1,5 @@
use crate::core::api_adapters::aspect_ratio::common_aspect_ratio::CommonAspectRatio;
use crate::core::api_adapters::aspect_ratio::convert::aspect_ratio_to_artcraft_nano_banana_pro::aspect_ratio_to_artcraft_nano_banana_pro;
use crate::core::api_adapters::resolution::common_resolution::CommonResolution;
use crate::core::api_adapters::resolution::convert::resolution_to_artcraft_nano_banana_pro::resolution_to_artcraft_nano_banana_pro;
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason, ProviderFailureReason};
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageResolution, EditImageSize};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
@@ -63,12 +61,18 @@ pub async fn handle_artcraft_nano_banana_pro_text_to_image(
};
let aspect_ratio = get_aspect_ratio(request);
let resolution = get_resolution(request);
let resolution = request.image_resolution
.map(|image_resolution| match image_resolution {
TextToImageResolution::OneK => NanoBananaProMultiFunctionImageGenImageResolution::OneK,
TextToImageResolution::TwoK => NanoBananaProMultiFunctionImageGenImageResolution::TwoK,
TextToImageResolution::FourK => NanoBananaProMultiFunctionImageGenImageResolution::FourK,
});
let request = NanoBananaProMultiFunctionImageGenRequest {
uuid_idempotency_token,
prompt: request.prompt.clone(),
image_media_tokens: request.image_media_tokens.clone(),
image_media_tokens: None,
num_images,
resolution,
aspect_ratio,
@@ -122,20 +126,3 @@ fn get_aspect_ratio(request: &EnqueueTextToImageRequest) -> Option<NanoBananaPro
None
}
fn get_resolution(request: &EnqueueTextToImageRequest) -> Option<NanoBananaProMultiFunctionImageGenImageResolution> {
if let Some(common_resolution) = request.common_resolution {
return Some(resolution_to_artcraft_nano_banana_pro(common_resolution));
}
if let Some(resolution) = request.image_resolution {
// Deprecated resolution
return Some(match resolution {
TextToImageResolution::OneK => NanoBananaProMultiFunctionImageGenImageResolution::OneK,
TextToImageResolution::TwoK => NanoBananaProMultiFunctionImageGenImageResolution::TwoK,
TextToImageResolution::FourK => NanoBananaProMultiFunctionImageGenImageResolution::FourK,
})
}
None
}
@@ -1,5 +1,3 @@
use crate::core::api_adapters::aspect_ratio::common_aspect_ratio::CommonAspectRatio;
use crate::core::api_adapters::resolution::common_resolution::CommonResolution;
use crate::core::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors;
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason};
use crate::core::commands::enqueue::image_edit::image_edit_models::image_edit_model_to_model_type;
@@ -18,8 +16,6 @@ use crate::core::events::generation_events::common::{GenerationAction, Generatio
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::core::events::sendable_event_trait::SendableEvent;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_type::{ArtcraftUsagePage, ArtcraftUsageType};
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
use crate::core::state::task_database::TaskDatabase;
@@ -38,6 +34,7 @@ use serde::{Deserialize, Serialize};
use sqlite_tasks::queries::create_task::{create_task, CreateTaskArgs};
use tauri::{AppHandle, State};
use tokens::tokens::media_files::MediaFileToken;
use crate::core::api_adapters::aspect_ratio::common_aspect_ratio::CommonAspectRatio;
/// This is used in the Tauri command bridge.
/// Don't change the serializations without coordinating with the frontend.
@@ -89,20 +86,15 @@ pub struct EnqueueTextToImageRequest {
/// Text prompt for the image generation. Required.
pub prompt: Option<String>,
/// Aspect ratio.
#[deprecated(note="use common_aspect_ratio")]
pub aspect_ratio: Option<TextToImageSize>,
/// New field for aspect ratio.
/// Not all models support each of these aspect ratios, but we can choose or interpolate sensibly.
pub common_aspect_ratio: Option<CommonAspectRatio>,
/// New field for resolution.
/// Not all models support each of these resolutions, but we can choose or interpolate sensibly.
pub common_resolution: Option<CommonResolution>,
/// Aspect ratio.
#[deprecated(note="use common_aspect_ratio")]
pub aspect_ratio: Option<TextToImageSize>,
/// Aspect ratio.
#[deprecated(note="use common_resolution")]
pub image_resolution: Option<TextToImageResolution>,
/// The number of images to generate.
@@ -187,7 +179,6 @@ pub async fn enqueue_text_to_image_command(
app: AppHandle,
app_data_root: State<'_, AppDataRoot>,
app_env_configs: State<'_, AppEnvConfigs>,
artcraft_usage_tracker: State<'_, ArtcraftUsageTracker>,
provider_priority_store: State<'_, ProviderPriorityStore>,
task_database: State<'_, TaskDatabase>,
mj_creds_manager: State<'_, MidjourneyCredentialManager>,
@@ -206,7 +197,6 @@ pub async fn enqueue_text_to_image_command(
request,
&app,
&app_data_root,
&artcraft_usage_tracker,
&provider_priority_store,
&task_database,
&mj_creds_manager,
@@ -277,7 +267,6 @@ pub async fn handle_request(
request: EnqueueTextToImageRequest,
app: &AppHandle,
app_data_root: &AppDataRoot,
artcraft_usage_tracker: &ArtcraftUsageTracker,
provider_priority_store: &ProviderPriorityStore,
task_database: &TaskDatabase,
mj_creds_manager: &MidjourneyCredentialManager,
@@ -321,24 +310,6 @@ pub async fn handle_request(
error!("Failed to create task in database: {:?}", err);
// NB: Fail open, but find a way to flag this.
}
let num_images = request.number_images.map(|n| n as u16).unwrap_or(1);
let is_image_to_image = request.image_media_tokens
.as_ref()
.map(|tokens| !tokens.is_empty())
.unwrap_or(false);
let usage_type = if is_image_to_image {
ArtcraftUsageType::ImageToResult
} else {
ArtcraftUsageType::TextToResult
};
if let Err(err) = artcraft_usage_tracker.record_image_generation(num_images, usage_type, ArtcraftUsagePage::ImagePage) {
// NB: Fail open.
warn!("Failed to report usage: {:?}", err);
}
Ok(success_event)
}
@@ -8,7 +8,6 @@ use crate::core::lifecycle::startup::tasks::spawn_sora_task_polling_thread::spaw
use crate::core::lifecycle::startup::tasks::spawn_storyteller_threads::spawn_storyteller_threads;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::artcraft_platform_info::ArtcraftPlatformInfo;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::services::grok::state::grok_credential_manager::GrokCredentialManager;
use crate::services::grok::state::grok_image_prompt_queue::GrokImagePromptQueue;
@@ -30,7 +29,6 @@ pub async fn handle_tauri_startup(
root: AppDataRoot,
app_env_configs: AppEnvConfigs,
artcraft_platform_info: ArtcraftPlatformInfo,
artcraft_usage_tracker: ArtcraftUsageTracker,
storyteller_creds_manager: StorytellerCredentialManager,
sora_credential_manager: SoraCredentialManager,
sora_task_queue: SoraTaskQueue,
@@ -63,7 +61,6 @@ pub async fn handle_tauri_startup(
spawn_storyteller_threads(
&app,
&app_env_configs,
&artcraft_usage_tracker,
&artcraft_platform_info,
&task_database,
&storyteller_creds_manager,
@@ -1,6 +1,5 @@
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::artcraft_platform_info::ArtcraftPlatformInfo;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
use crate::core::state::task_database::TaskDatabase;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use crate::services::storyteller::threads::storyteller_activity_thread::storyteller_activity_thread;
@@ -11,7 +10,6 @@ use tauri::{AppHandle, Manager};
pub fn spawn_storyteller_threads(
app: &AppHandle,
app_env_configs: &AppEnvConfigs,
artcraft_usage_tracker: &ArtcraftUsageTracker,
artcraft_platform_info: &ArtcraftPlatformInfo,
task_database: &TaskDatabase,
storyteller_creds_manager: &StorytellerCredentialManager,
@@ -27,7 +25,6 @@ pub fn spawn_storyteller_threads(
tauri::async_runtime::spawn(storyteller_activity_thread(
app_env_configs.clone(),
artcraft_platform_info.clone(),
artcraft_usage_tracker.clone(),
storyteller_creds_manager.clone(),
));
@@ -10,9 +10,6 @@ pub struct AppPreferences {
/// Play sounds on events.
pub play_sounds: bool,
/// Key pointing to file; defined in the frontend code.
pub delete_file_sound: Option<String>,
/// Key pointing to file; defined in the frontend code.
/// Defined for enqueue since image enqueue can be async
pub enqueue_success_sound: Option<String>,
@@ -43,7 +40,6 @@ impl Default for AppPreferences {
generation_success_sound: Some("special_flower".to_string()),
generation_failure_sound: Some("crumble".to_string()),
generation_enqueue_sound: Some("done".to_string()),
delete_file_sound: Some("trash".to_string()),
}
}
}
@@ -5,9 +5,7 @@ use errors::AnyhowResult;
use serde_derive::{Deserialize, Serialize};
/// Vector clock versioning string rather than semver.
/// - Version 1 - initial version.
/// - Version 2 - added "delete_file_sound", marked optionals "skip_serializing_if"
const CURRENT_VERSION: &str = "2";
const CURRENT_VERSION: &str = "1";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppPreferencesSerializable {
@@ -18,23 +16,15 @@ pub struct AppPreferencesSerializable {
pub preferred_download_directory: Option<PreferredDownloadDirectory>,
/// Play sounds on events.
#[serde(skip_serializing_if = "Option::is_none")]
pub play_sounds: Option<bool>,
/// Key pointing to file; defined in the frontend code.
#[serde(skip_serializing_if = "Option::is_none")]
pub delete_file_sound: Option<String>,
/// Key pointing to file; defined in the frontend code.
#[serde(skip_serializing_if = "Option::is_none")]
pub generation_success_sound: Option<String>,
/// Key pointing to file; defined in the frontend code.
#[serde(skip_serializing_if = "Option::is_none")]
pub generation_failure_sound: Option<String>,
/// Key pointing to file; defined in the frontend code.
#[serde(skip_serializing_if = "Option::is_none")]
pub generation_enqueue_sound: Option<String>,
}
@@ -55,7 +45,6 @@ impl AppPreferencesSerializable {
version: CURRENT_VERSION.to_string(),
preferred_download_directory: Some(preferences.preferred_download_directory.clone()),
play_sounds: Some(preferences.play_sounds),
delete_file_sound: preferences.delete_file_sound.clone(),
generation_success_sound: preferences.generation_success_sound.clone(),
generation_failure_sound: preferences.generation_failure_sound.clone(),
generation_enqueue_sound: preferences.generation_enqueue_sound.clone(),
@@ -72,12 +61,11 @@ impl AppPreferencesSerializable {
if let Some(play_sounds) = self.play_sounds {
preferences.play_sounds = play_sounds;
}
preferences.delete_file_sound = self.delete_file_sound.clone();
preferences.generation_success_sound = self.generation_success_sound.clone();
preferences.generation_failure_sound = self.generation_failure_sound.clone();
preferences.generation_enqueue_sound = self.generation_enqueue_sound.clone();
preferences
}
}
@@ -1,7 +1,9 @@
use crate::core::state::os_platform::OsPlatform;
use crate::version::ARTCRAFT_VERSION;
use chrono::{DateTime, Utc};
// TODO: Centrally configure version.
const ARTCRAFT_VERSION: &str = "0.3.0";
#[derive(Clone, Debug)]
pub struct ArtcraftPlatformInfo {
pub artcraft_version: String,
@@ -1,135 +0,0 @@
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker_data::ArtcraftUsageTrackerData;
use std::sync::{Arc, LockResult, RwLock};
use log::{error, warn};
use crate::core::artcraft_error::ArtcraftError;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_type::{ArtcraftUsagePage, ArtcraftUsageType};
/// Track generation volume so we know which areas to focus efforts on.
/// But also note: https://en.wikipedia.org/wiki/Survivorship_bias
#[derive(Clone)]
pub struct ArtcraftUsageTracker {
data: Arc<RwLock<ArtcraftUsageTrackerData>>,
}
impl ArtcraftUsageTracker {
pub fn new() -> Self {
Self {
data: Arc::new(RwLock::new(ArtcraftUsageTrackerData::new())),
}
}
pub fn record_image_generation(&self, num_images: u16, usage_type: ArtcraftUsageType, page: ArtcraftUsagePage) -> Result<(), ArtcraftError> {
let mut data = self.get()?;
data.total_generation_count += num_images;
data.image_generation_count += num_images;
match usage_type {
ArtcraftUsageType::TextToResult => {
data.text_to_image_count += num_images;
}
ArtcraftUsageType::ImageToResult => {
data.image_to_image_count += num_images;
}
ArtcraftUsageType::Other => {}
}
match page {
ArtcraftUsagePage::ImagePage => {
data.image_page_prompt_count += num_images;
}
ArtcraftUsagePage::EditPage => {
data.edit_page_prompt_count += num_images;
}
ArtcraftUsagePage::StagePage => {
data.stage_page_prompt_count += num_images;
}
ArtcraftUsagePage::OtherPage => {
data.other_page_prompt_count += num_images;
}
ArtcraftUsagePage::VideoPage => {} // NB: Does not generate images.
ArtcraftUsagePage::ObjectPage => {} // NB: Does not generate images.
}
self.set(data)?;
Ok(())
}
pub fn record_video_generation(&self, num_videos: u16, usage_type: ArtcraftUsageType, page: ArtcraftUsagePage) -> Result<(), ArtcraftError> {
let mut data = self.get()?;
data.total_generation_count += num_videos;
data.video_generation_count += num_videos;
match usage_type {
ArtcraftUsageType::TextToResult => {
data.text_to_video_count += num_videos;
}
ArtcraftUsageType::ImageToResult => {
data.image_to_video_count += num_videos;
}
ArtcraftUsageType::Other => {}
}
match page {
ArtcraftUsagePage::VideoPage => {
data.video_page_prompt_count += num_videos;
}
ArtcraftUsagePage::OtherPage => {
data.other_page_prompt_count += num_videos;
}
ArtcraftUsagePage::ImagePage => {} // NB: Does not generate videos.
ArtcraftUsagePage::EditPage => {} // NB: Does not generate videos.
ArtcraftUsagePage::StagePage => {} // NB: Does not generate videos.
ArtcraftUsagePage::ObjectPage => {} // NB: Does not generate videos.
}
self.set(data)?;
Ok(())
}
pub fn record_object_generation(&self, num_objects: u16, usage_type: ArtcraftUsageType, page: ArtcraftUsagePage) -> Result<(), ArtcraftError> {
let mut data = self.get()?;
data.total_generation_count += num_objects;
data.object_generation_count += num_objects;
match usage_type {
ArtcraftUsageType::TextToResult => {
data.text_to_object_count += num_objects;
}
ArtcraftUsageType::ImageToResult => {
data.image_to_object_count += num_objects;
}
ArtcraftUsageType::Other => {}
}
match page {
ArtcraftUsagePage::ObjectPage => {
data.object_page_prompt_count += num_objects;
}
ArtcraftUsagePage::OtherPage => {
data.other_page_prompt_count += num_objects;
}
ArtcraftUsagePage::ImagePage => {} // NB: Does not generate objects.
ArtcraftUsagePage::VideoPage => {} // NB: Does not generate objects.
ArtcraftUsagePage::EditPage => {} // NB: Does not generate objects.
ArtcraftUsagePage::StagePage => {} // NB: Does not generate objects.
}
self.set(data)?;
Ok(())
}
pub fn get(&self) -> Result<ArtcraftUsageTrackerData, ArtcraftError> {
match self.data.read() {
Ok(data) => Ok(data.clone()),
Err(err) => {
error!("Lock read error: {:?}", err);
Err(ArtcraftError::RwLockReadError)
}
}
}
fn set(&self, data: ArtcraftUsageTrackerData) -> Result<(), ArtcraftError> {
match self.data.write() {
Ok(mut lock) => {
*lock = data;
Ok(())
},
Err(err) => {
error!("Lock write error: {:?}", err);
Err(ArtcraftError::RwLockWriteError)
}
}
}
}
@@ -1,46 +0,0 @@
/// Track generation volume so we know which areas to focus efforts on.
/// But also note: https://en.wikipedia.org/wiki/Survivorship_bias
#[derive(Clone)]
pub struct ArtcraftUsageTrackerData {
pub total_generation_count: u16,
pub image_generation_count: u16,
pub video_generation_count: u16,
pub object_generation_count: u16,
pub text_to_image_count: u16,
pub image_to_image_count: u16,
pub text_to_video_count: u16,
pub image_to_video_count: u16,
pub text_to_object_count: u16,
pub image_to_object_count: u16,
pub image_page_prompt_count: u16,
pub video_page_prompt_count: u16,
pub edit_page_prompt_count: u16,
pub stage_page_prompt_count: u16,
pub object_page_prompt_count: u16,
pub other_page_prompt_count: u16,
}
impl ArtcraftUsageTrackerData {
pub fn new() -> Self {
Self {
total_generation_count: 0,
image_generation_count: 0,
video_generation_count: 0,
object_generation_count: 0,
text_to_image_count: 0,
image_to_image_count: 0,
text_to_video_count: 0,
image_to_video_count: 0,
text_to_object_count: 0,
image_to_object_count: 0,
image_page_prompt_count: 0,
video_page_prompt_count: 0,
edit_page_prompt_count: 0,
stage_page_prompt_count: 0,
object_page_prompt_count: 0,
other_page_prompt_count: 0,
}
}
}
@@ -1,18 +0,0 @@
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ArtcraftUsageType {
TextToResult,
ImageToResult,
Other,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ArtcraftUsagePage {
ImagePage,
VideoPage,
EditPage,
StagePage,
ObjectPage,
OtherPage,
}
@@ -1,3 +0,0 @@
pub mod artcraft_usage_tracker;
pub mod artcraft_usage_type;
mod artcraft_usage_tracker_data;
@@ -2,7 +2,6 @@ pub mod app_env_configs;
pub mod app_preferences;
pub mod app_startup_time;
pub mod artcraft_platform_info;
pub mod artcraft_usage_tracker;
pub mod data_dir;
pub mod expanduser;
pub mod os_platform;
-7
View File
@@ -1,6 +1,5 @@
pub mod core;
pub mod services;
pub mod version;
use tauri::Manager;
@@ -73,7 +72,6 @@ use tauri_plugin_dialog;
use tauri_plugin_http;
use tauri_plugin_log::Target;
use tauri_plugin_log::TargetKind;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
@@ -126,9 +124,6 @@ pub fn run() {
let worldlabs_bearer_bridge = WorldlabsBearerBridge::empty();
let worldlabs_bearer_bridge_2 = worldlabs_bearer_bridge.clone();
let artcraft_usage_tracker = ArtcraftUsageTracker::new();
let artcraft_usage_tracker_2 = artcraft_usage_tracker.clone();
println!("Initializing backend runtime...");
@@ -164,7 +159,6 @@ pub fn run() {
root,
env_config,
artcraft_platform_info_2,
artcraft_usage_tracker_2,
storyteller_creds,
sora_creds,
sora_tasks,
@@ -187,7 +181,6 @@ pub fn run() {
.manage(app_env_configs)
.manage(app_preferences)
.manage(artcraft_platform_info)
.manage(artcraft_usage_tracker)
.manage(grok_creds_manager)
.manage(grok_prompt_queue)
.manage(midjourney_creds_manager)
@@ -1,7 +1,6 @@
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::artcraft_platform_info::ArtcraftPlatformInfo;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use crate::core::state::os_platform::OsPlatform;
@@ -23,7 +22,6 @@ use sqlite_tasks::queries::update_task_status::{update_task_status, UpdateTaskAr
use std::time::Instant;
use storyteller_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
use storyteller_client::endpoints::analytics::log_active_user::log_active_user;
use storyteller_client::endpoints::analytics::log_active_user_v2::log_active_user_v2;
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 storyteller_client::error::api_error::ApiError;
@@ -31,14 +29,15 @@ use storyteller_client::error::storyteller_error::StorytellerError;
use tauri::AppHandle;
use tokens::tokens::app_session::AppSessionToken;
// TODO: Configure this with the build and increment.
const CLIENT_NAME : &str = "artcraft";
const CLIENT_VERSION : &str = "0.3.0";
const ERROR_SLEEP_MILLIS : u64 = 1_000 * 60 * 3; // 3 minutes;
pub async fn storyteller_activity_thread(
app_env_configs: AppEnvConfigs,
artcraft_platform_info: ArtcraftPlatformInfo,
artcraft_usage_tracker: ArtcraftUsageTracker,
storyteller_creds_manager: StorytellerCredentialManager,
) -> ! {
let startup = Instant::now();
@@ -49,7 +48,6 @@ pub async fn storyteller_activity_thread(
loop {
let res = polling_loop(
&app_env_configs,
&artcraft_usage_tracker,
&storyteller_creds_manager,
startup,
&artcraft_platform_info,
@@ -65,7 +63,6 @@ pub async fn storyteller_activity_thread(
async fn polling_loop(
app_env_configs: &AppEnvConfigs,
artcraft_usage_tracker: &ArtcraftUsageTracker,
storyteller_creds_manager: &StorytellerCredentialManager,
startup: Instant,
artcraft_platform_info: &ArtcraftPlatformInfo,
@@ -88,8 +85,6 @@ async fn polling_loop(
},
};
let usage_data = artcraft_usage_tracker.get()?;
let time_since_startup = Instant::now().duration_since(startup);
let request = LogAppActiveUserRequest {
@@ -99,27 +94,11 @@ async fn polling_loop(
maybe_os_platform: Some(artcraft_platform_info.os_platform.as_str().to_owned()),
maybe_os_version: Some(artcraft_platform_info.os_version.clone()),
maybe_session_duration_seconds: Some(time_since_startup.as_secs()),
total_generation_count: Some(usage_data.total_generation_count),
image_generation_count: Some(usage_data.image_generation_count),
video_generation_count: Some(usage_data.video_generation_count),
object_generation_count: Some(usage_data.object_generation_count),
text_to_image_count: Some(usage_data.text_to_image_count),
image_to_image_count: Some(usage_data.image_to_image_count),
text_to_video_count: Some(usage_data.text_to_video_count),
image_to_video_count: Some(usage_data.image_to_video_count),
text_to_object_count: Some(usage_data.text_to_object_count),
image_to_object_count: Some(usage_data.image_to_object_count),
image_page_prompt_count: Some(usage_data.image_page_prompt_count),
video_page_prompt_count: Some(usage_data.video_page_prompt_count),
edit_page_prompt_count: Some(usage_data.edit_page_prompt_count),
stage_page_prompt_count: Some(usage_data.stage_page_prompt_count),
object_page_prompt_count: Some(usage_data.object_page_prompt_count),
other_page_prompt_count: Some(usage_data.other_page_prompt_count),
};
debug!("Logging active user with storyteller.");
let result = log_active_user_v2(
let result = log_active_user(
&app_env_configs.storyteller_host,
Some(&creds),
request,
-4
View File
@@ -1,4 +0,0 @@
/// The version of the ArtCraft desktop app reported in the UI and via Analytics.
/// This should match the CI builds.
pub const ARTCRAFT_VERSION: &str = "0.5.0";
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2.0.0-rc",
"productName": "ArtCraft",
"version": "0.5.0",
"version": "0.3.0",
"identifier": "ai.artcraft.app",
"build": {
"frontendDist": "../../../frontend/apps/artcraft/dist",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2.0.0-rc",
"productName": "ArtCraft",
"version": "0.5.0",
"version": "0.3.0",
"identifier": "ai.artcraft.app",
"build": {
"frontendDist": "../../../frontend/apps/artcraft/dist",
@@ -2,7 +2,7 @@ use std::collections::HashSet;
use once_cell::sync::Lazy;
const TERMS : &str = include_str!("../../../../../../includes/binary_includes/safety/dictionary_children_terms.txt");
const TERMS : &str = include_str!("../../../../../../includes/binary_includes/dictionary_children_terms.txt");
pub (crate) fn contains_children_keyword(prompt_tokens: &[String]) -> bool {
static TERM_DICTIONARY : Lazy<HashSet<String>> = Lazy::new(|| {
@@ -2,7 +2,7 @@ use std::collections::HashSet;
use once_cell::sync::Lazy;
const TERMS: &str = include_str!("../../../../../../includes/binary_includes/safety/dictionary_potential_minor_terms.txt");
const TERMS: &str = include_str!("../../../../../../includes/binary_includes/dictionary_potential_minor_terms.txt");
pub (crate) fn contains_potential_minor_keyword(prompt_tokens: &[String]) -> bool {
static TERM_DICTIONARY: Lazy<HashSet<String>> = Lazy::new(|| {
@@ -2,7 +2,7 @@ use std::collections::HashSet;
use once_cell::sync::Lazy;
const TERMS : &str = include_str!("../../../../../../includes/binary_includes/safety/dictionary_racist_terms.txt");
const TERMS : &str = include_str!("../../../../../../includes/binary_includes/dictionary_racist_terms.txt");
pub (crate) fn contains_racist_keyword(prompt_tokens: &[String]) -> bool {
static TERM_DICTIONARY : Lazy<HashSet<String>> = Lazy::new(|| {
@@ -2,7 +2,7 @@ use std::collections::HashSet;
use once_cell::sync::Lazy;
const TERMS : &str = include_str!("../../../../../../includes/binary_includes/safety/dictionary_sex_terms.txt");
const TERMS : &str = include_str!("../../../../../../includes/binary_includes/dictionary_sex_terms.txt");
pub (crate) fn contains_sex_keyword(prompt_tokens: &[String]) -> bool {
static TERM_DICTIONARY : Lazy<HashSet<String>> = Lazy::new(|| {
TERMS.lines()
@@ -0,0 +1,33 @@
[package]
name = "shared_service_components"
edition = "2021"
version = "0.0.1"
authors = [
"Brandon Thomas <bt@brand.io>",
"Brandon Thomas <echelon@gmail.com>",
]
publish = false
[lib]
name = "shared_service_components"
path = "src/lib.rs"
[package.metadata.cargo-udeps.ignore]
normal = ["workspace-build-acceleration"]
[dependencies]
# cargo-hakari workspace hack (faster compile times)
workspace-build-acceleration = { workspace = true }
# Internal
openai_sora_client = { workspace = true }
errors = { workspace = true }
# External
anyhow = { workspace = true }
log = { workspace = true }
r2d2_redis = "0.14.0"
reqwest = { workspace = true, features = ["multipart", "gzip"] }
[dev-dependencies]
# None yet
@@ -0,0 +1,4 @@
//! This library contains code shared between `storyteller-web` and jobs.
pub mod sora_redis_credentials;
pub mod sora_redis_sentinel_refresh;
@@ -0,0 +1,33 @@
use crate::sora_redis_credentials::keys::{BEARER_SUBKEY, COOKIE_SUBKEY, SENTINEL_SUBKEY, SORA_SECRET_REDIS_KEY};
use anyhow::anyhow;
use errors::AnyhowResult;
use openai_sora_client::creds::sora_credential_builder::SoraCredentialBuilder;
use openai_sora_client::creds::sora_credential_set::SoraCredentialSet;
use r2d2_redis::r2d2::PooledConnection;
use r2d2_redis::redis::Commands;
use r2d2_redis::RedisConnectionManager;
use std::collections::HashMap;
pub fn get_sora_credentials_from_redis(
redis: &mut PooledConnection<RedisConnectionManager>
) -> AnyhowResult<SoraCredentialSet> {
let values : HashMap<String, String> = redis.hgetall(SORA_SECRET_REDIS_KEY)
.map_err(|e| anyhow!("Failed to get Sora credentials from Redis: {}", e))?;
let bearer = values.get(BEARER_SUBKEY);
let cookie = values.get(COOKIE_SUBKEY);
let sentinel = values.get(SENTINEL_SUBKEY);
match (bearer, cookie, sentinel) {
(Some(b), Some(c), Some(s)) => {
let creds = SoraCredentialBuilder::new()
.with_jwt_bearer_token(&b)
.with_cookies(&c)
.with_sora_sentinel(&s)
.build()?;
Ok(creds)
}
_ => Err(anyhow!("redis sora credential values not present")),
}
}
@@ -0,0 +1,29 @@
/// Redis key
pub (crate) const SORA_SECRET_REDIS_KEY : &str = "sora_secret";
/// HKEY field for the bearer token
pub (crate) const BEARER_SUBKEY : &str = "bearer";
/// HKEY field for the cookie
pub (crate) const COOKIE_SUBKEY : &str = "cookie";
/// HKEY field for the sentinel
pub (crate) const SENTINEL_SUBKEY : &str = "sentinel";
/// Which subkey to write
#[derive(Clone,Copy,Debug)]
pub enum RedisSoraCredentialSubkey {
Bearer,
Cookie,
Sentinel,
}
impl RedisSoraCredentialSubkey {
pub (crate) fn to_str(&self) -> &'static str {
match self {
RedisSoraCredentialSubkey::Bearer => BEARER_SUBKEY,
RedisSoraCredentialSubkey::Cookie => COOKIE_SUBKEY,
RedisSoraCredentialSubkey::Sentinel => SENTINEL_SUBKEY,
}
}
}
@@ -0,0 +1,3 @@
pub mod keys;
pub mod set_sora_credential_field_in_redis;
pub mod get_sora_credentials_from_redis;
@@ -0,0 +1,18 @@
use anyhow::anyhow;
use r2d2_redis::r2d2::PooledConnection;
use r2d2_redis::redis::Commands;
use r2d2_redis::RedisConnectionManager;
use errors::AnyhowResult;
use crate::sora_redis_credentials::keys::{RedisSoraCredentialSubkey, SORA_SECRET_REDIS_KEY};
pub fn set_sora_credential_field_in_redis(
redis: &mut PooledConnection<RedisConnectionManager>,
subkey: RedisSoraCredentialSubkey,
value: &str,
) -> AnyhowResult<()> {
redis.hset(SORA_SECRET_REDIS_KEY, subkey.to_str(), value)
.map_err(|e| anyhow!("Failed to set Sora credentials in Redis: {}", e))?;
Ok(())
}
@@ -0,0 +1 @@
pub mod refresh;
@@ -0,0 +1,19 @@
/// Send a POST request to env variable SORA_SENTINEL_REFRESH_URL
use errors::AnyhowResult;
use reqwest::Client;
use std::env;
const SORA_SENTINEL_REFRESH_URL: &str = "http://oai-token-server/refresh_token";
/// Refresh the Sora sentinel
/// The response body does _not_ contain the new sentinel
/// We should read it again from redis or generate a new one here
/// For now we refresh in the server handler
pub async fn refresh_sentinel() -> AnyhowResult<String> {
let client = Client::new();
let url = env::var("SORA_SENTINEL_REFRESH_URL").unwrap_or(SORA_SENTINEL_REFRESH_URL.to_string());
let response = client.post(url).send().await?;
let body = response.text().await?;
Ok(body)
}
+1 -1
View File
@@ -8,7 +8,7 @@
#![forbid(unused_variables)]
#![forbid(warnings)]
pub (crate) const BANNED_SLURS : &str = include_str!("../../../../includes/binary_includes/safety/banned_slurs.txt");
pub (crate) const BANNED_SLURS : &str = include_str!("../../../../includes/binary_includes/banned_slurs.txt");
pub mod check_for_slurs;
pub mod latin_alphabet;
@@ -18,23 +18,6 @@ pub struct UpsertAnalyticsAppSession<'a> {
pub os_platform: Option<&'a str>,
pub os_version: Option<&'a str>,
pub session_duration_seconds: Option<u64>,
// Generation counts
pub total_generation_count: u16,
pub image_generation_count: u16,
pub video_generation_count: u16,
pub object_generation_count: u16,
pub text_to_image_count: u16,
pub image_to_image_count: u16,
pub text_to_video_count: u16,
pub image_to_video_count: u16,
pub text_to_object_count: u16,
pub image_to_object_count: u16,
pub image_page_prompt_count: u16,
pub video_page_prompt_count: u16,
pub edit_page_prompt_count: u16,
pub stage_page_prompt_count: u16,
pub object_page_prompt_count: u16,
pub other_page_prompt_count: u16,
}
impl <'a> UpsertAnalyticsAppSession<'a> {
@@ -52,22 +35,6 @@ SET
session_duration_seconds = ?,
ip_address = ?,
measurement_count = measurement_count + 1,
total_generation_count = ?,
image_generation_count = ?,
video_generation_count = ?,
object_generation_count = ?,
text_to_image_count = ?,
image_to_image_count = ?,
text_to_video_count = ?,
image_to_video_count = ?,
text_to_object_count = ?,
image_to_object_count = ?,
image_page_prompt_count = ?,
video_page_prompt_count = ?,
edit_page_prompt_count = ?,
stage_page_prompt_count = ?,
object_page_prompt_count = ?,
other_page_prompt_count = ?,
first_active_at = NOW(),
last_active_at = NOW()
ON DUPLICATE KEY UPDATE
@@ -79,22 +46,6 @@ ON DUPLICATE KEY UPDATE
session_duration_seconds = ?,
ip_address = ?,
measurement_count = measurement_count + 1,
total_generation_count = ?,
image_generation_count = ?,
video_generation_count = ?,
object_generation_count = ?,
text_to_image_count = ?,
image_to_image_count = ?,
text_to_video_count = ?,
image_to_video_count = ?,
text_to_object_count = ?,
image_to_object_count = ?,
image_page_prompt_count = ?,
video_page_prompt_count = ?,
edit_page_prompt_count = ?,
stage_page_prompt_count = ?,
object_page_prompt_count = ?,
other_page_prompt_count = ?,
last_active_at = NOW()
"#,
// Insert case
@@ -106,22 +57,6 @@ ON DUPLICATE KEY UPDATE
self.os_version,
self.session_duration_seconds,
self.ip_address,
self.total_generation_count,
self.image_generation_count,
self.video_generation_count,
self.object_generation_count,
self.text_to_image_count,
self.image_to_image_count,
self.text_to_video_count,
self.image_to_video_count,
self.text_to_object_count,
self.image_to_object_count,
self.image_page_prompt_count,
self.video_page_prompt_count,
self.edit_page_prompt_count,
self.stage_page_prompt_count,
self.object_page_prompt_count,
self.other_page_prompt_count,
// Update case
self.namespace.to_str(),
self.user_token.as_str(),
@@ -130,22 +65,6 @@ ON DUPLICATE KEY UPDATE
self.os_version,
self.session_duration_seconds,
self.ip_address,
self.total_generation_count,
self.image_generation_count,
self.video_generation_count,
self.object_generation_count,
self.text_to_image_count,
self.image_to_image_count,
self.text_to_video_count,
self.image_to_video_count,
self.text_to_object_count,
self.image_to_object_count,
self.image_page_prompt_count,
self.video_page_prompt_count,
self.edit_page_prompt_count,
self.stage_page_prompt_count,
self.object_page_prompt_count,
self.other_page_prompt_count,
)
}
@@ -7,7 +7,6 @@ use sqlx;
use sqlx::mysql::MySqlRow;
use sqlx::pool::PoolConnection;
use sqlx::MySql;
use enums::common::stripe_subscription_status::StripeSubscriptionStatus;
use tokens::tokens::user_subscriptions::UserSubscriptionToken;
use tokens::tokens::users::UserToken;
@@ -22,7 +21,7 @@ pub struct UserSubscription {
pub stripe_customer_id: String,
pub stripe_product_id: String,
pub stripe_subscription_id: String,
pub stripe_subscription_status: StripeSubscriptionStatus,
pub stripe_subscription_status: String,
pub stripe_invoice_is_paid: bool,
/// When the subscription was created (Stripe's POV, not our db record),
@@ -126,7 +125,7 @@ SELECT
maybe_stripe_customer_id,
maybe_stripe_product_id,
maybe_stripe_subscription_id,
maybe_stripe_subscription_status as `maybe_stripe_subscription_status: enums::common::stripe_subscription_status::StripeSubscriptionStatus`,
maybe_stripe_subscription_status,
maybe_stripe_invoice_is_paid,
subscription_start_at,
subscription_expires_at,
@@ -165,7 +164,7 @@ struct RawUserSubscription {
maybe_stripe_customer_id: Option<String>,
maybe_stripe_product_id: Option<String>,
maybe_stripe_subscription_id: Option<String>,
maybe_stripe_subscription_status: Option<StripeSubscriptionStatus>,
maybe_stripe_subscription_status: Option<String>,
maybe_stripe_invoice_is_paid: Option<i8>,
subscription_start_at: DateTime<Utc>,
+2 -5
View File
@@ -4,11 +4,8 @@ pub mod generation_provider;
pub mod job_status;
pub mod job_status_plus;
pub mod model_type;
pub mod sqlite;
pub mod payments_namespace;
pub mod stripe_subscription_status;
pub mod view_as;
pub mod visibility;
pub mod vocoder_type;
// Sqlite specific types.
pub mod sqlite;
pub mod vocoder_type;
@@ -1,248 +0,0 @@
//! This is copied from the stripe crate's `SubscriptionStatus`,
//! and then from `reusable_types`.
// There are three changes from the source create:
// - Renamed the struct from SubscriptionStatus to StripeSubscriptionStatus
// - Added StripeSubscriptionStatus::from_str()
// - Added derive sqlx::Type and sqlx(rename_all)
// - Added Deref impl
// - Added tests
//
// Then, moving into enums:
// - Changed derives
// - as_st() -> to_str() (gross! but convention as of now...)
// - from_str() : changed error from string to EnumError
// - Added all_variants()
// - commented out all other impls.
//
use std::collections::BTreeSet;
use crate::error::enum_error::EnumError;
#[cfg(test)]
use strum::EnumCount;
#[cfg(test)]
use strum::EnumIter;
use utoipa::ToSchema;
// NB: Added "sqlx::Type".
#[cfg_attr(test, derive(EnumIter, EnumCount))]
#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum StripeSubscriptionStatus {
Active,
Canceled,
Incomplete,
IncompleteExpired,
PastDue,
Trialing,
Unpaid,
Paused,
}
impl_enum_display_and_debug_using_to_str!(StripeSubscriptionStatus);
impl_mysql_enum_coders!(StripeSubscriptionStatus);
impl_mysql_from_row!(StripeSubscriptionStatus);
impl StripeSubscriptionStatus {
pub fn to_str(self) -> &'static str {
match self {
Self::Active => "active",
Self::Canceled => "canceled",
Self::Incomplete => "incomplete",
Self::IncompleteExpired => "incomplete_expired",
Self::PastDue => "past_due",
Self::Trialing => "trialing",
Self::Unpaid => "unpaid",
Self::Paused => "paused"
}
}
pub fn from_str(value: &str) -> Result<Self, EnumError> {
match value {
"active" => Ok(Self::Active),
"canceled" => Ok(Self::Canceled),
"incomplete" => Ok(Self::Incomplete),
"incomplete_expired" => Ok(Self::IncompleteExpired),
"past_due" => Ok(Self::PastDue),
"trialing" => Ok(Self::Trialing),
"unpaid" => Ok(Self::Unpaid),
"paused" => Ok(Self::Paused),
_ => Err(EnumError::CouldNotConvertFromString(value.to_string())),
}
}
pub fn all_variants() -> BTreeSet<Self> {
// NB: BTreeSet is sorted
// NB: BTreeSet::from() isn't const, but not worth using LazyStatic, etc.
BTreeSet::from([
Self::Active,
Self::Canceled,
Self::Incomplete,
Self::IncompleteExpired,
Self::PastDue,
Self::Trialing,
Self::Unpaid,
Self::Paused,
])
}
}
//impl AsRef<str> for StripeSubscriptionStatus {
// fn as_ref(&self) -> &str {
// self.as_str()
// }
//}
//
//// NB: Added by us.
//impl std::ops::Deref for StripeSubscriptionStatus {
// type Target = str;
//
// fn deref(&self) -> &Self::Target {
// self.as_str()
// }
//}
//
//impl std::fmt::Display for StripeSubscriptionStatus {
// fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
// self.as_str().fmt(f)
// }
//}
//
//impl std::default::Default for StripeSubscriptionStatus {
// fn default() -> Self {
// Self::Active
// }
//}
#[cfg(test)]
mod tests {
use crate::error::enum_error::EnumError;
use crate::test_helpers::assert_serialization;
mod explicit_checks {
use super::*;
use crate::common::stripe_subscription_status::StripeSubscriptionStatus;
#[test]
fn test_serialization() {
assert_serialization(StripeSubscriptionStatus::Active, "active");
assert_serialization(StripeSubscriptionStatus::Canceled, "canceled");
assert_serialization(StripeSubscriptionStatus::Incomplete, "incomplete");
assert_serialization(StripeSubscriptionStatus::IncompleteExpired, "incomplete_expired");
assert_serialization(StripeSubscriptionStatus::PastDue, "past_due");
assert_serialization(StripeSubscriptionStatus::Trialing, "trialing");
assert_serialization(StripeSubscriptionStatus::Unpaid, "unpaid");
assert_serialization(StripeSubscriptionStatus::Paused, "paused");
}
#[test]
fn to_str() {
assert_eq!(StripeSubscriptionStatus::Active.to_str(), "active");
assert_eq!(StripeSubscriptionStatus::Canceled.to_str(), "canceled");
assert_eq!(StripeSubscriptionStatus::Incomplete.to_str(), "incomplete");
assert_eq!(StripeSubscriptionStatus::IncompleteExpired.to_str(), "incomplete_expired");
assert_eq!(StripeSubscriptionStatus::PastDue.to_str(), "past_due");
assert_eq!(StripeSubscriptionStatus::Trialing.to_str(), "trialing");
assert_eq!(StripeSubscriptionStatus::Unpaid.to_str(), "unpaid");
assert_eq!(StripeSubscriptionStatus::Paused.to_str(), "paused");
}
#[test]
fn from_str() {
assert_eq!(StripeSubscriptionStatus::from_str("active").unwrap(), StripeSubscriptionStatus::Active);
assert_eq!(StripeSubscriptionStatus::from_str("canceled").unwrap(), StripeSubscriptionStatus::Canceled);
assert_eq!(StripeSubscriptionStatus::from_str("incomplete").unwrap(), StripeSubscriptionStatus::Incomplete);
assert_eq!(StripeSubscriptionStatus::from_str("incomplete_expired").unwrap(), StripeSubscriptionStatus::IncompleteExpired);
assert_eq!(StripeSubscriptionStatus::from_str("past_due").unwrap(), StripeSubscriptionStatus::PastDue);
assert_eq!(StripeSubscriptionStatus::from_str("trialing").unwrap(), StripeSubscriptionStatus::Trialing);
assert_eq!(StripeSubscriptionStatus::from_str("unpaid").unwrap(), StripeSubscriptionStatus::Unpaid);
assert_eq!(StripeSubscriptionStatus::from_str("paused").unwrap(), StripeSubscriptionStatus::Paused);
}
#[test]
fn from_str_err() {
let result = StripeSubscriptionStatus::from_str("asdf");
assert!(result.is_err());
if let Err(EnumError::CouldNotConvertFromString(value)) = result {
assert_eq!(value, "asdf");
} else {
panic!("Expected EnumError::CouldNotConvertFromString");
}
}
#[test]
fn all_variants() {
let mut variants = StripeSubscriptionStatus::all_variants();
assert_eq!(variants.len(), 8);
assert_eq!(variants.pop_first(), Some(StripeSubscriptionStatus::Active));
assert_eq!(variants.pop_first(), Some(StripeSubscriptionStatus::Canceled));
assert_eq!(variants.pop_first(), Some(StripeSubscriptionStatus::Incomplete));
assert_eq!(variants.pop_first(), Some(StripeSubscriptionStatus::IncompleteExpired));
assert_eq!(variants.pop_first(), Some(StripeSubscriptionStatus::PastDue));
assert_eq!(variants.pop_first(), Some(StripeSubscriptionStatus::Trialing));
assert_eq!(variants.pop_first(), Some(StripeSubscriptionStatus::Unpaid));
assert_eq!(variants.pop_first(), Some(StripeSubscriptionStatus::Paused));
assert_eq!(variants.pop_first(), None);
}
}
mod mechanical_checks {
use crate::common::stripe_subscription_status::StripeSubscriptionStatus;
#[test]
fn variant_length() {
use strum::IntoEnumIterator;
assert_eq!(StripeSubscriptionStatus::all_variants().len(), StripeSubscriptionStatus::iter().len());
}
#[test]
fn round_trip() {
for variant in StripeSubscriptionStatus::all_variants() {
// Test to_str(), from_str(), Display, and Debug.
assert_eq!(variant, StripeSubscriptionStatus::from_str(variant.to_str()).unwrap());
assert_eq!(variant, StripeSubscriptionStatus::from_str(&format!("{}", variant)).unwrap());
assert_eq!(variant, StripeSubscriptionStatus::from_str(&format!("{:?}", variant)).unwrap());
}
}
#[test]
fn serialized_length_ok_for_database() {
const MAX_LENGTH : usize = 32;
for variant in StripeSubscriptionStatus::all_variants() {
let serialized = variant.to_str();
assert!(serialized.len() > 0, "variant {:?} is too short", variant);
assert!(serialized.len() <= MAX_LENGTH, "variant {:?} is too long", variant);
}
}
}
}
@@ -73,6 +73,7 @@ redis_common = { path = "../../../schema/database/redis_common" }
redis_schema = { path = "../../../schema/database/redis_schema" }
reusable_types = { path = "../../../schema/public/reusable_types" }
server_environment = { path = "../../../lib/server_environment" }
#shared_service_components.workspace = true
thumbnail_generator = { path = "../../../api_clients/thumbnail_generator" }
tokens.workspace = true
tts_common = { path = "../../../lib/tts_common" }
@@ -1,6 +1,6 @@
/// Full string matches that can not be used as usernames.
pub const RESERVED_USERNAMES : &str = include_str!("../../../../../../includes/binary_includes/usernames/reserved_usernames.txt");
pub const RESERVED_USERNAMES : &str = include_str!("../../../../../../includes/binary_includes/reserved_usernames.txt");
/// Substrings that are reserved and cannot be used as part of a username.
pub const RESERVED_SUBSTRINGS : &str = include_str!("../../../../../../includes/binary_includes/usernames/reserved_usernames_including.txt");
pub const RESERVED_SUBSTRINGS : &str = include_str!("../../../../../../includes/binary_includes/reserved_usernames_including.txt");
@@ -1,5 +1,65 @@
use utoipa::OpenApi;
use artcraft_api_defs::common::responses::job_details::JobDetailsLipsyncRequest;
use artcraft_api_defs::common::responses::job_details::JobDetailsLivePortraitRequest;
use artcraft_api_defs::common::responses::media_links::*;
use artcraft_api_defs::common::responses::simple_generic_json_success::SimpleGenericJsonSuccess;
use artcraft_api_defs::generate::image::edit::gpt_image_1_edit_image::*;
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageAspectRatio;
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageNumImages;
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageRequest;
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageResponse;
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageAspectRatio;
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageNumImages;
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageRequest;
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageResponse;
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageAspectRatio;
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageNumImages;
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageRequest;
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageResponse;
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageAspectRatio;
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageNumImages;
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageRequest;
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageResponse;
use artcraft_api_defs::generate::image::generate_gpt_image_1_text_to_image::GenerateGptImage1TextToImageImageQuality;
use artcraft_api_defs::generate::image::generate_gpt_image_1_text_to_image::GenerateGptImage1TextToImageImageSize;
use artcraft_api_defs::generate::image::generate_gpt_image_1_text_to_image::GenerateGptImage1TextToImageNumImages;
use artcraft_api_defs::generate::image::generate_gpt_image_1_text_to_image::GenerateGptImage1TextToImageRequest;
use artcraft_api_defs::generate::image::generate_gpt_image_1_text_to_image::GenerateGptImage1TextToImageResponse;
use artcraft_api_defs::generate::image::remove_image_background::RemoveImageBackgroundRequest;
use artcraft_api_defs::generate::image::remove_image_background::RemoveImageBackgroundResponse;
use artcraft_api_defs::generate::object::generate_hunyuan_2_0_image_to_3d::GenerateHunyuan20ImageTo3dRequest;
use artcraft_api_defs::generate::object::generate_hunyuan_2_0_image_to_3d::GenerateHunyuan20ImageTo3dResponse;
use artcraft_api_defs::generate::object::generate_hunyuan_2_1_image_to_3d::GenerateHunyuan21ImageTo3dRequest;
use artcraft_api_defs::generate::object::generate_hunyuan_2_1_image_to_3d::GenerateHunyuan21ImageTo3dResponse;
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProAspectRatio;
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProDuration;
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProImageToVideoRequest;
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProImageToVideoResponse;
use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::GenerateKling21MasterAspectRatio;
use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::GenerateKling21MasterDuration;
use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::GenerateKling21MasterImageToVideoRequest;
use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::GenerateKling21MasterImageToVideoResponse;
use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::GenerateKling21ProAspectRatio;
use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::GenerateKling21ProDuration;
use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::GenerateKling21ProImageToVideoRequest;
use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::GenerateKling21ProImageToVideoResponse;
use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteDuration;
use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteImageToVideoRequest;
use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteImageToVideoResponse;
use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteResolution;
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2AspectRatio;
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2Duration;
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2ImageToVideoRequest;
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2ImageToVideoResponse;
use artcraft_api_defs::jobs::list_session_jobs::*;
use artcraft_api_defs::media_file::delete_media_file::DeleteMediaFilePathInfo;
use artcraft_api_defs::media_file::delete_media_file::DeleteMediaFileRequest;
use artcraft_api_defs::prompts::create_prompt::CreatePromptRequest;
use artcraft_api_defs::prompts::create_prompt::CreatePromptResponse;
use billing_component::stripe::http_endpoints::checkout::create::stripe_create_checkout_session_error::CreateCheckoutSessionError;
use billing_component::stripe::http_endpoints::checkout::create::stripe_create_checkout_session_json_handler::*;
use billing_component::users::http_endpoints::list_active_user_subscriptions_handler::*;
use crate::http_server::common_requests::auto_product_category::AutoProductCategory;
use crate::http_server::common_requests::media_file_token_path_info::MediaFileTokenPathInfo;
use crate::http_server::common_responses::media::cover_image_links::CoverImageLinks;
@@ -77,7 +137,6 @@ use crate::http_server::endpoints::media_files::upload::upload_new_scene_media_f
use crate::http_server::endpoints::media_files::upload::upload_pmx::upload_pmx_media_file_handler::*;
use crate::http_server::endpoints::media_files::upload::upload_saved_scene_media_file_handler::*;
use crate::http_server::endpoints::media_files::upload::upload_scene_snapshot_media_file_handler::*;
use crate::http_server::endpoints::media_files::upload::upload_spz_media_file_handler::*;
use crate::http_server::endpoints::media_files::upload::upload_studio_shot::upload_studio_shot_media_file_handler::*;
use crate::http_server::endpoints::media_files::upload::upload_video_new::upload_new_video_media_file_handler::*;
use crate::http_server::endpoints::media_files::upload::upload_video_old::upload_video_media_file_handler::*;
@@ -89,8 +148,8 @@ use crate::http_server::endpoints::moderation::user_feature_flags::edit_user_fea
use crate::http_server::endpoints::prompts::get_prompt_handler::*;
use crate::http_server::endpoints::service::status_alert_handler::*;
use crate::http_server::endpoints::stats::get_unified_queue_stats_handler::*;
use crate::http_server::endpoints::studio_gen2::enqueue_studio_gen2_handler::EnqueueStudioGen2Request;
use crate::http_server::endpoints::studio_gen2::enqueue_studio_gen2_handler::*;
use crate::http_server::endpoints::studio_gen2::enqueue_studio_gen2_handler::EnqueueStudioGen2Request;
use crate::http_server::endpoints::tags::list_tags_for_entity_handler::*;
use crate::http_server::endpoints::tags::set_tags_for_entity_handler::*;
use crate::http_server::endpoints::tts::enqueue_infer_tts_handler::enqueue_infer_tts_handler::*;
@@ -122,66 +181,6 @@ use crate::http_server::endpoints::weights::list::list_weights_by_user_handler::
use crate::http_server::endpoints::weights::search::search_model_weights_impl::*;
use crate::http_server::endpoints::weights::update::set_model_weight_cover_image_handler::*;
use crate::http_server::endpoints::weights::update::update_weight_handler::*;
use artcraft_api_defs::common::responses::job_details::JobDetailsLipsyncRequest;
use artcraft_api_defs::common::responses::job_details::JobDetailsLivePortraitRequest;
use artcraft_api_defs::common::responses::media_links::*;
use artcraft_api_defs::common::responses::simple_generic_json_success::SimpleGenericJsonSuccess;
use artcraft_api_defs::generate::image::bg_removal::remove_image_background::RemoveImageBackgroundRequest;
use artcraft_api_defs::generate::image::bg_removal::remove_image_background::RemoveImageBackgroundResponse;
use artcraft_api_defs::generate::image::edit::gpt_image_1_edit_image::*;
use artcraft_api_defs::generate::image::text::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageAspectRatio;
use artcraft_api_defs::generate::image::text::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageNumImages;
use artcraft_api_defs::generate::image::text::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageRequest;
use artcraft_api_defs::generate::image::text::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageResponse;
use artcraft_api_defs::generate::image::text::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageAspectRatio;
use artcraft_api_defs::generate::image::text::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageNumImages;
use artcraft_api_defs::generate::image::text::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageRequest;
use artcraft_api_defs::generate::image::text::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageResponse;
use artcraft_api_defs::generate::image::text::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageAspectRatio;
use artcraft_api_defs::generate::image::text::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageNumImages;
use artcraft_api_defs::generate::image::text::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageRequest;
use artcraft_api_defs::generate::image::text::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageResponse;
use artcraft_api_defs::generate::image::text::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageAspectRatio;
use artcraft_api_defs::generate::image::text::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageNumImages;
use artcraft_api_defs::generate::image::text::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageRequest;
use artcraft_api_defs::generate::image::text::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageResponse;
use artcraft_api_defs::generate::image::text::generate_gpt_image_1_text_to_image::GenerateGptImage1TextToImageImageQuality;
use artcraft_api_defs::generate::image::text::generate_gpt_image_1_text_to_image::GenerateGptImage1TextToImageImageSize;
use artcraft_api_defs::generate::image::text::generate_gpt_image_1_text_to_image::GenerateGptImage1TextToImageNumImages;
use artcraft_api_defs::generate::image::text::generate_gpt_image_1_text_to_image::GenerateGptImage1TextToImageRequest;
use artcraft_api_defs::generate::image::text::generate_gpt_image_1_text_to_image::GenerateGptImage1TextToImageResponse;
use artcraft_api_defs::generate::object::generate_hunyuan_2_0_image_to_3d::GenerateHunyuan20ImageTo3dRequest;
use artcraft_api_defs::generate::object::generate_hunyuan_2_0_image_to_3d::GenerateHunyuan20ImageTo3dResponse;
use artcraft_api_defs::generate::object::generate_hunyuan_2_1_image_to_3d::GenerateHunyuan21ImageTo3dRequest;
use artcraft_api_defs::generate::object::generate_hunyuan_2_1_image_to_3d::GenerateHunyuan21ImageTo3dResponse;
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProAspectRatio;
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProDuration;
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProImageToVideoRequest;
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProImageToVideoResponse;
use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::GenerateKling21MasterAspectRatio;
use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::GenerateKling21MasterDuration;
use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::GenerateKling21MasterImageToVideoRequest;
use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::GenerateKling21MasterImageToVideoResponse;
use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::GenerateKling21ProAspectRatio;
use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::GenerateKling21ProDuration;
use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::GenerateKling21ProImageToVideoRequest;
use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::GenerateKling21ProImageToVideoResponse;
use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteDuration;
use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteImageToVideoRequest;
use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteImageToVideoResponse;
use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteResolution;
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2AspectRatio;
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2Duration;
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2ImageToVideoRequest;
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2ImageToVideoResponse;
use artcraft_api_defs::jobs::list_session_jobs::*;
use artcraft_api_defs::media_file::delete_media_file::DeleteMediaFilePathInfo;
use artcraft_api_defs::media_file::delete_media_file::DeleteMediaFileRequest;
use artcraft_api_defs::prompts::create_prompt::CreatePromptRequest;
use artcraft_api_defs::prompts::create_prompt::CreatePromptResponse;
use billing_component::stripe::http_endpoints::checkout::create::stripe_create_checkout_session_error::CreateCheckoutSessionError;
use billing_component::stripe::http_endpoints::checkout::create::stripe_create_checkout_session_json_handler::*;
use billing_component::users::http_endpoints::list_active_user_subscriptions_handler::*;
use enums::by_table::beta_keys::beta_key_product::BetaKeyProduct;
use enums::by_table::comments::comment_entity_type::CommentEntityType;
use enums::by_table::featured_items::featured_item_entity_type::FeaturedItemEntityType;
@@ -286,7 +285,6 @@ use tokens::tokens::zs_voice_datasets::*;
crate::http_server::endpoints::media_files::upload::upload_image_media_file_handler::upload_image_media_file_handler,
crate::http_server::endpoints::media_files::upload::upload_new_engine_asset_media_file_handler::upload_new_engine_asset_media_file_handler,
crate::http_server::endpoints::media_files::upload::upload_new_scene_media_file_handler::upload_new_scene_media_file_handler,
crate::http_server::endpoints::media_files::upload::upload_spz_media_file_handler::upload_spz_media_file_handler,
crate::http_server::endpoints::media_files::upload::upload_pmx::upload_pmx_media_file_handler::upload_pmx_media_file_handler,
crate::http_server::endpoints::media_files::upload::upload_saved_scene_media_file_handler::upload_saved_scene_media_file_handler,
crate::http_server::endpoints::media_files::upload::upload_scene_snapshot_media_file_handler::upload_scene_snapshot_media_file_handler,
@@ -755,8 +753,6 @@ use tokens::tokens::zs_voice_datasets::*;
UploadNewEngineAssetSuccessResponse,
UploadNewSceneMediaFileForm,
UploadNewSceneMediaFileSuccessResponse,
UploadSpzMediaFileForm,
UploadSpzMediaFileSuccessResponse,
UploadNewVideoMediaFileForm,
UploadNewVideoMediaFileSuccessResponse,
UploadPmxFileForm,
@@ -116,22 +116,6 @@ pub async fn log_app_active_user_handler(
os_platform: request.maybe_os_platform.as_deref(),
os_version: request.maybe_os_version.as_deref(),
session_duration_seconds: request.maybe_session_duration_seconds,
total_generation_count: 0,
image_generation_count: 0,
video_generation_count: 0,
object_generation_count: 0,
text_to_image_count: 0,
image_to_image_count: 0,
text_to_video_count: 0,
image_to_video_count: 0,
text_to_object_count: 0,
image_to_object_count: 0,
image_page_prompt_count: 0,
video_page_prompt_count: 0,
edit_page_prompt_count: 0,
stage_page_prompt_count: 0,
object_page_prompt_count: 0,
other_page_prompt_count: 0,
};
upsert.upsert_with_connection(&mut mysql_connection).await?;
@@ -1,149 +0,0 @@
use std::sync::Arc;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::state::server_state::ServerState;
use actix_helpers::extractors::get_request_user_agent::get_request_user_agent;
use actix_web::web::Json;
use actix_web::{web, HttpRequest};
use artcraft_api_defs::analytics::log_active_user::{LogAppActiveUserRequest, LogAppActiveUserResponse};
use enums::common::payments_namespace::PaymentsNamespace;
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;
const CLIENT_WAIT_FOR_RETRY_MILLIS: u64 = 1_000 * 60 * 1; // Every minute
/// Log an active app user (JSON body version) - user must be logged in.
#[utoipa::path(
post,
tag = "Analytics",
path = "/v1/analytics/active_user_v2",
request_body = LogAppActiveUserRequest,
responses(
(status = 200, description = "Success", body = LogAppActiveUserSuccessResponse),
),
)]
pub async fn log_app_active_user_json_handler(
http_request: HttpRequest,
request: Json<LogAppActiveUserRequest>,
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<LogAppActiveUserResponse>, CommonWebError>
{
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await?;
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
.await?;
let user_token = maybe_user_session
.ok_or(CommonWebError::NotAuthorized)?
.user_token;
let ip_address = get_request_ip(&http_request);
info!("Logging active user (JSON): {:?}", request);
let app_version = {
let user_agent = get_request_user_agent(&http_request);
let mut maybe_recorded_version = None;
if let Some(user_agent) = user_agent {
maybe_recorded_version = Some(user_agent.to_string());
};
let pair = (
request.maybe_app_name.as_deref(),
request.maybe_app_version.as_deref(),
);
match pair {
(Some(name), Some(version)) => {
let version = version.trim().to_string();
let name = name.trim().to_string();
if !version.is_empty() && !name.is_empty() {
maybe_recorded_version = Some(format!("{}/{}", name, version));
}
},
(Some(name), None) => {
let name = name.trim().to_string();
if !name.is_empty() {
maybe_recorded_version = Some(name);
}
},
(None, Some(version)) => {
let version = version.trim().to_string();
if !version.is_empty() {
maybe_recorded_version = Some(version);
}
},
_ => {}
}
maybe_recorded_version
};
let upsert = UpsertAnalyticsAppActiveUser {
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?;
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,
total_generation_count: request.total_generation_count.unwrap_or(0),
image_generation_count: request.image_generation_count.unwrap_or(0),
video_generation_count: request.video_generation_count.unwrap_or(0),
object_generation_count: request.object_generation_count.unwrap_or(0),
text_to_image_count: request.text_to_image_count.unwrap_or(0),
image_to_image_count: request.image_to_image_count.unwrap_or(0),
text_to_video_count: request.text_to_video_count.unwrap_or(0),
image_to_video_count: request.image_to_video_count.unwrap_or(0),
text_to_object_count: request.text_to_object_count.unwrap_or(0),
image_to_object_count: request.image_to_object_count.unwrap_or(0),
image_page_prompt_count: request.image_page_prompt_count.unwrap_or(0),
video_page_prompt_count: request.video_page_prompt_count.unwrap_or(0),
edit_page_prompt_count: request.edit_page_prompt_count.unwrap_or(0),
stage_page_prompt_count: request.stage_page_prompt_count.unwrap_or(0),
object_page_prompt_count: request.object_page_prompt_count.unwrap_or(0),
other_page_prompt_count: request.other_page_prompt_count.unwrap_or(0),
};
upsert.upsert_with_connection(&mut mysql_connection).await?;
}
Ok(Json(LogAppActiveUserResponse {
success: true,
wait_for_retry_millis: CLIENT_WAIT_FOR_RETRY_MILLIS,
}))
}
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(())
}
@@ -1,3 +1,2 @@
pub mod log_browser_session_handler;
pub mod log_app_active_user_handler;
pub mod log_app_active_user_json_handler;
pub mod log_app_active_user_handler;
@@ -14,8 +14,6 @@ use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptC
use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::model_type::ModelType;
use enums::common::payments_namespace::PaymentsNamespace;
use enums::common::stripe_subscription_status::StripeSubscriptionStatus;
use enums::common::visibility::Visibility;
use fal_client::creds::open_ai_api_key::OpenAiApiKey;
use fal_client::requests::webhook::image::edit::enqueue_gemini_25_flash_edit_webhook::{enqueue_gemini_25_flash_edit_webhook, Gemini25FlashEditArgs, Gemini25FlashEditNumImages};
@@ -31,7 +29,6 @@ use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_
use mysql_queries::queries::media_files::get::batch_get_media_files_by_tokens::{batch_get_media_files_by_tokens, batch_get_media_files_by_tokens_with_connection};
use mysql_queries::queries::prompt_context_items::insert_batch_prompt_context_items::{insert_batch_prompt_context_items, InsertBatchArgs, PromptContextItem};
use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs};
use mysql_queries::queries::users::user_subscriptions::find_subscription_for_owner_user::find_subscription_for_owner_user_using_connection;
use sqlx::Acquire;
use utoipa::ToSchema;
@@ -55,11 +52,7 @@ pub async fn gemini_25_flash_edit_image_handler(
) -> Result<Json<Gemini25FlashEditImageResponse>, CommonWebError> {
payments_error_test(&request.prompt.as_deref().unwrap_or(""))?;
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await?;
@@ -73,10 +66,6 @@ pub async fn gemini_25_flash_edit_image_handler(
CommonWebError::ServerError
})?;
let maybe_user_token = maybe_user_session
.as_ref()
.map(|s| &s.user_token);
let maybe_avt_token = server_state
.avt_cookie_manager
.get_avt_token_from_request(&http_request);
@@ -91,30 +80,10 @@ pub async fn gemini_25_flash_edit_image_handler(
// }
//};
let mut downgrade_for_free_user = true;
if let Some(user_token) = maybe_user_token.as_ref() {
let result = find_subscription_for_owner_user_using_connection(
user_token,
PaymentsNamespace::Artcraft,
&mut mysql_connection,
).await;
if let Ok(Some(subscription)) = result {
info!("User {:?} has subscription: {:?} (stripe customer: {:?}, status: {:?})",
user_token,
subscription.token,
subscription.stripe_customer_id,
subscription.stripe_subscription_status);
// NB: Failing open means subscribers might get fewer results, but they're free right now.
if subscription.stripe_subscription_status == StripeSubscriptionStatus::Active {
downgrade_for_free_user = false;
}
}
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
info!("downgrade_for_free_user: {}", downgrade_for_free_user);
const CAN_SEE_DELETED: bool = false;
let tokens = match request.image_media_tokens.as_ref() {
@@ -173,7 +142,7 @@ pub async fn gemini_25_flash_edit_image_handler(
info!("Fal webhook URL: {}", server_state.fal.webhook_url);
let mut num_images = match request.num_images {
let num_images = match request.num_images {
Some(Gemini25FlashEditImageNumImages::One) => Gemini25FlashEditNumImages::One,
Some(Gemini25FlashEditImageNumImages::Two) => Gemini25FlashEditNumImages::Two,
Some(Gemini25FlashEditImageNumImages::Three) => Gemini25FlashEditNumImages::Three,
@@ -181,10 +150,6 @@ pub async fn gemini_25_flash_edit_image_handler(
None => Gemini25FlashEditNumImages::One, // Default to One
};
if downgrade_for_free_user {
num_images = Gemini25FlashEditNumImages::One;
}
let args = Gemini25FlashEditArgs {
image_urls,
prompt: request.prompt.as_deref().unwrap_or(""),
@@ -223,7 +188,9 @@ pub async fn gemini_25_flash_edit_image_handler(
let prompt_result = insert_prompt(InsertPromptArgs {
maybe_apriori_prompt_token: None,
prompt_type: PromptType::ArtcraftApp,
maybe_creator_user_token: maybe_user_token,
maybe_creator_user_token: maybe_user_session
.as_ref()
.map(|s| &s.user_token),
maybe_model_type: Some(ModelType::Gemini25Flash),
maybe_generation_provider: Some(GenerationProvider::Artcraft),
maybe_positive_prompt: request.prompt.as_deref(),
@@ -266,7 +233,7 @@ pub async fn gemini_25_flash_edit_image_handler(
fal_category: FalCategory::ImageGeneration,
maybe_inference_args: None,
maybe_prompt_token: prompt_token.as_ref(),
maybe_creator_user_token: maybe_user_token,
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
@@ -14,8 +14,6 @@ use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptC
use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::model_type::ModelType;
use enums::common::payments_namespace::PaymentsNamespace;
use enums::common::stripe_subscription_status::StripeSubscriptionStatus;
use enums::common::visibility::Visibility;
use fal_client::creds::open_ai_api_key::OpenAiApiKey;
use fal_client::requests::webhook::image::edit::enqueue_gpt_image_1_edit_image_webhook::enqueue_gpt_image_1_edit_image_webhook;
@@ -30,7 +28,6 @@ use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_
use mysql_queries::queries::media_files::get::batch_get_media_files_by_tokens::{batch_get_media_files_by_tokens, batch_get_media_files_by_tokens_with_connection};
use mysql_queries::queries::prompt_context_items::insert_batch_prompt_context_items::{insert_batch_prompt_context_items, InsertBatchArgs, PromptContextItem};
use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs};
use mysql_queries::queries::users::user_subscriptions::find_subscription_for_owner_user::find_subscription_for_owner_user_using_connection;
use sqlx::Acquire;
use utoipa::ToSchema;
@@ -54,10 +51,6 @@ pub async fn gpt_image_1_edit_image_handler(
payments_error_test(&request.prompt.as_deref().unwrap_or(""))?;
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await?;
@@ -74,10 +67,6 @@ pub async fn gpt_image_1_edit_image_handler(
let maybe_avt_token = server_state
.avt_cookie_manager
.get_avt_token_from_request(&http_request);
let maybe_user_token = maybe_user_session
.as_ref()
.map(|s| &s.user_token);
// TODO: Limit usage for new accounts. Billing, free credits metering, etc.
@@ -89,30 +78,10 @@ pub async fn gpt_image_1_edit_image_handler(
// }
//};
let mut downgrade_for_free_user = true;
if let Some(user_token) = maybe_user_token.as_ref() {
let result = find_subscription_for_owner_user_using_connection(
user_token,
PaymentsNamespace::Artcraft,
&mut mysql_connection,
).await;
if let Ok(Some(subscription)) = result {
info!("User {:?} has subscription: {:?} (stripe customer: {:?}, status: {:?})",
user_token,
subscription.token,
subscription.stripe_customer_id,
subscription.stripe_subscription_status);
// NB: Failing open means subscribers might get fewer results, but they're free right now.
if subscription.stripe_subscription_status == StripeSubscriptionStatus::Active {
downgrade_for_free_user = false;
}
}
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
info!("downgrade_for_free_user: {}", downgrade_for_free_user);
const CAN_SEE_DELETED: bool = false;
let tokens = match request.image_media_tokens.as_ref() {
@@ -178,7 +147,7 @@ pub async fn gpt_image_1_edit_image_handler(
None => GptEditImageSize::Square, // Default to Square
};
let mut num_images = match request.num_images {
let num_images = match request.num_images {
Some(GptImage1EditImageNumImages::One) => GptEditImageNumImages::One,
Some(GptImage1EditImageNumImages::Two) => GptEditImageNumImages::Two,
Some(GptImage1EditImageNumImages::Three) => GptEditImageNumImages::Three,
@@ -198,10 +167,6 @@ pub async fn gpt_image_1_edit_image_handler(
let openai_api_key = OpenAiApiKey::from_str(&server_state.openai.api_key);
if downgrade_for_free_user {
num_images = GptEditImageNumImages::One;
}
let args = GptEditImageByokArgs {
image_urls,
prompt: request.prompt.as_deref().unwrap_or(""),
@@ -242,7 +207,9 @@ pub async fn gpt_image_1_edit_image_handler(
let prompt_result = insert_prompt(InsertPromptArgs {
maybe_apriori_prompt_token: None,
prompt_type: PromptType::ArtcraftApp,
maybe_creator_user_token: maybe_user_token,
maybe_creator_user_token: maybe_user_session
.as_ref()
.map(|s| &s.user_token),
maybe_model_type: Some(ModelType::GptImage1),
maybe_generation_provider: Some(GenerationProvider::Artcraft),
maybe_positive_prompt: request.prompt.as_deref(),
@@ -285,7 +252,7 @@ pub async fn gpt_image_1_edit_image_handler(
fal_category: FalCategory::ImageGeneration,
maybe_inference_args: None,
maybe_prompt_token: prompt_token.as_ref(),
maybe_creator_user_token: maybe_user_token,
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
@@ -14,8 +14,6 @@ use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptC
use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::model_type::ModelType;
use enums::common::payments_namespace::PaymentsNamespace;
use enums::common::stripe_subscription_status::StripeSubscriptionStatus;
use enums::common::visibility::Visibility;
use fal_client::creds::open_ai_api_key::OpenAiApiKey;
use fal_client::requests::webhook::image::enqueue_gpt_image_1_text_to_image_webhook::{enqueue_gpt_image_1_text_to_image_webhook, GptTextToImageByokArgs, GptTextToImageNumImages, GptTextToImageQuality, GptTextToImageSize};
@@ -28,7 +26,6 @@ use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_
use mysql_queries::queries::media_files::get::batch_get_media_files_by_tokens::{batch_get_media_files_by_tokens, batch_get_media_files_by_tokens_with_connection};
use mysql_queries::queries::prompt_context_items::insert_batch_prompt_context_items::{insert_batch_prompt_context_items, InsertBatchArgs, PromptContextItem};
use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs};
use mysql_queries::queries::users::user_subscriptions::find_subscription_for_owner_user::find_subscription_for_owner_user_using_connection;
use sqlx::Acquire;
use utoipa::ToSchema;
@@ -51,10 +48,6 @@ pub async fn generate_gpt_image_1_text_to_image_handler(
) -> Result<Json<GenerateGptImage1TextToImageResponse>, CommonWebError> {
payments_error_test(&request.prompt.as_deref().unwrap_or(""))?;
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
let mut mysql_connection = server_state.mysql_pool
.acquire()
@@ -73,10 +66,6 @@ pub async fn generate_gpt_image_1_text_to_image_handler(
.avt_cookie_manager
.get_avt_token_from_request(&http_request);
let maybe_user_token = maybe_user_session
.as_ref()
.map(|s| &s.user_token);
// TODO: Limit usage for new accounts. Billing, free credits metering, etc.
//let user_session = match maybe_user_session {
@@ -86,31 +75,11 @@ pub async fn generate_gpt_image_1_text_to_image_handler(
// return Err(CommonWebError::NotAuthorized);
// }
//};
let mut downgrade_for_free_user = true;
if let Some(user_token) = maybe_user_token.as_ref() {
let result = find_subscription_for_owner_user_using_connection(
user_token,
PaymentsNamespace::Artcraft,
&mut mysql_connection,
).await;
if let Ok(Some(subscription)) = result {
info!("User {:?} has subscription: {:?} (stripe customer: {:?}, status: {:?})",
user_token,
subscription.token,
subscription.stripe_customer_id,
subscription.stripe_subscription_status);
// NB: Failing open means subscribers might get fewer results, but they're free right now.
if subscription.stripe_subscription_status == StripeSubscriptionStatus::Active {
downgrade_for_free_user = false;
}
}
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
info!("downgrade_for_free_user: {}", downgrade_for_free_user);
const CAN_SEE_DELETED: bool = false;
let media_domain = get_media_domain(&http_request);
@@ -131,7 +100,7 @@ pub async fn generate_gpt_image_1_text_to_image_handler(
None => GptTextToImageSize::Square, // Default to Square
};
let mut num_images = match request.num_images {
let num_images = match request.num_images {
Some(GenerateGptImage1TextToImageNumImages::One) => GptTextToImageNumImages::One,
Some(GenerateGptImage1TextToImageNumImages::Two) => GptTextToImageNumImages::Two,
Some(GenerateGptImage1TextToImageNumImages::Three) => GptTextToImageNumImages::Three,
@@ -148,10 +117,6 @@ pub async fn generate_gpt_image_1_text_to_image_handler(
},
None => GptTextToImageQuality::High, // Default to High
};
if downgrade_for_free_user {
num_images = GptTextToImageNumImages::One;
}
let openai_api_key = OpenAiApiKey::from_str(&server_state.openai.api_key);
@@ -194,7 +159,9 @@ pub async fn generate_gpt_image_1_text_to_image_handler(
let prompt_result = insert_prompt(InsertPromptArgs {
maybe_apriori_prompt_token: None,
prompt_type: PromptType::ArtcraftApp,
maybe_creator_user_token: maybe_user_token,
maybe_creator_user_token: maybe_user_session
.as_ref()
.map(|s| &s.user_token),
maybe_model_type: Some(ModelType::GptImage1),
maybe_generation_provider: Some(GenerationProvider::Artcraft),
maybe_positive_prompt: request.prompt.as_deref(),
@@ -219,7 +186,7 @@ pub async fn generate_gpt_image_1_text_to_image_handler(
fal_category: FalCategory::ImageGeneration,
maybe_inference_args: None,
maybe_prompt_token: prompt_token.as_ref(),
maybe_creator_user_token: maybe_user_token,
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
@@ -17,8 +17,6 @@ use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptC
use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::model_type::ModelType;
use enums::common::payments_namespace::PaymentsNamespace;
use enums::common::stripe_subscription_status::StripeSubscriptionStatus;
use enums::common::visibility::Visibility;
use fal_client::creds::open_ai_api_key::OpenAiApiKey;
use fal_client::requests::webhook::image::edit::enqueue_gpt_image_1p5_edit_image_webhook::{enqueue_gpt_image_1p5_image_edit_webhook, EnqueueGptImage1p5EditImageArgs, EnqueueGptImage1p5EditImageBackground, EnqueueGptImage1p5EditImageInputFidelity, EnqueueGptImage1p5EditImageNumImages, EnqueueGptImage1p5EditImageQuality, EnqueueGptImage1p5EditImageSize};
@@ -32,7 +30,6 @@ use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_
use mysql_queries::queries::media_files::get::batch_get_media_files_by_tokens::{batch_get_media_files_by_tokens, batch_get_media_files_by_tokens_with_connection};
use mysql_queries::queries::prompt_context_items::insert_batch_prompt_context_items::{insert_batch_prompt_context_items, InsertBatchArgs, PromptContextItem};
use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs};
use mysql_queries::queries::users::user_subscriptions::find_subscription_for_owner_user::find_subscription_for_owner_user_using_connection;
use server_environment::ServerEnvironment;
use sqlx::pool::PoolConnection;
use sqlx::{Acquire, MySql};
@@ -58,10 +55,6 @@ pub async fn gpt_image_1p5_multi_function_image_gen_handler(
) -> Result<Json<GptImage1p5MultiFunctionImageGenResponse>, CommonWebError> {
payments_error_test(&request.prompt.as_deref().unwrap_or(""))?;
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
let mut mysql_connection = server_state.mysql_pool
.acquire()
@@ -80,10 +73,6 @@ pub async fn gpt_image_1p5_multi_function_image_gen_handler(
.avt_cookie_manager
.get_avt_token_from_request(&http_request);
let maybe_user_token = maybe_user_session
.as_ref()
.map(|s| &s.user_token);
// TODO: Limit usage for new accounts. Billing, free credits metering, etc.
//let user_session = match maybe_user_session {
@@ -94,30 +83,10 @@ pub async fn gpt_image_1p5_multi_function_image_gen_handler(
// }
//};
let mut downgrade_for_free_user = true;
if let Some(user_token) = maybe_user_token.as_ref() {
let result = find_subscription_for_owner_user_using_connection(
user_token,
PaymentsNamespace::Artcraft,
&mut mysql_connection,
).await;
if let Ok(Some(subscription)) = result {
info!("User {:?} has subscription: {:?} (stripe customer: {:?}, status: {:?})",
user_token,
subscription.token,
subscription.stripe_customer_id,
subscription.stripe_subscription_status);
// NB: Failing open means subscribers might get fewer results, but they're free right now.
if subscription.stripe_subscription_status == StripeSubscriptionStatus::Active {
downgrade_for_free_user = false;
}
}
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
info!("downgrade_for_free_user: {}", downgrade_for_free_user);
let mut query_media_tokens = None;
if let Some(media_tokens) = request.image_media_tokens.as_ref() {
@@ -179,7 +148,7 @@ pub async fn gpt_image_1p5_multi_function_image_gen_handler(
if let Some(input_image_urls) = maybe_image_urls.as_deref() {
info!("gpt image 1.5 edit image");
let mut num_images = match request.num_images {
let num_images = match request.num_images {
Some(GptImage1p5MultiFunctionImageGenNumImages::One) => EnqueueGptImage1p5EditImageNumImages::One,
Some(GptImage1p5MultiFunctionImageGenNumImages::Two) => EnqueueGptImage1p5EditImageNumImages::Two,
Some(GptImage1p5MultiFunctionImageGenNumImages::Three) => EnqueueGptImage1p5EditImageNumImages::Three,
@@ -214,10 +183,6 @@ pub async fn gpt_image_1p5_multi_function_image_gen_handler(
None => EnqueueGptImage1p5EditImageInputFidelity::High,
};
if downgrade_for_free_user {
num_images = EnqueueGptImage1p5EditImageNumImages::One;
}
let args = EnqueueGptImage1p5EditImageArgs {
prompt: request.prompt.as_deref().unwrap_or(""),
image_urls: input_image_urls.to_owned(),
@@ -242,7 +207,7 @@ pub async fn gpt_image_1p5_multi_function_image_gen_handler(
} else {
info!("gpt image 1.5 text-to-image");
let mut num_images = match request.num_images {
let num_images = match request.num_images {
Some(GptImage1p5MultiFunctionImageGenNumImages::One) => EnqueueGptImage1p5TextToImageNumImages::One,
Some(GptImage1p5MultiFunctionImageGenNumImages::Two) => EnqueueGptImage1p5TextToImageNumImages::Two,
Some(GptImage1p5MultiFunctionImageGenNumImages::Three) => EnqueueGptImage1p5TextToImageNumImages::Three,
@@ -271,10 +236,6 @@ pub async fn gpt_image_1p5_multi_function_image_gen_handler(
None => EnqueueGptImage1p5TextToImageQuality::High,
};
if downgrade_for_free_user {
num_images = EnqueueGptImage1p5TextToImageNumImages::One;
}
let args = EnqueueGptImage1p5TextToImageArgs {
prompt: request.prompt.as_deref().unwrap_or(""),
num_images,
@@ -316,7 +277,9 @@ pub async fn gpt_image_1p5_multi_function_image_gen_handler(
let prompt_result = insert_prompt(InsertPromptArgs {
maybe_apriori_prompt_token: None,
prompt_type: PromptType::ArtcraftApp,
maybe_creator_user_token: maybe_user_token,
maybe_creator_user_token: maybe_user_session
.as_ref()
.map(|s| &s.user_token),
maybe_model_type: Some(ModelType::GptImage1p5),
maybe_generation_provider: Some(GenerationProvider::Artcraft),
maybe_positive_prompt: request.prompt.as_deref(),
@@ -361,7 +324,7 @@ pub async fn gpt_image_1p5_multi_function_image_gen_handler(
fal_category: FalCategory::ImageGeneration,
maybe_inference_args: None,
maybe_prompt_token: prompt_token.as_ref(),
maybe_creator_user_token: maybe_user_token,
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
@@ -18,8 +18,6 @@ use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptC
use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::model_type::ModelType;
use enums::common::payments_namespace::PaymentsNamespace;
use enums::common::stripe_subscription_status::StripeSubscriptionStatus;
use enums::common::visibility::Visibility;
use fal_client::creds::open_ai_api_key::OpenAiApiKey;
use fal_client::requests::webhook::image::edit::enqueue_gemini_25_flash_edit_webhook::{enqueue_gemini_25_flash_edit_webhook, Gemini25FlashEditArgs, Gemini25FlashEditAspectRatio, Gemini25FlashEditNumImages};
@@ -35,7 +33,6 @@ use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_
use mysql_queries::queries::media_files::get::batch_get_media_files_by_tokens::{batch_get_media_files_by_tokens, batch_get_media_files_by_tokens_with_connection};
use mysql_queries::queries::prompt_context_items::insert_batch_prompt_context_items::{insert_batch_prompt_context_items, InsertBatchArgs, PromptContextItem};
use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs};
use mysql_queries::queries::users::user_subscriptions::find_subscription_for_owner_user::find_subscription_for_owner_user_using_connection;
use server_environment::ServerEnvironment;
use sqlx::pool::PoolConnection;
use sqlx::{Acquire, MySql};
@@ -61,10 +58,6 @@ pub async fn nano_banana_multi_function_image_gen_handler(
) -> Result<Json<NanoBananaMultiFunctionImageGenResponse>, CommonWebError> {
payments_error_test(&request.prompt.as_deref().unwrap_or(""))?;
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
let mut mysql_connection = server_state.mysql_pool
.acquire()
@@ -83,10 +76,6 @@ pub async fn nano_banana_multi_function_image_gen_handler(
.avt_cookie_manager
.get_avt_token_from_request(&http_request);
let maybe_user_token = maybe_user_session
.as_ref()
.map(|s| &s.user_token);
// TODO: Limit usage for new accounts. Billing, free credits metering, etc.
//let user_session = match maybe_user_session {
@@ -97,29 +86,9 @@ pub async fn nano_banana_multi_function_image_gen_handler(
// }
//};
let mut downgrade_for_free_user = true;
if let Some(user_token) = maybe_user_token.as_ref() {
let result = find_subscription_for_owner_user_using_connection(
user_token,
PaymentsNamespace::Artcraft,
&mut mysql_connection,
).await;
if let Ok(Some(subscription)) = result {
info!("User {:?} has subscription: {:?} (stripe customer: {:?}, status: {:?})",
user_token,
subscription.token,
subscription.stripe_customer_id,
subscription.stripe_subscription_status);
// NB: Failing open means subscribers might get fewer results, but they're free right now.
if subscription.stripe_subscription_status == StripeSubscriptionStatus::Active {
downgrade_for_free_user = false;
}
}
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
info!("downgrade_for_free_user: {}", downgrade_for_free_user);
let image_urls = match request.image_media_tokens.as_ref() {
Some(media_tokens) => {
@@ -148,7 +117,7 @@ pub async fn nano_banana_multi_function_image_gen_handler(
if let Some(input_image_urls) = image_urls.as_deref() {
info!("nano banana edit image");
let mut num_images = match request.num_images {
let num_images = match request.num_images {
Some(NanoBananaMultiFunctionImageGenNumImages::One) => Gemini25FlashEditNumImages::One,
Some(NanoBananaMultiFunctionImageGenNumImages::Two) => Gemini25FlashEditNumImages::Two,
Some(NanoBananaMultiFunctionImageGenNumImages::Three) => Gemini25FlashEditNumImages::Three,
@@ -171,10 +140,6 @@ pub async fn nano_banana_multi_function_image_gen_handler(
None => Gemini25FlashEditAspectRatio::OneByOne,
};
if downgrade_for_free_user {
num_images = Gemini25FlashEditNumImages::One;
}
let args = Gemini25FlashEditArgs {
prompt: request.prompt.as_deref().unwrap_or(""),
image_urls: input_image_urls.to_owned(),
@@ -194,7 +159,7 @@ pub async fn nano_banana_multi_function_image_gen_handler(
} else {
info!("nano banana text-to-image");
let mut num_images = match request.num_images {
let num_images = match request.num_images {
Some(NanoBananaMultiFunctionImageGenNumImages::One) => Gemini25FlashTextToImageNumImages::One,
Some(NanoBananaMultiFunctionImageGenNumImages::Two) => Gemini25FlashTextToImageNumImages::Two,
Some(NanoBananaMultiFunctionImageGenNumImages::Three) => Gemini25FlashTextToImageNumImages::Three,
@@ -217,10 +182,6 @@ pub async fn nano_banana_multi_function_image_gen_handler(
None => Gemini25FlashTextToImageAspectRatio::OneByOne,
};
if downgrade_for_free_user {
num_images = Gemini25FlashTextToImageNumImages::One;
}
let args = Gemini25FlashTextToImageArgs {
prompt: request.prompt.as_deref().unwrap_or(""),
num_images,
@@ -259,7 +220,9 @@ pub async fn nano_banana_multi_function_image_gen_handler(
let prompt_result = insert_prompt(InsertPromptArgs {
maybe_apriori_prompt_token: None,
prompt_type: PromptType::ArtcraftApp,
maybe_creator_user_token: maybe_user_token,
maybe_creator_user_token: maybe_user_session
.as_ref()
.map(|s| &s.user_token),
maybe_model_type: Some(ModelType::NanoBanana),
maybe_generation_provider: Some(GenerationProvider::Artcraft),
maybe_positive_prompt: request.prompt.as_deref(),
@@ -304,7 +267,7 @@ pub async fn nano_banana_multi_function_image_gen_handler(
fal_category: FalCategory::ImageGeneration,
maybe_inference_args: None,
maybe_prompt_token: prompt_token.as_ref(),
maybe_creator_user_token: maybe_user_token,
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
@@ -17,8 +17,6 @@ use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptC
use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::model_type::ModelType;
use enums::common::payments_namespace::PaymentsNamespace;
use enums::common::stripe_subscription_status::StripeSubscriptionStatus;
use enums::common::visibility::Visibility;
use fal_client::creds::open_ai_api_key::OpenAiApiKey;
use fal_client::requests::webhook::image::edit::enqueue_nano_banana_pro_edit_image_webhook::{enqueue_nano_banana_pro_image_edit_webhook, EnqueueNanoBananaProEditImageArgs, EnqueueNanoBananaProEditImageAspectRatio, EnqueueNanoBananaProEditImageNumImages, EnqueueNanoBananaProEditImageResolution};
@@ -32,7 +30,6 @@ use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_
use mysql_queries::queries::media_files::get::batch_get_media_files_by_tokens::{batch_get_media_files_by_tokens, batch_get_media_files_by_tokens_with_connection};
use mysql_queries::queries::prompt_context_items::insert_batch_prompt_context_items::{insert_batch_prompt_context_items, InsertBatchArgs, PromptContextItem};
use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs};
use mysql_queries::queries::users::user_subscriptions::find_subscription_for_owner_user::find_subscription_for_owner_user_using_connection;
use server_environment::ServerEnvironment;
use sqlx::pool::PoolConnection;
use sqlx::{Acquire, MySql};
@@ -58,11 +55,7 @@ pub async fn nano_banana_pro_multi_function_image_gen_handler(
) -> Result<Json<NanoBananaProMultiFunctionImageGenResponse>, CommonWebError> {
payments_error_test(&request.prompt.as_deref().unwrap_or(""))?;
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await?;
@@ -80,10 +73,6 @@ pub async fn nano_banana_pro_multi_function_image_gen_handler(
.avt_cookie_manager
.get_avt_token_from_request(&http_request);
let maybe_user_token = maybe_user_session
.as_ref()
.map(|s| &s.user_token);
// TODO: Limit usage for new accounts. Billing, free credits metering, etc.
//let user_session = match maybe_user_session {
@@ -94,30 +83,10 @@ pub async fn nano_banana_pro_multi_function_image_gen_handler(
// }
//};
let mut downgrade_for_free_user = true;
if let Some(user_token) = maybe_user_token.as_ref() {
let result = find_subscription_for_owner_user_using_connection(
user_token,
PaymentsNamespace::Artcraft,
&mut mysql_connection,
).await;
if let Ok(Some(subscription)) = result {
info!("User {:?} has subscription: {:?} (stripe customer: {:?}, status: {:?})",
user_token,
subscription.token,
subscription.stripe_customer_id,
subscription.stripe_subscription_status);
// NB: Failing open means subscribers might get fewer results, but they're free right now.
if subscription.stripe_subscription_status == StripeSubscriptionStatus::Active {
downgrade_for_free_user = false;
}
}
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
info!("downgrade_for_free_user: {}", downgrade_for_free_user);
let image_urls = match request.image_media_tokens.as_ref() {
Some(media_tokens) => {
info!("Looking up image media tokens: {:?}", media_tokens);
@@ -145,7 +114,7 @@ pub async fn nano_banana_pro_multi_function_image_gen_handler(
if let Some(input_image_urls) = image_urls.as_deref() {
info!("nano banana pro edit image");
let mut num_images = match request.num_images {
let num_images = match request.num_images {
Some(NanoBananaProMultiFunctionImageGenNumImages::One) => EnqueueNanoBananaProEditImageNumImages::One,
Some(NanoBananaProMultiFunctionImageGenNumImages::Two) => EnqueueNanoBananaProEditImageNumImages::Two,
Some(NanoBananaProMultiFunctionImageGenNumImages::Three) => EnqueueNanoBananaProEditImageNumImages::Three,
@@ -153,7 +122,7 @@ pub async fn nano_banana_pro_multi_function_image_gen_handler(
None => EnqueueNanoBananaProEditImageNumImages::One, // Default to One
};
let mut resolution = match request.resolution {
let resolution = match request.resolution {
Some(NanoBananaProMultiFunctionImageGenImageResolution::OneK) => EnqueueNanoBananaProEditImageResolution::OneK,
Some(NanoBananaProMultiFunctionImageGenImageResolution::TwoK) => EnqueueNanoBananaProEditImageResolution::TwoK,
Some(NanoBananaProMultiFunctionImageGenImageResolution::FourK) => EnqueueNanoBananaProEditImageResolution::FourK,
@@ -175,11 +144,6 @@ pub async fn nano_banana_pro_multi_function_image_gen_handler(
None => EnqueueNanoBananaProEditImageAspectRatio::OneByOne,
};
if downgrade_for_free_user {
num_images = EnqueueNanoBananaProEditImageNumImages::One;
resolution = EnqueueNanoBananaProEditImageResolution::OneK;
}
let args = EnqueueNanoBananaProEditImageArgs {
prompt: request.prompt.as_deref().unwrap_or(""),
image_urls: input_image_urls.to_owned(),
@@ -200,7 +164,7 @@ pub async fn nano_banana_pro_multi_function_image_gen_handler(
} else {
info!("nano banana pro text-to-image");
let mut num_images = match request.num_images {
let num_images = match request.num_images {
Some(NanoBananaProMultiFunctionImageGenNumImages::One) => EnqueueNanoBananaProTextToImageNumImages::One,
Some(NanoBananaProMultiFunctionImageGenNumImages::Two) => EnqueueNanoBananaProTextToImageNumImages::Two,
Some(NanoBananaProMultiFunctionImageGenNumImages::Three) => EnqueueNanoBananaProTextToImageNumImages::Three,
@@ -208,7 +172,7 @@ pub async fn nano_banana_pro_multi_function_image_gen_handler(
None => EnqueueNanoBananaProTextToImageNumImages::One, // Default to One
};
let mut resolution = match request.resolution {
let resolution = match request.resolution {
Some(NanoBananaProMultiFunctionImageGenImageResolution::OneK) => EnqueueNanoBananaProTextToImageResolution::OneK,
Some(NanoBananaProMultiFunctionImageGenImageResolution::TwoK) => EnqueueNanoBananaProTextToImageResolution::TwoK,
Some(NanoBananaProMultiFunctionImageGenImageResolution::FourK) => EnqueueNanoBananaProTextToImageResolution::FourK,
@@ -230,11 +194,6 @@ pub async fn nano_banana_pro_multi_function_image_gen_handler(
None => EnqueueNanoBananaProTextToImageAspectRatio::OneByOne,
};
if downgrade_for_free_user {
num_images = EnqueueNanoBananaProTextToImageNumImages::One;
resolution = EnqueueNanoBananaProTextToImageResolution::OneK;
}
let args = EnqueueNanoBananaProTextToImageArgs {
prompt: request.prompt.as_deref().unwrap_or(""),
num_images,
@@ -274,7 +233,9 @@ pub async fn nano_banana_pro_multi_function_image_gen_handler(
let prompt_result = insert_prompt(InsertPromptArgs {
maybe_apriori_prompt_token: None,
prompt_type: PromptType::ArtcraftApp,
maybe_creator_user_token: maybe_user_token,
maybe_creator_user_token: maybe_user_session
.as_ref()
.map(|s| &s.user_token),
maybe_model_type: Some(ModelType::NanoBananaPro),
maybe_generation_provider: Some(GenerationProvider::Artcraft),
maybe_positive_prompt: request.prompt.as_deref(),
@@ -319,7 +280,7 @@ pub async fn nano_banana_pro_multi_function_image_gen_handler(
fal_category: FalCategory::ImageGeneration,
maybe_inference_args: None,
maybe_prompt_token: prompt_token.as_ref(),
maybe_creator_user_token: maybe_user_token,
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
@@ -5,7 +5,6 @@ pub mod upload_generic;
pub mod upload_image_media_file_handler;
pub mod upload_new_engine_asset_media_file_handler;
pub mod upload_new_scene_media_file_handler;
pub mod upload_spz_media_file_handler;
pub mod upload_pmx;
pub mod upload_saved_scene_media_file_handler;
pub mod upload_scene_snapshot_media_file_handler;
@@ -1,273 +0,0 @@
use std::io::Read;
use std::path::PathBuf;
use std::sync::Arc;
use actix_multipart::form::tempfile::TempFile;
use actix_multipart::form::text::Text;
use actix_multipart::form::MultipartForm;
use actix_web::web::Json;
use actix_web::{web, HttpRequest};
use log::{error, info, warn};
use utoipa::ToSchema;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use enums::by_table::media_files::media_file_class::MediaFileClass;
use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory;
use enums::by_table::media_files::media_file_type::MediaFileType;
use enums::common::visibility::Visibility;
use hashing::sha256::sha256_hash_bytes::sha256_hash_bytes;
use http_server_common::request::get_request_ip::get_request_ip;
use mimetypes::mimetype_for_bytes::get_mimetype_for_bytes;
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
use mysql_queries::queries::media_files::create::insert_builder::media_file_insert_builder::MediaFileInsertBuilder;
use mysql_queries::queries::media_files::create::specialized_insert::insert_media_file_from_file_upload::{insert_media_file_from_file_upload, InsertMediaFileFromUploadArgs, UploadType};
use tokens::tokens::media_files::MediaFileToken;
use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::state::server_state::ServerState;
const GZIP_MIME_TYPE: &str = "application/gzip";
const SPZ_EXTENSION: &str = ".spz";
const CERAMIC_SPZ_EXTENSION: &str = ".ceramic.spz";
/// Form-multipart request fields for SPZ (Gaussian Splat) upload.
///
/// IF VIEWING DOCS, PLEASE SEE BOTTOM OF PAGE `UploadSpzMediaFileForm` (Under "Schema") FOR DETAILS ON FIELDS AND NULLABILITY.
#[derive(MultipartForm, ToSchema)]
#[multipart(duplicate_field = "deny")]
pub struct UploadSpzMediaFileForm {
/// UUID for request idempotency
#[multipart(limit = "2 KiB")]
#[schema(value_type = String, format = Binary)]
uuid_idempotency_token: Text<String>,
/// The uploaded SPZ file
#[multipart(limit = "512 MiB")]
#[schema(value_type = Vec<u8>, format = Binary)]
file: TempFile,
/// Optional: Title (name) of the Gaussian splat
#[multipart(limit = "2 KiB")]
#[schema(value_type = Option<String>, format = Binary)]
maybe_title: Option<Text<String>>,
/// Optional: Visibility of the Gaussian splat
#[multipart(limit = "2 KiB")]
#[schema(value_type = Option<String>, format = Binary)]
maybe_visibility: Option<Text<Visibility>>,
}
#[derive(Serialize, ToSchema)]
pub struct UploadSpzMediaFileSuccessResponse {
pub success: bool,
pub media_file_token: MediaFileToken,
}
/// This endpoint is for uploading SPZ (Gaussian Splat) files.
///
/// SPZ files are compressed Gaussian splat files used for 3D scene representation.
/// The file must have a `.spz` extension and be gzip-compressed.
#[utoipa::path(
post,
tag = "Media Files (Upload)",
path = "/v1/media_files/upload/spz",
responses(
(status = 200, description = "Success", body = UploadSpzMediaFileSuccessResponse),
(status = 400, description = "Bad input", body = MediaFileUploadError),
(status = 401, description = "Not authorized", body = MediaFileUploadError),
(status = 429, description = "Too many requests", body = MediaFileUploadError),
(status = 500, description = "Server error", body = MediaFileUploadError),
),
params(
(
"request" = UploadSpzMediaFileForm,
description = "IF VIEWING DOCS, PLEASE SEE BOTTOM OF PAGE `UploadSpzMediaFileForm` (Under 'Schema') FOR DETAILS ON FIELDS AND NULLABILITY."
),
)
)]
pub async fn upload_spz_media_file_handler(
http_request: HttpRequest,
server_state: web::Data<Arc<ServerState>>,
MultipartForm(mut form): MultipartForm<UploadSpzMediaFileForm>,
) -> Result<Json<UploadSpzMediaFileSuccessResponse>, MediaFileUploadError> {
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await
.map_err(|err| {
error!("MySql pool error: {:?}", err);
MediaFileUploadError::ServerError
})?;
// ==================== READ SESSION ==================== //
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
error!("Session checker error: {:?}", e);
MediaFileUploadError::ServerError
})?;
let maybe_user_token = maybe_user_session
.as_ref()
.map(|session| session.get_strongly_typed_user_token());
let maybe_avt_token = server_state
.avt_cookie_manager
.get_avt_token_from_request(&http_request);
// ==================== BANNED USERS ==================== //
if let Some(ref user) = maybe_user_session {
if user.is_banned {
return Err(MediaFileUploadError::NotAuthorizedVerbose("user is banned".to_string()));
}
}
// ==================== RATE LIMIT ==================== //
let rate_limiter = match maybe_user_session {
None => &server_state.redis_rate_limiters.file_upload_logged_out,
Some(ref _session) => &server_state.redis_rate_limiters.file_upload_logged_in,
};
if let Err(_err) = rate_limiter.rate_limit_request(&http_request).await {
return Err(MediaFileUploadError::RateLimited);
}
// ==================== HANDLE IDEMPOTENCY ==================== //
let uuid_idempotency_token = form.uuid_idempotency_token.as_ref();
if let Err(reason) = validate_idempotency_token_format(uuid_idempotency_token) {
return Err(MediaFileUploadError::BadInput(reason));
}
insert_idempotency_token(uuid_idempotency_token, &mut *mysql_connection)
.await
.map_err(|err| {
error!("Error inserting idempotency token: {:?}", err);
MediaFileUploadError::BadInput("invalid idempotency token".to_string())
})?;
// ==================== UPLOAD METADATA ==================== //
let maybe_title = form.maybe_title
.map(|title| title.trim().to_string())
.filter(|title| !title.is_empty());
let creator_set_visibility = form.maybe_visibility
.map(|visibility| visibility.0)
.or_else(|| {
maybe_user_session
.as_ref()
.map(|user_session| user_session.preferred_tts_result_visibility)
})
.unwrap_or(Visibility::default());
// ==================== USER DATA ==================== //
let ip_address = get_request_ip(&http_request);
// ==================== FILE VALIDATION ==================== //
let maybe_filename = form.file.file_name.as_deref()
.as_deref()
.map(|filename| PathBuf::from(filename));
let filename_lowercase = maybe_filename
.as_ref()
.and_then(|f| f.to_str())
.map(|s| s.to_ascii_lowercase())
.unwrap_or_default();
// Validate file extension
if !filename_lowercase.ends_with(".spz") {
return Err(MediaFileUploadError::BadInput(
"File must have .spz extension".to_string()
));
}
let mut file_bytes = Vec::new();
form.file.file.read_to_end(&mut file_bytes)
.map_err(|e| {
error!("Problem reading file: {:?}", e);
MediaFileUploadError::ServerError
})?;
// Validate mimetype (SPZ files are gzip-compressed)
let detected_mimetype = get_mimetype_for_bytes(file_bytes.as_ref());
if detected_mimetype != Some(GZIP_MIME_TYPE) {
return Err(MediaFileUploadError::BadInput(
"SPZ file must be gzip-compressed".to_string()
));
}
let file_size_bytes = file_bytes.len();
let hash = sha256_hash_bytes(&file_bytes)
.map_err(|io_error| {
error!("Problem hashing bytes: {:?}", io_error);
MediaFileUploadError::ServerError
})?;
// ==================== UPLOAD AND SAVE ==================== //
// Check for WorldLabs ceramic SPZ files
let is_world_labs_spz = filename_lowercase.contains("ceramic") && filename_lowercase.ends_with(".spz");
let extension = if is_world_labs_spz {
CERAMIC_SPZ_EXTENSION
} else {
SPZ_EXTENSION
};
const PREFIX: Option<&str> = Some("artcraft_");
let public_upload_path = MediaFileBucketPath::generate_new(PREFIX, Some(extension));
info!("Uploading SPZ media to bucket path: {}", public_upload_path.get_full_object_path_str());
server_state.public_bucket_client.upload_file_with_content_type(
public_upload_path.get_full_object_path_str(),
file_bytes.as_ref(),
GZIP_MIME_TYPE)
.await
.map_err(|e| {
warn!("Upload SPZ bytes to bucket error: {:?}", e);
MediaFileUploadError::ServerError
})?;
let media_token = MediaFileInsertBuilder::new()
.media_file_class(MediaFileClass::Dimensional)
.media_file_type(MediaFileType::Spz)
.maybe_creator_user(maybe_user_token.as_ref())
.maybe_creator_anonymous_visitor(maybe_avt_token.as_ref())
.creator_ip_address(&ip_address)
.creator_set_visibility(creator_set_visibility)
.media_file_origin_category(MediaFileOriginCategory::Upload)
.mime_type(GZIP_MIME_TYPE)
.file_size_bytes(file_size_bytes as u64)
.checksum_sha2(&hash)
.maybe_title(maybe_title.as_deref())
.is_intermediate_system_file(false)
.public_bucket_directory_hash(&public_upload_path)
.maybe_origin_filename(form.file.file_name.as_deref())
.insert_pool(&server_state.mysql_pool)
.await
.map_err(|err| {
warn!("New SPZ file creation DB error: {:?}", err);
MediaFileUploadError::ServerError
})?;
info!("new SPZ media file token: {:?}", &media_token);
Ok(Json(UploadSpzMediaFileSuccessResponse {
success: true,
media_file_token: media_token,
}))
}
@@ -1,5 +1,4 @@
use crate::http_server::endpoints::analytics::log_app_active_user_handler::log_app_active_user_handler;
use crate::http_server::endpoints::analytics::log_app_active_user_json_handler::log_app_active_user_json_handler;
use actix_http::body::MessageBody;
use actix_service::ServiceFactory;
use actix_web::dev::{ServiceRequest, ServiceResponse};
@@ -22,9 +21,5 @@ where
.route(web::post().to(log_app_active_user_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
.service(web::resource("/active_user_v2")
.route(web::post().to(log_app_active_user_json_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
)
}
@@ -29,7 +29,6 @@ use crate::http_server::endpoints::media_files::upload::upload_new_scene_media_f
use crate::http_server::endpoints::media_files::upload::upload_pmx::upload_pmx_media_file_handler::upload_pmx_media_file_handler;
use crate::http_server::endpoints::media_files::upload::upload_saved_scene_media_file_handler::upload_saved_scene_media_file_handler;
use crate::http_server::endpoints::media_files::upload::upload_scene_snapshot_media_file_handler::upload_scene_snapshot_media_file_handler;
use crate::http_server::endpoints::media_files::upload::upload_spz_media_file_handler::upload_spz_media_file_handler;
use crate::http_server::endpoints::media_files::upload::upload_studio_shot::upload_studio_shot_media_file_handler::upload_studio_shot_media_file_handler;
use crate::http_server::endpoints::media_files::upload::upload_video_new::upload_new_video_media_file_handler::upload_new_video_media_file_handler;
use crate::http_server::endpoints::media_files::upload::upload_video_old::upload_video_media_file_handler::upload_video_media_file_handler;
@@ -150,10 +149,6 @@ pub fn add_media_file_routes<T, B> (app: App<T>) -> App<T>
.route(web::post().to(upload_new_scene_media_file_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
.service(web::resource("/upload/spz")
.route(web::post().to(upload_spz_media_file_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
.service(web::resource("/upload/scene_snapshot")
.route(web::post().to(upload_scene_snapshot_media_file_handler))
.route(web::head().to(|| HttpResponse::Ok()))
-4
View File
@@ -45,7 +45,3 @@ vite.config.*.timestamp*
vitest.config.*.timestamp*
.cursor/rules/nx-rules.mdc
.github/instructions/nx.instructions.md
# Environment files
.env.local
.env*.local
@@ -1,4 +0,0 @@
# Google OAuth Configuration
# Get the Client ID from the Google Cloud Console:
# https://console.cloud.google.com/apis/credentials
VITE_GOOGLE_CLIENT_ID=YOUR_CLIENT_ID.apps.googleusercontent.com
+1 -32
View File
@@ -1,32 +1,13 @@
/checkout_cancel /index.html 200
/checkout_cancel/ /index.html 200
/checkout_cancel/* /index.html 200
/checkout_success /index.html 200
/checkout_success/ /index.html 200
/checkout_success/* /index.html 200
/checkout* /index.html 200
/checkout/* /index.html 200
/download /index.html 200
/download/ /index.html 200
/tutorials /index.html 200
/tutorials/ /index.html 200
/login /index.html 200
/login/ /index.html 200
/login/* /index.html 200
/media /index.html 200
/media/ /index.html 200
/media/* /index.html 200
/pricing /index.html 200
/pricing/ /index.html 200
/pricing/* /index.html 200
/faq /index.html 200
/faq/ /index.html 200
/faq/* /index.html 200
@@ -35,19 +16,7 @@
/news/ /index.html 200
/news/* /index.html 200
/press-kit /index.html 200
/press-kit/ /index.html 200
/press-kit/* /index.html 200
# Redirects for growth measurement
/reddit /?utm_source=reddit 301
/news.json /index.html 200
/signup /index.html 200
/signup/ /index.html 200
/signup/* /index.html 200
/welcome /index.html 200
/welcome/ /index.html 200
/welcome/* /index.html 200
/news.json /index.html 200
+9 -19
View File
@@ -1,30 +1,19 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Google tag (gtag.js) -->
<script
async
src="https://www.googletagmanager.com/gtag/js?id=G-VKW89949V0"
></script>
<script async src="https://www.googletagmanager.com/gtag/js?id=G-VKW89949V0"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag("js", new Date());
gtag("config", "G-VKW89949V0");
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-VKW89949V0');
</script>
<meta charset="utf-8" />
<title>ArtCraft. AI Video and Images. Fast and Open Desktop App.</title>
<meta
name="description"
content="ArtCraft is an Open Desktop app for generating AI Video and Images. You own ArtCraft!"
/>
<meta
name="keywords"
content="ArtCraft, AI, AI Video, AI Images, Open Source"
/>
<meta name="description" content="ArtCraft is an Open Desktop app for generating AI Video and Images. You own ArtCraft!" />
<meta name="keywords" content="ArtCraft, AI, AI Video, AI Images, Open Source" />
<base href="/" />
@@ -34,9 +23,10 @@
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Outfit:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"
href="https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"
rel="stylesheet"
/>
</head>
<body>
<div id="root"></div>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 806 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

+2 -27
View File
@@ -1,21 +1,13 @@
import { Route, Routes, Navigate } from "react-router-dom";
import { Route, Routes } from "react-router-dom";
import Download from "../pages/download";
import Media from "../pages/media";
import PressKit from "../pages/press-kit";
import Navbar from "../components/navbar";
import Landing2 from "../pages/landing2";
import TutorialsPage from "../pages/tutorials";
import TutorialsArticle from "../pages/tutorials/article";
import FaqIndex from "../pages/faq/index";
import FaqArticle from "../pages/faq/article";
import NewsIndex from "../pages/news/news-index";
import NewsPost from "../pages/news/news-post";
import Pricing from "../pages/pricing";
import Login from "../pages/login";
import Signup from "../pages/signup";
import ForgotPassword from "../pages/forgot-password";
import Welcome from "../pages/welcome";
import { CheckoutSuccess, CheckoutCancel } from "../pages/checkout";
import { NewsIndex, NewsPost } from "@storyteller/markdown-content";
export function App() {
return (
@@ -27,29 +19,12 @@ export function App() {
<Route path="/download" element={<Download />} />
<Route path="/media" element={<Media />} />
<Route path="/media/:id" element={<Media />} />
<Route path="/press-kit" element={<PressKit />} />
<Route path="/tutorials" element={<TutorialsPage />} />
<Route path="/tutorials/:slug" element={<TutorialsArticle />} />
<Route path="/faq" element={<FaqIndex />} />
<Route path="/faq/:slug" element={<FaqArticle />} />
<Route path="/news" element={<NewsIndex basePath="/news" />} />
<Route path="/news/:slug" element={<NewsPost basePath="/news" />} />
<Route path="/pricing" element={<Pricing />} />
<Route path="/login" element={<Login />} />
<Route path="/signup" element={<Signup />} />
<Route path="/forgot-password" element={<ForgotPassword />} />
<Route path="/welcome" element={<Welcome />} />
<Route path="/checkout/success" element={<CheckoutSuccess />} />
<Route path="/checkout/cancel" element={<CheckoutCancel />} />
{/* Redirects for underscore-based URLs (legacy Stripe config) */}
<Route
path="/checkout_success"
element={<Navigate to="/checkout/success" replace />}
/>
<Route
path="/checkout_cancel"
element={<Navigate to="/checkout/cancel" replace />}
/>
</Routes>
</div>
);
@@ -1,105 +0,0 @@
import { GoogleLogin, CredentialResponse } from "@react-oauth/google";
import { UsersApi } from "@storyteller/api";
import { faSpinner } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useState } from "react";
interface GoogleLoginButtonProps {
onSuccess: (isNewUser: boolean) => void;
onError: (error: string) => void;
mode?: "login" | "signup";
className?: string;
}
export function GoogleLoginButton({
onSuccess,
onError,
mode = "login",
className = "",
}: GoogleLoginButtonProps) {
const [isLoading, setIsLoading] = useState(false);
const handleGoogleSuccess = async (
credentialResponse: CredentialResponse,
) => {
if (!credentialResponse.credential) {
onError("No credential received from Google");
return;
}
setIsLoading(true);
try {
const api = new UsersApi();
const result = await api.GoogleSSO({
credential: credentialResponse.credential,
});
if (result.success) {
window.dispatchEvent(new Event("auth-change"));
onSuccess(result.data?.usernameNotYetCustomized ?? false);
} else {
onError(result.errorMessage || "Authentication failed");
}
} catch (err) {
onError("Network error");
} finally {
setIsLoading(false);
}
};
return (
<div className={`relative w-full h-12 ${className}`}>
{/* Custom visual button - shown behind */}
<div className="absolute inset-0 w-full h-full flex items-center justify-center gap-3 bg-white text-black rounded-lg font-medium border border-gray-200 hover:bg-gray-50 transition-colors">
{isLoading ? (
<FontAwesomeIcon
icon={faSpinner}
spin
className="text-lg text-gray-600"
/>
) : (
<>
<GoogleIcon />
<span>Continue with Google</span>
</>
)}
</div>
{/* Google's actual button - invisible overlay that receives clicks */}
<div
className="absolute inset-0 w-full h-full overflow-hidden cursor-pointer flex items-center justify-center"
style={{ opacity: 0.01, zIndex: 10, transform: "scaleY(1.2)" }}
>
<GoogleLogin
onSuccess={handleGoogleSuccess}
onError={() => onError("Google login failed")}
size="large"
width={400}
text="continue_with"
/>
</div>
</div>
);
}
function GoogleIcon() {
return (
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<path
d="M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844c-.209 1.125-.843 2.078-1.796 2.717v2.258h2.908c1.702-1.567 2.684-3.874 2.684-6.615z"
fill="#4285F4"
/>
<path
d="M9 18c2.43 0 4.467-.806 5.956-2.18l-2.908-2.259c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332A8.997 8.997 0 009 18z"
fill="#34A853"
/>
<path
d="M3.964 10.71A5.41 5.41 0 013.682 9c0-.593.102-1.17.282-1.71V4.958H.957A8.996 8.996 0 000 9c0 1.452.348 2.827.957 4.042l3.007-2.332z"
fill="#FBBC05"
/>
<path
d="M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0A8.997 8.997 0 00.957 4.958L3.964 7.29C4.672 5.163 6.656 3.58 9 3.58z"
fill="#EA4335"
/>
</svg>
);
}
@@ -1,2 +0,0 @@
export * from "./signup-form";
export * from "./GoogleLoginButton";
@@ -1,160 +0,0 @@
import { faEye, faEyeSlash, faSpinner } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button } from "@storyteller/ui-button";
import { useState } from "react";
import { UsersApi } from "@storyteller/api";
import { GoogleLoginButton } from "./GoogleLoginButton";
interface SignupFormProps {
onSuccess: (isNewUser?: boolean) => void;
signupSource: string;
className?: string;
showGoogleButton?: boolean;
autoFocus?: boolean;
}
export const SignupForm = ({
onSuccess,
signupSource,
className = "",
showGoogleButton = true,
autoFocus = false,
}: SignupFormProps) => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSignup = async () => {
setError(null);
if (!email || !password) {
setError("All fields are required");
return;
}
if (password.length < 8) {
setError("Password must be at least 8 characters");
return;
}
setIsLoading(true);
const api = new UsersApi();
// Generate a username from the email
const emailPrefix = email.split("@")[0].replace(/[^a-zA-Z0-9]/g, "");
// Ensure we don't exceed max length (7 chars from email + 3 digits = 10 total)
const truncatedPrefix = emailPrefix.substring(0, 7);
const randomSuffix = Math.floor(Math.random() * 900) + 100; // 100-999
const generatedUsername = `${truncatedPrefix}${randomSuffix}`;
const response = await api.Signup({
username: generatedUsername,
email,
password,
passwordConfirmation: password,
signupSource,
});
setIsLoading(false);
if (response.success) {
window.dispatchEvent(new Event("auth-change"));
onSuccess();
} else {
setError(response.errorMessage || "Failed to create account");
}
};
//const handleGoogleSuccess = (isNewUser: boolean) => {
// onSuccess(isNewUser);
//};
//const handleGoogleError = (errorMessage: string) => {
// setError(errorMessage);
//};
return (
<div className={`space-y-4 ${className}`}>
{/* {showGoogleButton && (
<>
<GoogleLoginButton
mode="signup"
onSuccess={handleGoogleSuccess}
onError={handleGoogleError}
/>
<div className="relative flex items-center justify-center py-2">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-white/10"></div>
</div>
<span className="relative bg-[#1C1C20] px-4 text-xs text-white/40 uppercase tracking-widest">
or
</span>
</div>
</>
)}
*/}
{error && (
<div className="bg-red-500/10 border border-red-500/20 text-red-400 px-4 py-3 rounded-xl text-sm text-center animate-in fade-in slide-in-from-top-2">
{error}
</div>
)}
<div className="space-y-2">
<label className="text-xs font-bold text-white/60 uppercase tracking-wide ml-1">
Email
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
autoFocus={autoFocus}
className="w-full bg-black/20 border border-white/10 focus:border-primary/50 rounded-xl px-4 py-3 text-white placeholder-white/20 outline-none transition-colors"
onKeyDown={(e) => e.key === "Enter" && handleSignup()}
/>
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-white/60 uppercase tracking-wide ml-1">
Password
</label>
<div className="relative">
<input
type={showPassword ? "text" : "password"}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Min. 8 characters"
className="w-full bg-black/20 border border-white/10 focus:border-primary/50 rounded-xl px-4 py-3 text-white placeholder-white/20 outline-none transition-colors pr-12"
onKeyDown={(e) => e.key === "Enter" && handleSignup()}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-4 top-1/2 -translate-y-1/2 text-white/30 hover:text-white/60 transition-colors"
tabIndex={-1}
>
<FontAwesomeIcon icon={showPassword ? faEyeSlash : faEye} />
</button>
</div>
</div>
<Button
className="w-full bg-primary hover:bg-primary-600 text-white border-none justify-center font-bold h-12 mt-2"
onClick={handleSignup}
disabled={isLoading}
>
{isLoading ? (
<FontAwesomeIcon icon={faSpinner} spin />
) : (
"Create Account"
)}
</Button>
</div>
);
};
@@ -17,7 +17,7 @@ export const DiscordButton = ({
return (
<Button
className={`relative z-10 ${sizeClasses} font-semibold transition-all duration-300 shadow-lg bg-white text-black hover:bg-gray-200 ${className}`}
className={`relative z-10 ${sizeClasses} font-semibold transition-all duration-300 shadow-lg hover:shadow-blue-500/25 ${className}`}
icon={faDiscord}
as="link"
href={SOCIAL_LINKS.DISCORD}
@@ -1,153 +0,0 @@
import { faGoogle } from "@fortawesome/free-brands-svg-icons";
import {
faTimes,
faEnvelope,
faArrowLeft,
} from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useEffect, useState } from "react";
import { createPortal } from "react-dom";
import { Button } from "@storyteller/ui-button";
import { useNavigate } from "react-router-dom";
import { SignupForm } from "./auth";
interface DownloadModalProps {
isOpen: boolean;
onClose: () => void;
}
export const DownloadModal = ({ isOpen, onClose }: DownloadModalProps) => {
const navigate = useNavigate();
const [isMounted, setIsMounted] = useState(false);
const [isVisible, setIsVisible] = useState(false);
const [view, setView] = useState<"menu" | "signup">("menu");
useEffect(() => {
setIsMounted(true);
}, []);
useEffect(() => {
if (isOpen) {
setIsVisible(true);
document.body.style.overflow = "hidden";
} else {
const timer = setTimeout(() => {
setIsVisible(false);
// Reset state after animation
setView("menu");
}, 300);
document.body.style.overflow = "unset";
return () => clearTimeout(timer);
}
}, [isOpen]);
if (!isMounted) return null;
if (!isVisible && !isOpen) return null;
return createPortal(
<div
className={`fixed inset-0 z-50 flex items-center justify-center px-4 transition-all duration-300 ${
isOpen
? "opacity-100 backdrop-blur-lg"
: "opacity-0 backdrop-blur-none pointer-events-none"
}`}
>
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/80 transition-opacity duration-300"
onClick={onClose}
/>
{/* Modal Content */}
<div
className={`relative w-full max-w-md bg-[#1C1C20] border border-white/10 rounded-3xl p-8 shadow-2xl transform transition-all duration-300 ${
isOpen ? "scale-100 translate-y-0" : "scale-95 translate-y-8"
}`}
>
<button
onClick={onClose}
className="absolute top-4 right-4 w-8 h-8 flex items-center justify-center rounded-full bg-white/5 hover:bg-white/10 text-white/40 hover:text-white/80 transition-colors z-20"
>
<FontAwesomeIcon icon={faTimes} />
</button>
{view === "signup" && (
<button
onClick={() => setView("menu")}
className="absolute top-4 left-4 w-8 h-8 flex items-center justify-center rounded-full bg-white/5 hover:bg-white/10 text-white/40 hover:text-white/80 transition-colors z-20"
>
<FontAwesomeIcon icon={faArrowLeft} />
</button>
)}
<div className="text-center mb-8">
<div className="w-16 h-16 bg-primary/20 rounded-2xl flex items-center justify-center mx-auto mb-6 text-primary">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-8 w-8 animate-bounce"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
</div>
<h2 className="text-2xl font-bold text-white mb-2">
Downloading ArtCraft...
</h2>
<p className="text-white/60 text-sm">
Your download has started.
<br />
While you wait, create an account to store your creations.
</p>
</div>
{view === "menu" ? (
<div className="space-y-3">
<Button
className="w-full bg-white text-black hover:bg-gray-100 border-none justify-center gap-3 font-medium h-12"
onClick={() => {}} // Dummy op
>
<FontAwesomeIcon icon={faGoogle} className="text-lg" />
Continue with Google
</Button>
<Button
className="w-full bg-white/5 hover:bg-white/10 text-white border-white/10 justify-center gap-3 font-medium h-12"
onClick={() => setView("signup")}
>
<FontAwesomeIcon icon={faEnvelope} className="text-lg" />
Sign up with Email
</Button>
</div>
) : (
<SignupForm
onSuccess={() => {
onClose();
navigate("/welcome");
}}
signupSource="artcraft"
showGoogleButton={false}
autoFocus={true}
/>
)}
<div className="mt-8 text-center pt-6 border-t border-white/5">
<button
onClick={onClose}
className="text-white/40 text-xs hover:text-white/60 transition-colors font-medium"
>
I already have an account
</button>
</div>
</div>
</div>,
document.body,
);
};
@@ -4,8 +4,6 @@ import {
faDiscord,
faYoutube,
faGithubAlt,
faLinkedinIn,
faInstagram,
} from "@fortawesome/free-brands-svg-icons";
import { SOCIAL_LINKS } from "../../config/links";
import { getFaqItems, getTutorialItems } from "@storyteller/markdown-content";
@@ -41,16 +39,7 @@ const navigation = {
href: SOCIAL_LINKS.GITHUB,
icon: (props: any) => <FontAwesomeIcon icon={faGithubAlt} {...props} />,
},
{
name: "Instagram",
href: SOCIAL_LINKS.INSTAGRAM,
icon: (props: any) => <FontAwesomeIcon icon={faInstagram} {...props} />,
},
{
name: "LinkedIn",
href: SOCIAL_LINKS.LINKEDIN,
icon: (props: any) => <FontAwesomeIcon icon={faLinkedinIn} {...props} />,
},
],
};
@@ -105,9 +94,6 @@ export default function Example() {
<Link to="/news" className="text-gray-400 hover:text-white">
News
</Link>
<Link to="/press-kit" className="text-gray-400 hover:text-white">
Press Kit
</Link>
</div>
<div className="flex flex-col items-center sm:items-start gap-2">
<div className="text-gray-200 mb-1">Tutorials</div>
@@ -135,9 +121,7 @@ export default function Example() {
{getFaqItems().map((item) => {
if (!item.isPublished) return null;
const truncated =
item.title.length > 36
? item.title.slice(0, 33) + "…"
: item.title;
item.title.length > 36 ? item.title.slice(0, 33) + "…" : item.title;
return (
<Link
key={item.slug}
@@ -151,7 +135,7 @@ export default function Example() {
</div>
</div>
<p className="text-center text-sm/6 text-gray-400">
&copy; 2026 ArtCraft. All rights reserved.
&copy; 2025 ArtCraft. All rights reserved.
</p>
</div>
</div>
@@ -1,4 +1,3 @@
export * from "./navbar";
export * from "./footer";
export * from "./download-button";
export * from "./pricing-table";
@@ -1,68 +1,20 @@
import {
Disclosure,
Menu,
MenuButton,
MenuItem,
MenuItems,
Transition,
} from "@headlessui/react";
import { Disclosure } from "@headlessui/react";
import { twMerge } from "tailwind-merge";
import { useEffect, useState, Fragment } from "react";
import { useEffect, useState } from "react";
import { Link, useLocation } from "react-router-dom";
import { Button } from "@storyteller/ui-button";
import { UsersApi, UserInfo } from "@storyteller/api";
import { DiscordButton } from "../discord-button";
const NAV_ITEMS = [
{ name: "Home", href: "/" },
{ name: "Tutorials", href: "/tutorials" },
{ name: "News", href: "/news" },
{ name: "FAQ", href: "/faq" },
{ name: "Press Kit", href: "/press-kit" },
{ name: "Download", href: "/download" },
];
export default function Navbar() {
const [scrolled, setScrolled] = useState(false);
const location = useLocation();
const [user, setUser] = useState<UserInfo | undefined>(undefined);
const [isLoading, setIsLoading] = useState(true);
// Check session on mount
// Check session on mount and when auth changes or location changes
useEffect(() => {
const checkSession = async () => {
const api = new UsersApi();
const response = await api.GetSession();
if (
response.success &&
response.data &&
response.data.loggedIn &&
response.data.user
) {
setUser(response.data.user);
} else {
setUser(undefined);
}
setIsLoading(false);
};
checkSession();
const handleAuthChange = () => {
setIsLoading(true);
checkSession();
};
window.addEventListener("auth-change", handleAuthChange);
return () => window.removeEventListener("auth-change", handleAuthChange);
}, [location.pathname]);
const handleLogout = async () => {
const api = new UsersApi();
await api.Logout();
// Reload to clear state/cache
window.location.href = "/";
};
// Underline hover handled purely via CSS
useEffect(() => {
const handleScroll = () => {
@@ -80,10 +32,10 @@ export default function Navbar() {
"z-20 fixed top-0 left-0 w-full transition-colors duration-200 bg-transparent",
scrolled
? "bg-[#1b1b1f]/70 backdrop-blur-lg lg:bg-transparent lg:backdrop-blur-none"
: "bg-transparent",
: "bg-transparent"
)}
>
<div className="mx-auto max-w-screen sm:px-6 lg:px-8 px-4 md:px-16 xl:px-4">
<div className="mx-auto max-w-screen sm:px-6 lg:px-8 px-6 md:px-16 xl:px-4">
<div className="flex h-16 justify-between">
<div className="flex">
<div className="flex shrink-0 items-center">
@@ -112,21 +64,21 @@ export default function Navbar() {
isCurrent
? "text-white"
: "text-white/60 hover:text-white",
"relative rounded-md text-[15px] font-semibold transition-all",
"relative rounded-md text-[15px] font-semibold transition-all"
)}
>
<span className="relative z-10">{item.name}</span>
<span
className={twMerge(
"pointer-events-none absolute left-0 right-0 -bottom-1 h-[2px] overflow-hidden",
isCurrent ? "" : "",
isCurrent ? "" : ""
)}
aria-hidden="true"
>
<span
className={twMerge(
"link-underline block h-full w-full bg-primary/90",
isCurrent ? "visible-line" : "",
isCurrent ? "visible-line" : ""
)}
/>
</span>
@@ -136,102 +88,21 @@ export default function Navbar() {
</div>
</div>
<div className="flex items-center">
{isLoading ? (
// Loading placeholder
<div className="hidden md:ml-4 md:flex items-center gap-6 opacity-0"></div>
) : user ? (
// Logged In State
<div className="hidden md:ml-4 md:flex items-center gap-6">
<Link
to="/pricing"
className="text-[15px] font-semibold text-white/60 hover:text-white transition-colors"
>
Pricing
</Link>
<Menu as="div" className="relative ml-3">
<div>
<MenuButton className="flex rounded-full bg-gray-800 text-sm focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800">
<span className="sr-only">Open user menu</span>
<img
className="h-8 w-8 rounded-full border border-white/10"
src={`https://www.gravatar.com/avatar/${user.email_gravatar_hash}?d=mp`}
alt=""
/>
</MenuButton>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<MenuItems
modal={false}
className="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-[#1C1C20] py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none border border-white/10"
>
<div className="px-4 py-3 border-b border-white/10">
<p className="text-sm text-white font-semibold truncate">
{user.display_name || user.username}
</p>
</div>
<MenuItem>
{({ active }) => (
<button
onClick={handleLogout}
className={twMerge(
active ? "bg-white/5" : "",
"block w-full text-left px-4 py-2 text-sm text-white/70",
)}
>
Sign out
</button>
)}
</MenuItem>
</MenuItems>
</Transition>
</Menu>
</div>
) : (
// Logged Out State
<div className="hidden md:ml-4 md:flex md:shrink-0 md:items-center gap-6">
<Link
to="/pricing"
className="text-[15px] font-semibold text-white/60 hover:text-white transition-colors"
>
Pricing
</Link>
<Button
as="link"
href="/signup"
className="bg-white text-black hover:bg-white/90 text-sm font-semibold px-4 py-2 h-auto rounded-lg shadow-md"
>
Sign up
</Button>
</div>
)}
{/* Mobile Menu Button - simplified for now */}
<div className="-ml-2 flex items-center md:hidden gap-3">
{!user && (
<Button
as="link"
href="/signup"
className="bg-white text-black hover:bg-white/90 text-xs font-semibold px-3 py-1.5 h-auto rounded-lg"
>
Sign up
</Button>
)}
{user && (
<img
className="h-8 w-8 rounded-full border border-white/10"
src={`https://www.gravatar.com/avatar/${user.email_gravatar_hash}?d=mp`}
alt=""
/>
)}
<div className="hidden md:ml-4 md:flex md:shrink-0 md:items-center">
{/* <Button as="link" href="/download">
Download
</Button> */}
<DiscordButton
small
className="bg-white text-black hover:bg-white/90"
/>
</div>
<div className="-ml-2 flex items-center md:hidden">
{/* Mobile menu button */}
<DiscordButton
className="text-sm bg-white text-black hover:bg-white/90"
small
/>
</div>
</div>
</div>
@@ -16,11 +16,7 @@ export const OwnershipComparison = () => {
<span>From Websites</span>
</h2>
<p className="md:text-xl text-lg text-white/80 max-w-2xl mx-auto">
We're artists, not landlords. We're going to give you ArtCraft to have and to hold <em>forever</em>. It's yours.
</p>
<br />
<p className="md:text-xl text-lg text-white/80 max-w-2xl mx-auto">
So you can dump that aggregator <s>subscription</s> rent payment.
Take back control of your tools.
</p>
</div>
@@ -62,7 +58,7 @@ export const OwnershipComparison = () => {
</h3>
<p className="text-white/80 mb-8 leading-relaxed opacity-100">
You're paying for permission, not a product. If they shut down, you lose everything. (Ask them about downloading your LoRAs! 🤫)
You're paying for permission, not a product. If they shut down, you lose everything.
</p>
<div className="flex flex-wrap justify-center gap-3 text-xs sm:text-sm font-bold tracking-wide uppercase">
@@ -100,7 +96,7 @@ export const OwnershipComparison = () => {
<p className="text-white/80 mb-8 leading-relaxed opacity-100">
Download ArtCraft. You own the code, the keys, and the output. You don't even have to pay us to use it.
<br />
<div className="text-xs text-white/50 pt-3">(Lots of 3rd party logins and API keys supported. Also please pay us. We turn that into coffee and development and movies.)</div>
<div className="text-xs text-white/50 pt-3">(Lots of 3rd party logins and API keys supported.)</div>
</p>
<div className="flex flex-wrap justify-center gap-3 text-xs sm:text-sm font-bold tracking-wide uppercase">
@@ -1 +0,0 @@
export { default as PricingTable } from "./pricing-table";

Some files were not shown because too many files have changed in this diff Show More