mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-03 21:10:58 -05:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eef1ed6cfb | ||
|
|
4bee9d9715 |
+3
-3
@@ -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"
|
||||
}
|
||||
-12
@@ -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"
|
||||
}
|
||||
+12
@@ -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
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
[](https://discord.gg/artcraft)
|
||||
[](https://www.youtube.com/@OfficialArtCraftStudios)
|
||||
[](https://www.linkedin.com/company/artcraft-ai)
|
||||
[](https://x.com/get_artcraft)
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Secrets
|
||||
db_secrets.toml
|
||||
|
||||
# Python Virtual Env
|
||||
python/
|
||||
|
||||
+88
@@ -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
@@ -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")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
-2
@@ -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?)
|
||||
|
||||
-18
@@ -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,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;
|
||||
-12
@@ -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;
|
||||
-4
@@ -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,
|
||||
|
||||
-6
@@ -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))
|
||||
|
||||
-10
@@ -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)
|
||||
}
|
||||
|
||||
-12
@@ -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)
|
||||
}
|
||||
|
||||
-12
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+1
-11
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
-9
@@ -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)
|
||||
}
|
||||
|
||||
-10
@@ -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)
|
||||
}
|
||||
|
||||
+2
-1
@@ -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
|
||||
|
||||
+8
-21
@@ -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
|
||||
}
|
||||
|
||||
+5
-34
@@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-16
@@ -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,
|
||||
|
||||
-135
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-46
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
+3
-24
@@ -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,
|
||||
|
||||
@@ -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,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,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
@@ -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(|| {
|
||||
|
||||
+1
-1
@@ -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(|| {
|
||||
|
||||
+1
-1
@@ -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;
|
||||
+33
@@ -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;
|
||||
+18
@@ -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)
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
-81
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+3
-4
@@ -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>,
|
||||
|
||||
@@ -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,
|
||||
|
||||
-16
@@ -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?;
|
||||
|
||||
-149
@@ -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;
|
||||
+8
-41
@@ -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,
|
||||
|
||||
+7
-40
@@ -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,
|
||||
|
||||
+7
-40
@@ -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,
|
||||
|
||||
+8
-45
@@ -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,
|
||||
|
||||
+8
-45
@@ -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,
|
||||
|
||||
+11
-50
@@ -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;
|
||||
|
||||
-273
@@ -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,
|
||||
}))
|
||||
}
|
||||
-5
@@ -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()))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
-5
@@ -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()))
|
||||
|
||||
@@ -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 +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
|
||||
@@ -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 |
BIN
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 |
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 2.3 MiB |
@@ -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">
|
||||
© 2026 ArtCraft. All rights reserved.
|
||||
© 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>
|
||||
|
||||
+3
-7
@@ -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
Reference in New Issue
Block a user