Compare commits

..
Author SHA1 Message Date
Brandon Thomas eef1ed6cfb more columns 2026-01-21 00:50:54 -05:00
Brandon Thomas 4bee9d9715 add new analytics columns
need to know where to best spend our engineering effort
2026-01-21 00:50:53 -05:00
614 changed files with 2966 additions and 59425 deletions
+2 -2
View File
@@ -233,8 +233,8 @@ jobs:
with:
projectPath: ./crates/desktop/artcraft
tagName: artcraft-v__VERSION__ # the action automatically replaces __VERSION__ with the app version.
releaseName: "ArtCraft v__VERSION__"
releaseBody: "See the assets below to download this version and install."
releaseName: "Artcraft v__VERSION__"
releaseBody: "See the assets to download this version and install."
releaseDraft: true
prerelease: false
args: ${{ matrix.args }}
@@ -99,9 +99,9 @@ jobs:
SQLX_OFFLINE: true
with:
projectPath: ./crates/desktop/artcraft
tagName: artcraft-v__VERSION__ # the action automatically replaces __VERSION__ with the app version.
releaseName: "ArtCraft v__VERSION__"
releaseBody: "Production build of ArtCraft. See the assets below to download and install."
tagName: artcraft-windows-v__VERSION__ # the action automatically replaces __VERSION__ with the app version.
releaseName: "Artcraft Windows v__VERSION__"
releaseBody: "Windows build - See the assets to download this version and install."
releaseDraft: true
prerelease: false
args: ${{ matrix.args }}
-3
View File
@@ -32,9 +32,6 @@ script/production_database_secret_backup.sh
script/production_database_secret_connect.sh
script/socat_dev_database_connect.sh
# Python virtual envs (named by convention)
venv/
# Scratch data
scratch/
test_data/temp/
@@ -1,12 +0,0 @@
{
"db_name": "MySQL",
"query": "\nUPDATE users\nSET\n password_hash = ?,\n ip_address_last_update = ?,\n is_without_password = false,\n password_version = password_version + 1,\n version = version + 1\nWHERE\n token = ?\nLIMIT 1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "03761e50f94f2805f1c5c64f598a23af8d708e514a01c6090570b2e0bfabfe2c"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nUPDATE users\nSET\n email_confirmed = true,\n password_hash = ?,\n ip_address_last_update = ?,\n password_version = password_version + 1,\n version = version + 1\nWHERE\n token = ?\nLIMIT 1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "05de463f1c408793d64fb011b1732e3c1b3cf485076b3e1a4a50d16683b99e46"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n users.token as user_token,\n users.username,\n users.display_name,\n\n users.email_address,\n users.email_gravatar_hash,\n \n users.email_confirmed,\n users.email_confirmed_by_google,\n users.email_is_synthetic,\n users.is_without_password,\n users.username_is_not_customized,\n\n users.maybe_stripe_customer_id,\n users.maybe_loyalty_program_key,\n\n users.disable_gravatar,\n users.auto_play_audio_preference,\n users.auto_play_video_preference,\n users.preferred_tts_result_visibility as `preferred_tts_result_visibility: enums::common::visibility::Visibility`,\n users.preferred_w2l_result_visibility as `preferred_w2l_result_visibility: enums::common::visibility::Visibility`,\n\n users.user_role_slug,\n users.is_banned,\n\n users.can_access_studio,\n users.maybe_feature_flags,\n\n user_roles.can_use_tts,\n user_roles.can_use_w2l,\n user_roles.can_delete_own_tts_results,\n user_roles.can_delete_own_w2l_results,\n user_roles.can_delete_own_account,\n\n user_roles.can_upload_tts_models,\n user_roles.can_upload_w2l_templates,\n user_roles.can_delete_own_tts_models,\n user_roles.can_delete_own_w2l_templates,\n\n user_roles.can_approve_w2l_templates,\n user_roles.can_edit_other_users_profiles,\n user_roles.can_edit_other_users_tts_models,\n user_roles.can_edit_other_users_w2l_templates,\n user_roles.can_delete_other_users_tts_models,\n user_roles.can_delete_other_users_tts_results,\n user_roles.can_delete_other_users_w2l_templates,\n user_roles.can_delete_other_users_w2l_results,\n user_roles.can_ban_users,\n user_roles.can_delete_users\n\nFROM users\nLEFT OUTER JOIN user_sessions\n ON users.token = user_sessions.user_token\nLEFT OUTER JOIN user_roles\n ON users.user_role_slug = user_roles.slug\nWHERE user_sessions.token = ?\n AND user_sessions.deleted_at IS NULL\n AND users.user_deleted_at IS NULL\n AND users.mod_deleted_at IS NULL\n ",
"query": "\nSELECT\n users.token as user_token,\n users.username,\n users.display_name,\n\n users.email_address,\n users.email_confirmed,\n users.email_gravatar_hash,\n\n users.maybe_stripe_customer_id,\n users.maybe_loyalty_program_key,\n\n users.disable_gravatar,\n users.auto_play_audio_preference,\n users.auto_play_video_preference,\n users.preferred_tts_result_visibility as `preferred_tts_result_visibility: enums::common::visibility::Visibility`,\n users.preferred_w2l_result_visibility as `preferred_w2l_result_visibility: enums::common::visibility::Visibility`,\n\n users.user_role_slug,\n users.is_banned,\n\n users.can_access_studio,\n users.maybe_feature_flags,\n\n user_roles.can_use_tts,\n user_roles.can_use_w2l,\n user_roles.can_delete_own_tts_results,\n user_roles.can_delete_own_w2l_results,\n user_roles.can_delete_own_account,\n\n user_roles.can_upload_tts_models,\n user_roles.can_upload_w2l_templates,\n user_roles.can_delete_own_tts_models,\n user_roles.can_delete_own_w2l_templates,\n\n user_roles.can_approve_w2l_templates,\n user_roles.can_edit_other_users_profiles,\n user_roles.can_edit_other_users_tts_models,\n user_roles.can_edit_other_users_w2l_templates,\n user_roles.can_delete_other_users_tts_models,\n user_roles.can_delete_other_users_tts_results,\n user_roles.can_delete_other_users_w2l_templates,\n user_roles.can_delete_other_users_w2l_results,\n user_roles.can_ban_users,\n user_roles.can_delete_users\n\nFROM users\nLEFT OUTER JOIN user_sessions\n ON users.token = user_sessions.user_token\nLEFT OUTER JOIN user_roles\n ON users.user_role_slug = user_roles.slug\nWHERE user_sessions.token = ?\n AND user_sessions.deleted_at IS NULL\n AND users.user_deleted_at IS NULL\n AND users.mod_deleted_at IS NULL\n ",
"describe": {
"columns": [
{
@@ -45,16 +45,6 @@
},
{
"ordinal": 4,
"name": "email_gravatar_hash",
"type_info": {
"type": "String",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 5,
"name": "email_confirmed",
"type_info": {
"type": "Tiny",
@@ -63,48 +53,18 @@
"max_size": 1
}
},
{
"ordinal": 5,
"name": "email_gravatar_hash",
"type_info": {
"type": "String",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 6,
"name": "email_confirmed_by_google",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 7,
"name": "email_is_synthetic",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 8,
"name": "is_without_password",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 9,
"name": "username_is_not_customized",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 10,
"name": "maybe_stripe_customer_id",
"type_info": {
"type": "VarString",
@@ -114,7 +74,7 @@
}
},
{
"ordinal": 11,
"ordinal": 7,
"name": "maybe_loyalty_program_key",
"type_info": {
"type": "VarString",
@@ -124,7 +84,7 @@
}
},
{
"ordinal": 12,
"ordinal": 8,
"name": "disable_gravatar",
"type_info": {
"type": "Tiny",
@@ -134,7 +94,7 @@
}
},
{
"ordinal": 13,
"ordinal": 9,
"name": "auto_play_audio_preference",
"type_info": {
"type": "Tiny",
@@ -144,7 +104,7 @@
}
},
{
"ordinal": 14,
"ordinal": 10,
"name": "auto_play_video_preference",
"type_info": {
"type": "Tiny",
@@ -154,7 +114,7 @@
}
},
{
"ordinal": 15,
"ordinal": 11,
"name": "preferred_tts_result_visibility: enums::common::visibility::Visibility",
"type_info": {
"type": "String",
@@ -164,7 +124,7 @@
}
},
{
"ordinal": 16,
"ordinal": 12,
"name": "preferred_w2l_result_visibility: enums::common::visibility::Visibility",
"type_info": {
"type": "String",
@@ -174,7 +134,7 @@
}
},
{
"ordinal": 17,
"ordinal": 13,
"name": "user_role_slug",
"type_info": {
"type": "VarString",
@@ -184,7 +144,7 @@
}
},
{
"ordinal": 18,
"ordinal": 14,
"name": "is_banned",
"type_info": {
"type": "Tiny",
@@ -194,7 +154,7 @@
}
},
{
"ordinal": 19,
"ordinal": 15,
"name": "can_access_studio",
"type_info": {
"type": "Tiny",
@@ -204,7 +164,7 @@
}
},
{
"ordinal": 20,
"ordinal": 16,
"name": "maybe_feature_flags",
"type_info": {
"type": "VarString",
@@ -214,7 +174,7 @@
}
},
{
"ordinal": 21,
"ordinal": 17,
"name": "can_use_tts",
"type_info": {
"type": "Tiny",
@@ -224,7 +184,7 @@
}
},
{
"ordinal": 22,
"ordinal": 18,
"name": "can_use_w2l",
"type_info": {
"type": "Tiny",
@@ -234,7 +194,7 @@
}
},
{
"ordinal": 23,
"ordinal": 19,
"name": "can_delete_own_tts_results",
"type_info": {
"type": "Tiny",
@@ -244,7 +204,7 @@
}
},
{
"ordinal": 24,
"ordinal": 20,
"name": "can_delete_own_w2l_results",
"type_info": {
"type": "Tiny",
@@ -254,7 +214,7 @@
}
},
{
"ordinal": 25,
"ordinal": 21,
"name": "can_delete_own_account",
"type_info": {
"type": "Tiny",
@@ -264,7 +224,7 @@
}
},
{
"ordinal": 26,
"ordinal": 22,
"name": "can_upload_tts_models",
"type_info": {
"type": "Tiny",
@@ -274,7 +234,7 @@
}
},
{
"ordinal": 27,
"ordinal": 23,
"name": "can_upload_w2l_templates",
"type_info": {
"type": "Tiny",
@@ -284,7 +244,7 @@
}
},
{
"ordinal": 28,
"ordinal": 24,
"name": "can_delete_own_tts_models",
"type_info": {
"type": "Tiny",
@@ -294,7 +254,7 @@
}
},
{
"ordinal": 29,
"ordinal": 25,
"name": "can_delete_own_w2l_templates",
"type_info": {
"type": "Tiny",
@@ -304,7 +264,7 @@
}
},
{
"ordinal": 30,
"ordinal": 26,
"name": "can_approve_w2l_templates",
"type_info": {
"type": "Tiny",
@@ -314,7 +274,7 @@
}
},
{
"ordinal": 31,
"ordinal": 27,
"name": "can_edit_other_users_profiles",
"type_info": {
"type": "Tiny",
@@ -324,7 +284,7 @@
}
},
{
"ordinal": 32,
"ordinal": 28,
"name": "can_edit_other_users_tts_models",
"type_info": {
"type": "Tiny",
@@ -334,7 +294,7 @@
}
},
{
"ordinal": 33,
"ordinal": 29,
"name": "can_edit_other_users_w2l_templates",
"type_info": {
"type": "Tiny",
@@ -344,7 +304,7 @@
}
},
{
"ordinal": 34,
"ordinal": 30,
"name": "can_delete_other_users_tts_models",
"type_info": {
"type": "Tiny",
@@ -354,7 +314,7 @@
}
},
{
"ordinal": 35,
"ordinal": 31,
"name": "can_delete_other_users_tts_results",
"type_info": {
"type": "Tiny",
@@ -364,7 +324,7 @@
}
},
{
"ordinal": 36,
"ordinal": 32,
"name": "can_delete_other_users_w2l_templates",
"type_info": {
"type": "Tiny",
@@ -374,7 +334,7 @@
}
},
{
"ordinal": 37,
"ordinal": 33,
"name": "can_delete_other_users_w2l_results",
"type_info": {
"type": "Tiny",
@@ -384,7 +344,7 @@
}
},
{
"ordinal": 38,
"ordinal": 34,
"name": "can_ban_users",
"type_info": {
"type": "Tiny",
@@ -394,7 +354,7 @@
}
},
{
"ordinal": 39,
"ordinal": 35,
"name": "can_delete_users",
"type_info": {
"type": "Tiny",
@@ -414,10 +374,6 @@
false,
false,
false,
false,
false,
false,
false,
true,
true,
false,
@@ -450,5 +406,5 @@
true
]
},
"hash": "3ca99449c071cb0a8d90a735e1a767a94be224a02e0d16e81997d569af8d4cbb"
"hash": "1c32bdb79ed2bc1b8988bbb6ea3f2a1db4ed4552d53cb4f31ff4b08de01691f7"
}
@@ -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"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO users\nSET\n token = ?,\n username = ?,\n display_name = ?,\n\n username_is_generated = ?,\n username_is_not_customized = ?,\n\n email_address = ?,\n email_gravatar_hash = ?,\n\n email_confirmed = FALSE,\n email_confirmed_by_google = ?,\n\n profile_markdown = ?,\n profile_rendered_html = ?,\n user_role_slug = ?,\n\n password_hash = ?,\n is_without_password = ?,\n\n maybe_feature_flags = ?,\n\n ip_address_creation = ?,\n ip_address_last_login = ?,\n ip_address_last_update = ?,\n\n maybe_source = ?,\n\n maybe_signup_method = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 19
},
"nullable": []
},
"hash": "453f0386cfe34e533501ba29dc2022fc449266f9858f16886bde9a0df6b82026"
}
@@ -1,12 +0,0 @@
{
"db_name": "MySQL",
"query": "\nUPDATE users\nSET\n email_address = ?,\n email_confirmed = false,\n email_confirmed_by_google = false,\n email_is_synthetic = false,\n email_gravatar_hash = ?,\n ip_address_last_update = ?,\n version = version + 1\nWHERE\n token = ?\nLIMIT 1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
},
"hash": "4737c44e7b1c73b21a245dd9d1f96aecfc4c614ed19ac4b590a26809c98fee03"
}
@@ -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"
}
@@ -1,12 +0,0 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO users\nSET\n token = ?,\n \n is_temporary = ?,\n \n username = ?,\n display_name = ?,\n\n username_is_generated = ?,\n username_is_not_customized = ?,\n\n email_address = ?,\n email_gravatar_hash = ?,\n\n email_confirmed = FALSE,\n email_confirmed_by_google = ?,\n email_is_synthetic = ?,\n \n was_eagerly_provisioned = ?,\n\n profile_markdown = ?,\n profile_rendered_html = ?,\n user_role_slug = ?,\n\n password_hash = ?,\n is_without_password = ?,\n\n maybe_feature_flags = ?,\n\n ip_address_creation = ?,\n ip_address_last_login = ?,\n ip_address_last_update = ?,\n\n maybe_source = ?,\n\n maybe_signup_method = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 22
},
"nullable": []
},
"hash": "56746bcb993cb33f5492f401ce4fc3fd44d91fed660c2f106cb22e482dffa740"
}
@@ -1,12 +0,0 @@
{
"db_name": "MySQL",
"query": "\nUPDATE users\nSET\n email_address = ?,\n email_gravatar_hash = ?,\n email_confirmed = false,\n email_confirmed_by_google = false,\n email_is_synthetic = false,\n version = version + 1\nWHERE\n token = ?\nLIMIT 1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "6fa90a16784c7b7e3436877c2a751bf0b60054640ebe69cb5f33938238def0fe"
}
@@ -1,12 +0,0 @@
{
"db_name": "MySQL",
"query": "\nUPDATE users\nSET\n email_confirmed = true,\n email_is_synthetic = false,\n is_without_password = false,\n is_temporary = false,\n password_hash = ?,\n ip_address_last_update = ?,\n password_version = password_version + 1,\n version = version + 1\nWHERE\n token = ?\nLIMIT 1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "9cd3a581f6857e5c1db68a103f47eba9714ee4b5eefc8ba02fc49a4b3a19310e"
}
@@ -1,36 +0,0 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n email_address,\n email_is_synthetic\nFROM users\nWHERE token = ?\nLIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "email_address",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 1,
"name": "email_is_synthetic",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false
]
},
"hash": "c2dd5120b34d62153772acc94f97c3b7a22b7c53216721e54882c0019d2467e6"
}
@@ -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
+103 -926
View File
File diff suppressed because it is too large Load Diff
+2 -26
View File
@@ -36,7 +36,6 @@ members = [
"crates/desktop/artcraft",
# Shared libraries
"crates/lib/actix_artcraft",
"crates/lib/actix_cors_configs",
"crates/lib/actix_helpers",
"crates/lib/bootstrap",
@@ -81,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",
@@ -90,7 +90,6 @@ members = [
"crates/lib/url_config",
"crates/lib/url_utils",
"crates/lib/user_input_common",
"crates/lib/users",
"crates/lib/web_scrapers",
#"crates/lib/api_clients/fakeyou_client",
#"crates/lib/machine_learning/ml_models",
@@ -136,23 +135,6 @@ members = [
"crates/vendor/fal-rs/generate",
"crates/vendor/limitation",
"crates/vendor/tauri-plugin-http",
"crates/vendor/Symphonia/symphonia",
"crates/vendor/Symphonia/symphonia-bundle-flac",
"crates/vendor/Symphonia/symphonia-bundle-mp3",
"crates/vendor/Symphonia/symphonia-codec-aac",
"crates/vendor/Symphonia/symphonia-codec-alac",
"crates/vendor/Symphonia/symphonia-codec-opus",
"crates/vendor/Symphonia/symphonia-codec-pcm",
"crates/vendor/Symphonia/symphonia-codec-vorbis",
"crates/vendor/Symphonia/symphonia-codec-wavpack",
"crates/vendor/Symphonia/symphonia-core",
"crates/vendor/Symphonia/symphonia-format-isomp4",
"crates/vendor/Symphonia/symphonia-format-mkv",
"crates/vendor/Symphonia/symphonia-format-ogg",
"crates/vendor/Symphonia/symphonia-format-wav",
"crates/vendor/Symphonia/symphonia-metadata",
"crates/vendor/Symphonia/symphonia-play",
"crates/vendor/Symphonia/symphonia-utils-xiph",
# NB(bt,2025-05-25): Too many failures on Mac. Disable for now.
#"crates/vendor/candle/candle-core",
#"crates/vendor/candle/candle-datasets",
@@ -204,17 +186,13 @@ workspace-build-acceleration = { path = "crates/workspace-build-acceleration" }
# Internal libs
# Setting paths here ensures we don't need to do so in every crate's Cargo.toml
# It makes refactoring much easier.
actix_artcraft = { path = "crates/lib/actix_artcraft" }
actix_helpers = { path = "crates/lib/actix_helpers" }
artcraft_api_defs = { path = "crates/api_clients/artcraft/artcraft_api_defs" }
build_metadata = { path = "crates/lib/build_metadata" }
cloud_storage = { path = "crates/api_clients/cloud_storage" }
cloudflare_errors = { path = "crates/lib/cloudflare_errors" }
cloudflare_mitigation = { path = "crates/lib/cloudflare_mitigation" }
collections = { path = "crates/lib/data/collections" }
component_traits = { path = "crates/service/plugins/component_traits" }
cookie_store = { path = "crates/lib/cookie_store" }
cookies = { path = "crates/lib/cookies" }
crockford = { path = "crates/lib/data/crockford" }
enums = { path = "crates/schema/public/enums" }
enums_public = { path = "crates/schema/public/enums_public" }
@@ -241,7 +219,6 @@ test_data = { path = "crates/lib/test_data" }
testing = { path = "crates/lib/testing" }
tokens = { path = "crates/schema/public/tokens" }
url_utils = { path = "crates/lib/url_utils" }
users = { path = "crates/lib/users" }
web_base64 = { path = "crates/lib/data/web_base64" }
world_labs_client = { path = "crates/api_clients/world_labs_client" }
@@ -270,13 +247,12 @@ futures = { version = "0.3.31" }
hex-literal = "1.0.0"
indexmap = "2.9.0"
log = "0.4.27"
md-5 = "0.10.6" # insecure but used for gravatar
once_cell = "1.21.3"
r2d2 = "0.8.10"
rand = "0.9.1"
redis = { version = "1.0.2", features = ["r2d2", "tokio-rustls-comp", "tokio-comp", "cluster-async", "aio"] }
regex = "1.12.2"
reqwest = { version = "0.12.23", default-features = false, features = [ "charset", "http2", "system-proxy", "json", "cookies", "rustls-tls", "gzip" ] }
reqwest = { version = "0.12.23", default-features = false, features = [ "charset", "http2", "system-proxy", "json", "cookies", "rustls-tls" ] }
reqwest-websocket = { version = "0.5.0" }
serde = { version = "1.0.219", features = ["derive"] }
serde_derive = "1.0.219"
+8 -15
View File
@@ -1,17 +1,3 @@
<p align="center">
<video src="https://github.com/user-attachments/assets/b4e24c27-d87d-4fd1-8599-dc0d0b8af48d" width="100%" autoplay="true" loop controls>
</p>
<p align="center">The IDE for artists.</p>
<p align="center">
<a href="https://discord.gg/artcraft"><img alt="Discord" src="https://img.shields.io/discord/1359579021108842617?style=for-the-badge&label=discord&color=ffffff&logo=discord&logoColor=ffffff" /></a>
<a href="https://www.youtube.com/@OfficialArtCraftStudios"><img alt="YouTube" src="https://img.shields.io/youtube/channel/subscribers/UCdjY4VG0ntoGwFsKZO4sVWA?style=for-the-badge&logo=YouTube" /></a>
<a href="https://x.com/intent/follow?screen_name=get_artcraft"><img alt="X" src="https://img.shields.io/twitter/follow/get_artcraft?style=for-the-badge&label=follow&logo=x&logoColor=ffffff&color=ffffff" /></a>
<a href="https://www.linkedin.com/company/artcraft-ai"><img alt="LinkedIn" src="https://img.shields.io/badge/linkedin--0A66C2?style=for-the-badge"></a>
</p>
---
ArtCraft
========
ArtCraft is the IDE for interactive AI image and video creation.
@@ -55,7 +41,7 @@ We haven't abandoned text-to-asset generation for quick prototyping and ideation
| Provider | Features |
|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Artcraft | Nano Banana, Nano Banana Pro, GPT-Image-1 / 1.5, Seedream 4 / 4.5, Flux 1.1 / Kontext, Veo 2 / 3 / 3.1, Kling 1.6 / 2.1 / 2.5 / 2.6, Seedance, Sora 2 / Pro, Hunyuan 3d 2 / 3 |
| Artcraft | Nano Banana, Nano Banana Pro, GPT-Image-1 / 1.5, Seedream 4 / 4.5, Flux 1.1 / Kontext, Veo 2 / 3 / 3.1, Kling 1.6 / 2.1 / 2.5 / 2.6, Seedance, Sora 2 / Pro, Hunyuan 3d |
| Grok | Grok Imagine, Grok Video |
| Midjourney | Image Gen (all versions) |
| Sora | Sora 1, Sora 2, GPT-Image-1 |
@@ -80,3 +66,10 @@ those providers, for example: OpenArt, FreePik, etc.
- [license](./LICENSE.md)
- [roadmap](./ROADMAP.md)
## Social
[![discord](https://raw.githubusercontent.com/CLorant/readme-social-icons/main/large/colored/discord.svg)](https://discord.gg/artcraft)
[![youtube](https://raw.githubusercontent.com/CLorant/readme-social-icons/main/large/colored/youtube.svg)](https://www.youtube.com/@OfficialArtCraftStudios)
[![x](https://raw.githubusercontent.com/CLorant/readme-social-icons/main/large/colored/linkedin.svg)](https://www.linkedin.com/company/artcraft-ai)
[![x](https://raw.githubusercontent.com/CLorant/readme-social-icons/main/large/colored/twitter-x.svg)](https://x.com/get_artcraft)
@@ -1,7 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE users
DROP COLUMN
was_eagerly_provisioned;
@@ -1,7 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE users
ADD COLUMN
was_eagerly_provisioned BOOLEAN NOT NULL DEFAULT false;
@@ -1,7 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE users
DROP COLUMN
is_temporary;
@@ -1,7 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE users
ADD COLUMN
is_temporary BOOLEAN NOT NULL DEFAULT false;
@@ -1,7 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE users
DROP COLUMN
email_is_synthetic;
@@ -1,7 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE users
ADD COLUMN
email_is_synthetic BOOLEAN NOT NULL DEFAULT false;
@@ -12,14 +12,6 @@ CREATE TABLE users (
-- Visible "primary key"
token VARCHAR(32) NOT NULL,
-- This is TRUE for accounts that were provisioned automatically (eg. eager stripe
-- checkout flow), and who haven't completed onboarding (eg. finishing with billing).
-- Once the user finishes setup, this is set to FALSE.
-- Existing users and normal signup flow always sets this to FALSE.
-- If enough time has elapsed for an incomplete `is_temporary` user, such user records
-- might be possible to delete as we assume the user abandoned sign up long ago.
is_temporary BOOLEAN NOT NULL DEFAULT false,
-- Username is a lookup key; display_name allows the user to add custom case.
username VARCHAR(20) NOT NULL,
display_name VARCHAR(20) NOT NULL,
@@ -38,8 +30,6 @@ CREATE TABLE users (
-- Set by the user at sign up:
-- - When email + password signup, this is the email provided.
-- - When Google SSO, this is the email provided by Google.
-- - When "eager" checkout flow, this is synthetically generated and
-- updated by the user later.
email_address VARCHAR(255) NOT NULL,
-- Email address is confirmed by us.
@@ -49,11 +39,6 @@ CREATE TABLE users (
-- Email address is confirmed by Google.
email_confirmed_by_google BOOLEAN NOT NULL DEFAULT false,
-- We created a user account provisionally, and the email address is not yet real.
-- This happens during "eager" checkout flow before users create accounts - we create
-- a user account for them with a fake email address (since this is a unique key).
email_is_synthetic BOOLEAN NOT NULL DEFAULT false,
-- The role assigned to the user confers permissions.
user_role_slug VARCHAR(16) NOT NULL,
@@ -101,7 +86,6 @@ CREATE TABLE users (
password_version INT UNSIGNED NOT NULL DEFAULT 0,
-- Users signing up from SSO do not have a password at time of account creation.
-- Users signing up with eager stripe checkout flow do not have a password at time of creation
is_without_password BOOLEAN NOT NULL DEFAULT false,
-- ========== ABUSE TRACKING ==========
@@ -207,19 +191,10 @@ CREATE TABLE users (
maybe_source VARCHAR(255) DEFAULT NULL,
-- How users created their account
-- Older users do not have a value and are assumed to be "email_password"
-- Values:
-- - "email_password"
-- - "google_sign_in"
-- - "stripe_checkout" (eager stripe checkout account provisioning)
-- Initially this will be "email_password" or "google_sign_in"
-- Older users do not have a value and are assumed to be "email_sign_in"
maybe_signup_method VARCHAR(16) DEFAULT NULL,
-- Account was provisioned automatically (eg. stripe eager checkout flow)
-- This will remain true even if the user finishes account setup, email change,
-- password setup, etc. (It's a permanent label on the account for later
-- statistical evaluation.)
was_eagerly_provisioned BOOLEAN NOT NULL DEFAULT false,
-- ========== MODERATION DETAILS ==========
-- Different than deleted.
-45
View File
@@ -1,45 +0,0 @@
Dev Setup
=========
# ArtCraft
ArtCraft is a Rust / Tauri app.
To set up the ArtCraft development environment, install the following:
1. [Install Rust](https://doc.rust-lang.org/cargo/getting-started/installation.html).
2. [Install npm](https://nodejs.org/en/download) or [nvm](https://github.com/nvm-sh/nvm). (Node version `v24.13.0` works at time of writing.)
3. [Install nx](https://nx.dev/docs/getting-started/installation). (Nx version `v22.4.5` works at time of writing.)
4. [Install Tauri CLI](https://v2.tauri.app/reference/cli/). (Version `tauri-cli 2.10.0` works at time of writing.)
An easy way to get started with running the app in development is to run the two commands (in separate terminals):
**Mac and Linux Development**
```bash
# Run the frontend dev server
./script/artcraft/unix_frontend_dev.sh
# Run the Tauri Rust application
./script/artcraft/unix_rust_dev.sh
```
**Windows Development**
```powershell
# Run the frontend dev server
.\script\artcraft\windows_frontend_dev.ps1
# Run the Tauri Rust application
.\script\artcraft\windows_rust_dev.ps1
```
# ArtCraft Server
ArtCraft's server is a Rust / Actix app called `storyteller-web`.
You don't need to run this to develop the ArtCraft application, but it can be useful to spin up
a development instance for adding new server functions or as your own private local copy.
See [dev_setup_server.md](./dev_setup_server.md) for instructions.
-105
View File
@@ -1,105 +0,0 @@
# ArtCraft Server
ArtCraft's server is a Rust / Actix app called `storyteller-web`.
You don't need to run this to develop the ArtCraft application, but it can be useful to spin up
a development instance for adding new server functions or as your own private local copy.
`storyteller-web` can function entirely within the development environment, and ArtCraft can be
pointed to a local instance of the server.
**TODO**: Concisely describe setting up server components. Note: we have
previous docs in `old/` that may still be relevant, though they're perhaps
slightly out of date.
## Server Setup (Mac)
### Install and Migrate MySQL Database
```bash
# For now, the tooling doesn't support MySQL 9.6 as it was just released in January 2026.
# This will probably change in the future to no longer need to specify the 8.* series.
brew install mysql@8.4
# Try to connect
mysql -uroot
# If connection fails, start service, then try again.
brew services start mysql
mysql -uroot
# If that fails, reboot and try connecting again.
```
Create the `storyteller` database table. Connect to the database:
```bash
sudo mysql -u root
# If it needs a password, use argument -p and when prompted use "root" as the typical default password
```
Then run this:
```mysql
use mysql;
CREATE DATABASE storyteller;
CREATE USER 'storyteller'@'localhost' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON storyteller.* TO 'storyteller'@'localhost';
```
Then verify access with `./script/dev_mysql_connect.sh`
```bash
cargo install diesel_cli \
--no-default-features \
--features mysql,sqlite
```
If this fails,
```bash
brew install zstd
brew install openssl
```
Possibly,
```bash
export RUSTFLAGS="-L$(brew --prefix zstd)/lib -L$(brew --prefix openssl)/lib"
```
In older Macs, there [have been some issues with Diesel CLI](https://github.com/diesel-rs/diesel/issues/2605)
that require a few extra dependencies to be installed:
```bash
# If you're on Mac and the above command didn't work, run the following and then retry:
brew install libpq
```
You're ready to run the migrations:
```bash
diesel migration run
```
You might get a scary message about `"Encountered unknown type for Mysql: enum"` -- you can safely ignore this
error if you see it in isolation. It doesn't impact the migrations whatsoever.
Finally, you'll need the sqlx CLI tool to run codegen.
You likely won't need this now, but if you change any queries, this will be necessary:
```bash
cargo install sqlx-cli --features rustls,mysql,sqlite
```
### Install Redis (Mac)
```bash
# Install Redis
brew install redis
# Start the service:
brew services start redis
```
+6
View File
@@ -0,0 +1,6 @@
# Secrets
db_secrets.toml
# Python Virtual Env
python/
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env python
# FIXME: Needs packages 'toml' and 'mysql-connector-python'. Put in requirements.txt.
import time
import toml
import mysql.connector
DB_SECRETS_FILE = 'db_secrets.toml'
# TODO: Share db-related setup when more scripts are added.
def read_database_secrets(secrets_file = DB_SECRETS_FILE):
secrets = None
with open(secrets_file, "r") as f:
secrets = toml.load(f)
required_keys = ['database', 'hostname', 'port', 'password', 'username']
for key in required_keys:
if key not in secrets:
raise Exception(f"Required key '#{key}' not present")
return secrets
def connect_to_mysql():
secrets = read_database_secrets()
connection = mysql.connector.connect(
host = secrets['hostname'],
port = secrets['port'],
user = secrets['username'],
password = secrets['password'],
database = secrets['database'],
)
return connection
connection = connect_to_mysql()
cursor = connection.cursor()
SAFE_ID = 76000000 # Reasonably completed on 2022-06-24
LIMIT = 100000
#def get_record_count():
# query = f"SELECT COUNT(*) FROM tts_inference_jobs"
# cursor.execute(query)
# result = cursor.fetchall()
# print(result)
# return result[0]
#
##total_count = get_record_count()
#total_count = 75082057
total_count = 21925602
def get_first_record(cursor):
query = f"select * from tts_inference_jobs order by id limit 1"
cursor.execute(query)
result = cursor.fetchall()
print(result)
return result
total_deleted = 0
keep_deleting = True
while keep_deleting:
query = f"DELETE FROM tts_inference_jobs WHERE id < {SAFE_ID} ORDER BY ID ASC LIMIT {LIMIT}"
query = f"DELETE FROM tts_inference_jobs WHERE status = 'complete_success' ORDER BY ID ASC LIMIT {LIMIT}"
#print(query)
cursor.execute(query)
connection.commit() # NB: Python does not autocommit changes!
#result = cursor.fetchall()
count = cursor.rowcount
total_deleted += count
percent_deleted = (total_deleted / total_count) * 100.0
print(f'Deleted {count}; total deleted = {total_deleted} ({percent_deleted:.4f})')
if count < LIMIT - 1:
keep_deleting = False
time.sleep(0.5)
#record = get_first_record(cursor)
#time.sleep(2)
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env python
# FIXME: Needs packages 'toml' and 'mysql-connector-python'. Put in requirements.txt.
import time
import toml
import mysql.connector
DB_SECRETS_FILE = 'db_secrets.toml'
# TODO: Share db-related setup when more scripts are added.
def read_database_secrets(secrets_file = DB_SECRETS_FILE):
secrets = None
with open(secrets_file, "r") as f:
secrets = toml.load(f)
required_keys = ['database', 'hostname', 'port', 'password', 'username']
for key in required_keys:
if key not in secrets:
raise Exception(f"Required key '#{key}' not present")
return secrets
def connect_to_mysql():
secrets = read_database_secrets()
connection = mysql.connector.connect(
host = secrets['hostname'],
port = secrets['port'],
user = secrets['username'],
password = secrets['password'],
database = secrets['database'],
)
return connection
connection = connect_to_mysql()
cursor = connection.cursor()
SAFE_ID = 76000000 # Reasonably completed on 2022-06-24
LIMIT = 100000
#def get_record_count():
# query = f"SELECT COUNT(*) FROM tts_inference_jobs"
# cursor.execute(query)
# result = cursor.fetchall()
# print(result)
# return result[0]
#
##total_count = get_record_count()
#total_count = 75082057
total_count = 21925602
def get_user_tokens(cursor):
query = f"""
SELECT
r.maybe_creator_user_token, r.creator_ip_address
FROM
tts_results as r
LEFT OUTER JOIN
users as u
ON
r.maybe_creator_user_token = u.token
WHERE
r.raw_inference_text LIKE '%spidermant%'
AND r.created_at > ( CURDATE() - INTERVAL 1 HOUR )
"""
cursor.execute(query)
results = cursor.fetchall()
user_tokens = [row[0] for row in results]
user_tokens = [token for token in user_tokens if token is not None]
user_tokens = set(user_tokens)
ip_addresses = [row[1] for row in results]
ip_addresses = [ip for ip in ip_addresses if ip is not None]
ip_addresses = set(ip_addresses)
return (user_tokens, ip_addresses)
(user_tokens, ip_addresses) = get_user_tokens(cursor)
print(user_tokens)
print(len(user_tokens))
print(len(ip_addresses))
with open('101_user_tokens.txt', 'w') as f:
for user_token in user_tokens:
f.write(user_token)
f.write("\n")
with open('101_ip_addresses.txt', 'w') as f:
for ip in ip_addresses:
f.write(ip)
f.write("\n")
+3
View File
@@ -0,0 +1,3 @@
mysql-connector-python==8.0.32
protobuf==3.20.3
toml==0.10.2
@@ -2,109 +2,32 @@ use serde_derive::{Deserialize, Serialize};
use tokens::tokens::app_session::AppSessionToken;
use utoipa::ToSchema;
/// This is used in Artcraft versions 0.0.1 (several) through 0.3.0
/// Clients to this version do not send generation count data (even if the endpoint supports it).
pub const LOG_ACTIVE_USER_V1_PATH: &str = "/v1/analytics/active_user";
pub const LOG_ACTIVE_USER_PATH: &str = "/v1/analytics/active_user";
/// This is used in Artcraft versions 0.4.0 and later.
pub const LOG_ACTIVE_USER_V2_PATH: &str = "/v1/analytics/active_user_v2";
#[derive(Serialize, Deserialize, ToSchema, Debug, Default)]
#[derive(Serialize, Deserialize, ToSchema, Debug)]
pub struct LogAppActiveUserRequest {
/// Clientside-generated session token.
/// If present, it'll be validated against an expected format.
#[serde(skip_serializing_if = "Option::is_none")]
pub maybe_app_session_token: Option<AppSessionToken>,
/// An override for the application platform/OS (windows, mac, linux).
#[serde(skip_serializing_if = "Option::is_none")]
pub maybe_os_platform: Option<String>,
/// An override for the version of the OS (e.g. 10.15.7, 11, 22.04).
#[serde(skip_serializing_if = "Option::is_none")]
pub maybe_os_version: Option<String>,
/// An override for the application name.
/// If set together with `maybe_app_version`, the two will be
/// If set together with `maybe_app_version`, the two will be
/// concatenated as `{maybe_app_name}/{maybe_app_version}`.
#[serde(skip_serializing_if = "Option::is_none")]
pub maybe_app_name: Option<String>,
/// An override for the application version.
/// If set together with `maybe_app_name`, the two will be
/// If set together with `maybe_app_name`, the two will be
/// concatenated as `{maybe_app_name}/{maybe_app_version}`.
#[serde(skip_serializing_if = "Option::is_none")]
pub maybe_app_version: Option<String>,
/// How long the user has been active in the app, in seconds.
#[serde(skip_serializing_if = "Option::is_none")]
pub maybe_session_duration_seconds: Option<u64>,
// ========== GENERATION COUNTS ==========
/// Number of items generated across all types.
#[serde(skip_serializing_if = "Option::is_none")]
pub total_generation_count: Option<u16>,
/// Number of images generated.
#[serde(skip_serializing_if = "Option::is_none")]
pub image_generation_count: Option<u16>,
/// Number of videos generated.
#[serde(skip_serializing_if = "Option::is_none")]
pub video_generation_count: Option<u16>,
/// Number of objects generated (meshes, etc.).
#[serde(skip_serializing_if = "Option::is_none")]
pub object_generation_count: Option<u16>,
/// Number of images generated with text to image.
#[serde(skip_serializing_if = "Option::is_none")]
pub text_to_image_count: Option<u16>,
/// Number of images generated with image to image (image edits).
#[serde(skip_serializing_if = "Option::is_none")]
pub image_to_image_count: Option<u16>,
/// Number of videos generated with text to video.
#[serde(skip_serializing_if = "Option::is_none")]
pub text_to_video_count: Option<u16>,
/// Number of videos generated with image to video.
#[serde(skip_serializing_if = "Option::is_none")]
pub image_to_video_count: Option<u16>,
/// Number of objects generated with text to object.
#[serde(skip_serializing_if = "Option::is_none")]
pub text_to_object_count: Option<u16>,
/// Number of objects generated with image to object.
#[serde(skip_serializing_if = "Option::is_none")]
pub image_to_object_count: Option<u16>,
/// Number of prompts from the image page.
#[serde(skip_serializing_if = "Option::is_none")]
pub image_page_prompt_count: Option<u16>,
/// Number of prompts from the video page.
#[serde(skip_serializing_if = "Option::is_none")]
pub video_page_prompt_count: Option<u16>,
/// Number of prompts from the edit page.
#[serde(skip_serializing_if = "Option::is_none")]
pub edit_page_prompt_count: Option<u16>,
/// Number of prompts from the stage page.
#[serde(skip_serializing_if = "Option::is_none")]
pub stage_page_prompt_count: Option<u16>,
/// Number of prompts from the object page.
#[serde(skip_serializing_if = "Option::is_none")]
pub object_page_prompt_count: Option<u16>,
/// Number of prompts from other pages.
#[serde(skip_serializing_if = "Option::is_none")]
pub other_page_prompt_count: Option<u16>,
}
#[derive(Serialize, Deserialize, ToSchema)]
@@ -7,5 +7,4 @@ pub mod media_file;
pub mod prompts;
pub mod stripe_artcraft;
pub mod subscriptions;
pub mod users;
pub mod utils;
@@ -1,54 +0,0 @@
use enums::common::artcraft_subscription_slug::ArtcraftSubscriptionSlug;
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
pub const CREATE_NEW_USER_ACCOUNT_AND_SUBSCRIPTION_CHECKOUT_URL_PATH: &str = "/v1/stripe_artcraft/user_signup/subscription_checkout";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct StripeArtcraftCreateSubscriptionCheckoutWithUserSignupRequest {
/// The (non-Stripe) internal identifier for the product or subscription.
/// This will be translated into a Stripe identifier.
pub plan: Option<ArtcraftSubscriptionSlug>,
pub cadence: Option<PlanBillingCadence>,
}
#[derive(Serialize, Deserialize, ToSchema, Debug, Copy, Clone)]
pub enum PlanBillingCadence {
#[serde(rename = "monthly")]
Monthly,
#[serde(rename = "yearly")]
Yearly,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct StripeArtcraftCreateSubscriptionCheckoutWithUserSignupResponse {
pub success: bool,
/// The checkout session URL.
pub stripe_checkout_redirect_url: String,
/// If a user account was created, these are the details.
pub generated_user: Option<UserDetails>,
/// If a session was created, these are the details.
pub session: Option<SessionDetails>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct UserDetails {
/// The generated username.
pub username: String,
/// The generated display name.
pub display_name: String,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct SessionDetails {
/// A signed session that can be sent as a header, bypassing cookies.
/// This is useful for API clients that don't support cookies or Google
/// browsers killing cross-domain cookies.
pub signed_session: String,
}
@@ -1,4 +1,3 @@
pub mod create_create_new_user_account_and_subscription_checkout;
pub mod create_credits_pack_checkout;
pub mod create_subscription_checkout;
pub mod customer_portal_cancel_plan;
@@ -1,13 +0,0 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Serialize, Deserialize, ToSchema)]
pub struct ChangePasswordRequest {
pub password: String,
pub password_confirmation: String,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct ChangePasswordResponse {
pub success: bool,
}
@@ -1,12 +0,0 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Serialize, Deserialize, ToSchema)]
pub struct EditEmailRequest {
pub email_address: String,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct EditEmailResponse {
pub success: bool,
}
@@ -1,12 +0,0 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Serialize, Deserialize, ToSchema)]
pub struct EditUsernameRequest {
pub display_name: String,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct EditUsernameResponse {
pub success: bool,
}
@@ -1,4 +0,0 @@
pub mod change_password;
pub mod edit_email;
pub mod edit_username;
pub mod session_info;
@@ -1,99 +0,0 @@
use crate::common::responses::user_details_light::UserDetailsLight;
use enums::by_table::users::user_feature_flag::UserFeatureFlag;
use serde_derive::Serialize;
use std::collections::BTreeSet;
use tokens::tokens::users::UserToken;
use utoipa::ToSchema;
#[derive(Serialize, Copy, Clone, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum FakeYouPlan {
Free,
Basic,
Standard,
Pro,
}
#[derive(Serialize, Copy, Clone, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum StorytellerStreamPlan {
Free,
Basic,
Standard,
Pro,
}
#[derive(Serialize, ToSchema)]
pub struct SessionUserInfo {
pub core_info: UserDetailsLight,
pub user_token: UserToken,
pub username: String,
pub display_name: String,
pub email_gravatar_hash: String,
pub onboarding: SessionOnboardingState,
// TODO(bt,2024-03-30): Remove legacy feature flag
#[deprecated(note = "DO NOT USE. Use `maybe_feature_flags` instead! The flag you're looking for is `studio`.")]
pub can_access_studio: bool,
/// Collection of feature / rollout flags
/// This is the proper place to detect if a user has access to some rollout (non-paywall) feature.
/// NB: The BTreeSet maintains order so React doesn't introduce re-render state bugs when order changes
pub maybe_feature_flags: BTreeSet<UserFeatureFlag>,
// Premium plans:
#[deprecated(note = "DO NOT USE. This was never used and is 100% meaningless.")]
pub fakeyou_plan: FakeYouPlan,
#[deprecated(note = "DO NOT USE. This was never used and is 100% meaningless.")]
pub storyteller_stream_plan: StorytellerStreamPlan,
// Usage permissions:
pub can_use_tts: bool,
pub can_use_w2l: bool,
pub can_delete_own_tts_results: bool,
pub can_delete_own_w2l_results: bool,
pub can_delete_own_account: bool,
// Contribution permissions:
pub can_upload_tts_models: bool,
pub can_upload_w2l_templates: bool,
pub can_delete_own_tts_models: bool,
pub can_delete_own_w2l_templates: bool,
// Moderation permissions:
pub can_approve_w2l_templates: bool,
pub can_edit_other_users_profiles: bool,
pub can_edit_other_users_tts_models: bool,
pub can_edit_other_users_w2l_templates: bool,
pub can_delete_other_users_tts_models: bool,
pub can_delete_other_users_tts_results: bool,
pub can_delete_other_users_w2l_templates: bool,
pub can_delete_other_users_w2l_results: bool,
pub can_ban_users: bool,
pub can_delete_users: bool,
}
#[derive(Serialize, ToSchema)]
pub struct SessionOnboardingState {
/// If true, the user hasn't set their email.
pub email_not_set: bool,
/// If true, the user hasn't confirmed their email.
pub email_not_confirmed: bool,
/// If true, the user hasn't set their password.
pub password_not_set: bool,
/// If true, the user hasn't set their username (the username they have is random).
pub username_not_customized: bool,
}
#[derive(Serialize, ToSchema)]
pub struct SessionInfoSuccessResponse {
pub success: bool,
pub logged_in: bool,
pub user: Option<SessionUserInfo>,
}
@@ -2,7 +2,7 @@ use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_query_string_post_request::basic_query_string_post_request;
use artcraft_api_defs::analytics::log_active_user::{LogAppActiveUserRequest, LogAppActiveUserResponse, LOG_ACTIVE_USER_V1_PATH};
use artcraft_api_defs::analytics::log_active_user::{LogAppActiveUserRequest, LogAppActiveUserResponse, LOG_ACTIVE_USER_PATH};
use std::collections::HashMap;
pub async fn log_active_user(
@@ -41,7 +41,7 @@ pub async fn log_active_user(
Ok(basic_query_string_post_request(
api_host,
LOG_ACTIVE_USER_V1_PATH,
LOG_ACTIVE_USER_PATH,
maybe_creds,
&query,
).await?)
@@ -1,18 +0,0 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::analytics::log_active_user::{LogAppActiveUserRequest, LogAppActiveUserResponse, LOG_ACTIVE_USER_V2_PATH};
pub async fn log_active_user_v2(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: LogAppActiveUserRequest,
) -> Result<LogAppActiveUserResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
LOG_ACTIVE_USER_V2_PATH,
maybe_creds,
&request,
).await?)
}
@@ -1,2 +1 @@
pub mod log_active_user;
pub mod log_active_user_v2;
pub mod log_active_user;
@@ -1,7 +1,6 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::endpoints::fal_ai::bytedance::seedream::bytedance_seedream_v4_edit_image::{bytedance_seedream_v4_edit_image, BytedanceSeedreamV4EditImageInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
@@ -54,23 +53,6 @@ pub enum EnqueueBytedanceSeedreamV4EditImageSize {
Auto4k,
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueBytedanceSeedreamV4EditImageArgs<'_, U> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// Your request will cost $0.03 per image.
let unit_cost = 3;
let cost = match self.num_images {
None => unit_cost,
Some(EnqueueBytedanceSeedreamV4EditImageNumImages::One) => unit_cost,
Some(EnqueueBytedanceSeedreamV4EditImageNumImages::Two) => unit_cost * 2,
Some(EnqueueBytedanceSeedreamV4EditImageNumImages::Three) => unit_cost * 3,
Some(EnqueueBytedanceSeedreamV4EditImageNumImages::Four) => unit_cost * 4,
};
cost as UsdCents
}
}
pub async fn enqueue_bytedance_seedream_v4_edit_image_webhook<R: IntoUrl>(
args: EnqueueBytedanceSeedreamV4EditImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
@@ -1,7 +1,6 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::endpoints::fal_ai::bytedance::seedream::bytedance_seedream_v4p5_edit_image::{bytedance_seedream_v4p5_edit_image, BytedanceSeedreamV4p5EditImageInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
@@ -53,23 +52,6 @@ pub enum EnqueueBytedanceSeedreamV4p5EditImageSize {
Auto4k,
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueBytedanceSeedreamV4p5EditImageArgs<'_, U> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// Your request will cost $0.04 per image.
let unit_cost = 4;
let cost = match self.num_images {
None => unit_cost,
Some(EnqueueBytedanceSeedreamV4p5EditImageNumImages::One) => unit_cost,
Some(EnqueueBytedanceSeedreamV4p5EditImageNumImages::Two) => unit_cost * 2,
Some(EnqueueBytedanceSeedreamV4p5EditImageNumImages::Three) => unit_cost * 3,
Some(EnqueueBytedanceSeedreamV4p5EditImageNumImages::Four) => unit_cost * 4,
};
cost as UsdCents
}
}
pub async fn enqueue_bytedance_seedream_v4p5_edit_image_webhook<R: IntoUrl>(
args: EnqueueBytedanceSeedreamV4p5EditImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
@@ -1,7 +1,6 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::endpoints::fal_ai::gemini_25_flash_image::edit::gemini_25_flash_image_edit;
use fal::prelude::fal_ai::gemini_25_flash_image::edit::Gemini25FlashImageEditInput;
use fal::webhook::WebhookResponse;
@@ -50,20 +49,6 @@ pub enum Gemini25FlashEditAspectRatio {
NineBySixteen, // NB: No NineByTwentyOne ?
}
impl <U: IntoUrl> FalRequestCostCalculator for Gemini25FlashEditArgs<'_, U> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// Your request will cost $0.039 per image. For $1.00, you can run this model 25 times.
match self.num_images {
Gemini25FlashEditNumImages::One => 4,
Gemini25FlashEditNumImages::Two => 8,
Gemini25FlashEditNumImages::Three => 12,
Gemini25FlashEditNumImages::Four => 16,
}
}
}
pub async fn enqueue_gemini_25_flash_edit_webhook<R: IntoUrl>(
args: Gemini25FlashEditArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
@@ -2,7 +2,6 @@ use crate::creds::fal_api_key::FalApiKey;
use crate::creds::open_ai_api_key::OpenAiApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::endpoints::fal_ai::gpt_image_1::edit_image::byok::{gpt_edit_image, GptEditImageRequest};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
@@ -45,31 +44,6 @@ pub enum GptEditImageNumImages{
Four,
}
// NB: These are BYOK, so they're not Fal's prices
impl <U: IntoUrl> FalRequestCostCalculator for GptEditImageByokArgs<'_, U> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// Can't find details, so using this: https://www.reddit.com/r/OpenAI/comments/1krfwa1/pricing_gpt_image_1_model/
// Prompts are billed similarly to other GPT models. Image outputs cost approximately $0.01 (low), $0.04 (medium),
// and $0.17 (high) for square images.
// We're likely losing money on this, but that's okay. Will adjust in the future to be fair to users and us.
let base_cost = match self.quality {
GptEditImageQuality::Auto => 17,
GptEditImageQuality::Low => 1,
GptEditImageQuality::Medium => 4,
GptEditImageQuality::High => 17,
};
let cost = match self.num_images {
GptEditImageNumImages::One => base_cost,
GptEditImageNumImages::Two => base_cost * 2,
GptEditImageNumImages::Three => base_cost * 3,
GptEditImageNumImages::Four => base_cost * 4,
};
cost as UsdCents
}
}
pub async fn enqueue_gpt_image_1_edit_image_webhook<V: IntoUrl>(
args: GptEditImageByokArgs<'_, V>
) -> Result<WebhookResponse, FalErrorPlus> {
@@ -1,7 +1,6 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::endpoints::fal_ai::gpt_image::gpt_image_1p5_image_edit::{gpt_image_1p5_image_edit, GptImage1p5ImageEditInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
@@ -70,34 +69,6 @@ pub enum EnqueueGptImage1p5EditImageOutputFormat {
Webp,
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueGptImage1p5EditImageArgs<'_, U> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// Your request will cost different amounts based on the number of images, quality, and size.
// For low quality, you will be charged $0.009 for 1024x1024 or $0.013 for any other size per image.
// For medium quality, you will be charged $0.034 for 1024x1024, $0.051 for 1024x1536 and $0.050 for 1536x1024 per image.
// For high quality, you will be charged $0.133 for 1024x1024, $0.200 for 1024x1536 or $0.199 for 1536x1024 per image.
let use_quality = self.quality.unwrap_or(EnqueueGptImage1p5EditImageQuality::Medium);
let use_size = self.image_size.unwrap_or(EnqueueGptImage1p5EditImageSize::Square);
let base_cost = match (use_quality, use_size) {
(EnqueueGptImage1p5EditImageQuality::Low, EnqueueGptImage1p5EditImageSize::Square) => 1,
(EnqueueGptImage1p5EditImageQuality::Low, _) => 1,
(EnqueueGptImage1p5EditImageQuality::Medium, EnqueueGptImage1p5EditImageSize::Square) => 3,
(EnqueueGptImage1p5EditImageQuality::Medium, _) => 5,
(EnqueueGptImage1p5EditImageQuality::High, EnqueueGptImage1p5EditImageSize::Square) => 13,
(EnqueueGptImage1p5EditImageQuality::High, _) => 20,
};
let cost = match self.num_images {
EnqueueGptImage1p5EditImageNumImages::One => base_cost,
EnqueueGptImage1p5EditImageNumImages::Two => base_cost * 2,
EnqueueGptImage1p5EditImageNumImages::Three => base_cost * 3,
EnqueueGptImage1p5EditImageNumImages::Four => base_cost * 4,
};
cost as UsdCents
}
}
pub async fn enqueue_gpt_image_1p5_image_edit_webhook<R: IntoUrl>(
args: EnqueueGptImage1p5EditImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
@@ -1,7 +1,6 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::endpoints::fal_ai::nano_banana_pro::nano_banana_pro_image_edit::{nano_banana_pro_image_edit, NanoBananaProImageEditInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
@@ -57,30 +56,6 @@ pub enum EnqueueNanoBananaProEditImageAspectRatio {
NineBySixteen, // NB: No NineByTwentyOne ?
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueNanoBananaProEditImageArgs<'_, U> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// Your request will cost $0.15 per image.
// For $1.00, you can run this model 7 times.
// 4K outputs will be charged at double the standard rate.
// If web search is used, an additional $0.015 will be charged.
// Note: Pricing may change in the future.
let cost = match self.resolution {
None => 15,
Some(EnqueueNanoBananaProEditImageResolution::OneK) => 15,
Some(EnqueueNanoBananaProEditImageResolution::TwoK) => 15,
Some(EnqueueNanoBananaProEditImageResolution::FourK) => 30,
};
let cost = match self.num_images {
EnqueueNanoBananaProEditImageNumImages::One => cost,
EnqueueNanoBananaProEditImageNumImages::Two => cost * 2,
EnqueueNanoBananaProEditImageNumImages::Three => cost * 3,
EnqueueNanoBananaProEditImageNumImages::Four => cost * 4,
};
cost as UsdCents
}
}
pub async fn enqueue_nano_banana_pro_image_edit_webhook<R: IntoUrl>(
args: EnqueueNanoBananaProEditImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
@@ -1,7 +1,6 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::endpoints::fal_ai::qwen_image_edit::{qwen_image_edit, QwenImageEditInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
@@ -46,23 +45,6 @@ pub enum QwenImageEditSize {
//Custom { width: u32, height: u32 }, // TODO
}
impl <U: IntoUrl, R: IntoUrl> FalRequestCostCalculator for QwenImageEditArgs<'_, U, R> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// Your request will cost $0.03 per megapixel. -- Ugh, not calculating that
let base_cost = 3;
let cost = match self.num_images {
None => base_cost,
Some(QwenImageEditNumImages::One) => base_cost,
Some(QwenImageEditNumImages::Two) => base_cost * 2,
Some(QwenImageEditNumImages::Three) => base_cost * 3,
Some(QwenImageEditNumImages::Four) => base_cost * 4,
};
cost as UsdCents
}
}
pub async fn enqueue_qwen_image_edit_webhook<U: IntoUrl, R: IntoUrl>(
args: QwenImageEditArgs<'_, U, R>
) -> Result<WebhookResponse, FalErrorPlus> {
@@ -1,7 +1,6 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::endpoints::fal_ai::flux_pro::v1_1::{v1_1, FluxProPlusTextToImageInput, ImageSizeProperty};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
@@ -34,22 +33,6 @@ pub enum FluxPro11NumImages {
Four,
}
impl <U: IntoUrl> FalRequestCostCalculator for FluxPro11Args<'_, U> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// Your request will cost $0.04 per megapixel. -- NB: What? Not going to calculate that.
let base_cost = 6;
let cost = match self.num_images {
FluxPro11NumImages::One => base_cost,
FluxPro11NumImages::Two => base_cost * 2,
FluxPro11NumImages::Three => base_cost * 3,
FluxPro11NumImages::Four => base_cost * 4,
};
cost as UsdCents
}
}
pub async fn enqueue_flux_pro_11_text_to_image_webhook<U: IntoUrl>(
args: FluxPro11Args<'_, U>
) -> Result<WebhookResponse, FalErrorPlus> {
@@ -1,7 +1,6 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::endpoints::fal_ai::flux_pro::v1_1_ultra::{v1_1_ultra, AspectRatioProperty, FluxProUltraTextToImageInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
@@ -36,22 +35,6 @@ pub enum FluxPro11UltraNumImages {
Four,
}
impl <U: IntoUrl> FalRequestCostCalculator for FluxPro11UltraArgs<'_, U> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// Your request will cost $0.06 per image.
let base_cost = 6;
let cost = match self.num_images {
FluxPro11UltraNumImages::One => base_cost,
FluxPro11UltraNumImages::Two => base_cost * 2,
FluxPro11UltraNumImages::Three => base_cost * 3,
FluxPro11UltraNumImages::Four => base_cost * 4,
};
cost as UsdCents
}
}
pub async fn enqueue_flux_pro_11_ultra_text_to_image_webhook<U: IntoUrl>(
args: FluxPro11UltraArgs<'_, U>
) -> Result<WebhookResponse, FalErrorPlus> {
@@ -2,7 +2,6 @@ use crate::creds::fal_api_key::FalApiKey;
use crate::creds::open_ai_api_key::OpenAiApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::prelude::fal_ai::gpt_image_1::edit_image::byok::{gpt_text_to_image, GptTextToImageRequest};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
@@ -44,31 +43,6 @@ pub enum GptTextToImageNumImages{
Four,
}
// NB: These are BYOK, so they're not Fal's prices
impl <U: IntoUrl> FalRequestCostCalculator for GptTextToImageByokArgs<'_, U> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// Can't find details, so using this: https://www.reddit.com/r/OpenAI/comments/1krfwa1/pricing_gpt_image_1_model/
// Prompts are billed similarly to other GPT models. Image outputs cost approximately $0.01 (low), $0.04 (medium),
// and $0.17 (high) for square images.
// We're likely losing money on this, but that's okay. Will adjust in the future to be fair to users and us.
let base_cost = match self.quality {
GptTextToImageQuality::Auto => 17,
GptTextToImageQuality::Low => 1,
GptTextToImageQuality::Medium => 4,
GptTextToImageQuality::High => 17,
};
let cost = match self.num_images {
GptTextToImageNumImages::One => base_cost,
GptTextToImageNumImages::Two => base_cost * 2,
GptTextToImageNumImages::Three => base_cost * 3,
GptTextToImageNumImages::Four => base_cost * 4,
};
cost as UsdCents
}
}
pub async fn enqueue_gpt_image_1_text_to_image_webhook<V: IntoUrl>(
args: GptTextToImageByokArgs<'_, V>
) -> Result<WebhookResponse, FalErrorPlus> {
@@ -1,7 +1,6 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::endpoints::fal_ai::bytedance::seedream::bytedance_seedream_v4_text_to_image::BytedanceSeedreamV4TextToImageInput;
use fal::prelude::fal_ai::bytedance::seedream::bytedance_seedream_v4_text_to_image::bytedance_seedream_v4_text_to_image;
use fal::webhook::WebhookResponse;
@@ -54,24 +53,6 @@ pub enum EnqueueBytedanceSeedreamV4TextToImageSize {
Auto4k,
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueBytedanceSeedreamV4TextToImageArgs<'_, U> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// Copied from edit image version
// Your request will cost $0.03 per image.
let unit_cost = 3;
let cost = match self.num_images {
None => unit_cost,
Some(EnqueueBytedanceSeedreamV4TextToImageNumImages::One) => unit_cost,
Some(EnqueueBytedanceSeedreamV4TextToImageNumImages::Two) => unit_cost * 2,
Some(EnqueueBytedanceSeedreamV4TextToImageNumImages::Three) => unit_cost * 3,
Some(EnqueueBytedanceSeedreamV4TextToImageNumImages::Four) => unit_cost * 4,
};
cost as UsdCents
}
}
pub async fn enqueue_bytedance_seedream_v4_text_to_image_webhook<R: IntoUrl>(
args: EnqueueBytedanceSeedreamV4TextToImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
@@ -4,8 +4,6 @@ use crate::error::fal_error_plus::FalErrorPlus;
use fal::endpoints::fal_ai::bytedance::seedream::bytedance_seedream_v4p5_text_to_image::{bytedance_seedream_v4p5_text_to_image, BytedanceSeedreamV4p5TextToImageInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use crate::requests::webhook::image::edit::enqueue_bytedance_seedream_v4p5_edit_image_webhook::{EnqueueBytedanceSeedreamV4p5EditImageArgs, EnqueueBytedanceSeedreamV4p5EditImageNumImages};
pub struct EnqueueBytedanceSeedreamV4p5TextToImageArgs<'a, R: IntoUrl> {
// Request required
@@ -53,24 +51,6 @@ pub enum EnqueueBytedanceSeedreamV4p5TextToImageSize {
Auto4k,
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueBytedanceSeedreamV4p5TextToImageArgs<'_, U> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// NB: Copied from edit image costs
// Your request will cost $0.04 per image.
let unit_cost = 4;
let cost = match self.num_images {
None => unit_cost,
Some(EnqueueBytedanceSeedreamV4p5TextToImageNumImages::One) => unit_cost,
Some(EnqueueBytedanceSeedreamV4p5TextToImageNumImages::Two) => unit_cost * 2,
Some(EnqueueBytedanceSeedreamV4p5TextToImageNumImages::Three) => unit_cost * 3,
Some(EnqueueBytedanceSeedreamV4p5TextToImageNumImages::Four) => unit_cost * 4,
};
cost as UsdCents
}
}
pub async fn enqueue_bytedance_seedream_v4p5_text_to_image_webhook<R: IntoUrl>(
args: EnqueueBytedanceSeedreamV4p5TextToImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
@@ -5,8 +5,6 @@ use fal::prelude::fal_ai::gemini_25_flash_image::text_to_image::gemini_25_flash_
use fal::prelude::fal_ai::gemini_25_flash_image::text_to_image::Gemini25FlashTextToImageInput;
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use crate::requests::webhook::image::edit::enqueue_gemini_25_flash_edit_webhook::{Gemini25FlashEditArgs, Gemini25FlashEditNumImages};
pub struct Gemini25FlashTextToImageArgs<'a, R: IntoUrl> {
// Request required
@@ -48,21 +46,6 @@ pub enum Gemini25FlashTextToImageAspectRatio {
NineBySixteen, // NB: No NineByTwentyOne ?
}
impl <U: IntoUrl> FalRequestCostCalculator for Gemini25FlashTextToImageArgs<'_, U> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// NB: This was copied from the image-to-image case.
// Your request will cost $0.039 per image. For $1.00, you can run this model 25 times.
match self.num_images {
Gemini25FlashTextToImageNumImages::One => 4,
Gemini25FlashTextToImageNumImages::Two => 8,
Gemini25FlashTextToImageNumImages::Three => 12,
Gemini25FlashTextToImageNumImages::Four => 16,
}
}
}
pub async fn enqueue_gemini_25_flash_text_to_image_webhook<R: IntoUrl>(
args: Gemini25FlashTextToImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
@@ -1,7 +1,6 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::endpoints::fal_ai::gpt_image::gpt_image_1p5_text_to_image::{gpt_image_1p5_text_to_image, GptImage1p5TextToImageInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
@@ -62,34 +61,6 @@ pub enum EnqueueGptImage1p5TextToImageOutputFormat {
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueGptImage1p5TextToImageArgs<'_, U> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// NB: copied from edit image case - should be correct, but not verified.
// Your request will cost different amounts based on the number of images, quality, and size.
// For low quality, you will be charged $0.009 for 1024x1024 or $0.013 for any other size per image.
// For medium quality, you will be charged $0.034 for 1024x1024, $0.051 for 1024x1536 and $0.050 for 1536x1024 per image.
// For high quality, you will be charged $0.133 for 1024x1024, $0.200 for 1024x1536 or $0.199 for 1536x1024 per image.
let use_quality = self.quality.unwrap_or(EnqueueGptImage1p5TextToImageQuality::Medium);
let use_size = self.image_size.unwrap_or(EnqueueGptImage1p5TextToImageSize::Square);
let base_cost = match (use_quality, use_size) {
(EnqueueGptImage1p5TextToImageQuality::Low, EnqueueGptImage1p5TextToImageSize::Square) => 1,
(EnqueueGptImage1p5TextToImageQuality::Low, _) => 1,
(EnqueueGptImage1p5TextToImageQuality::Medium, EnqueueGptImage1p5TextToImageSize::Square) => 3,
(EnqueueGptImage1p5TextToImageQuality::Medium, _) => 5,
(EnqueueGptImage1p5TextToImageQuality::High, EnqueueGptImage1p5TextToImageSize::Square) => 13,
(EnqueueGptImage1p5TextToImageQuality::High, _) => 20,
};
let cost = match self.num_images {
EnqueueGptImage1p5TextToImageNumImages::One => base_cost,
EnqueueGptImage1p5TextToImageNumImages::Two => base_cost * 2,
EnqueueGptImage1p5TextToImageNumImages::Three => base_cost * 3,
EnqueueGptImage1p5TextToImageNumImages::Four => base_cost * 4,
};
cost as UsdCents
}
}
pub async fn enqueue_gpt_image_1p5_text_to_image_webhook<R: IntoUrl>(
args: EnqueueGptImage1p5TextToImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
@@ -4,8 +4,6 @@ use crate::error::fal_error_plus::FalErrorPlus;
use fal::endpoints::fal_ai::nano_banana_pro::nano_banana_pro_text_to_image::{nano_banana_pro_text_to_image, NanoBananaProTextToImageInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use crate::requests::webhook::image::edit::enqueue_nano_banana_pro_edit_image_webhook::{EnqueueNanoBananaProEditImageArgs, EnqueueNanoBananaProEditImageNumImages, EnqueueNanoBananaProEditImageResolution};
pub struct EnqueueNanoBananaProTextToImageArgs<'a, R: IntoUrl> {
// Request required
@@ -55,31 +53,6 @@ pub enum EnqueueNanoBananaProTextToImageAspectRatio {
NineBySixteen, // NB: No NineByTwentyOne ?
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueNanoBananaProTextToImageArgs<'_, U> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// Your request will cost $0.15 per image.
// For $1.00, you can run this model 7 times.
// 4K outputs will be charged at double the standard rate.
// If web search is used, an additional $0.015 will be charged.
// Note: Pricing may change in the future.
let cost = match self.resolution {
None => 15,
Some(EnqueueNanoBananaProTextToImageResolution::OneK) => 15,
Some(EnqueueNanoBananaProTextToImageResolution::TwoK) => 15,
Some(EnqueueNanoBananaProTextToImageResolution::FourK) => 30,
};
let cost = match self.num_images {
EnqueueNanoBananaProTextToImageNumImages::One => cost,
EnqueueNanoBananaProTextToImageNumImages::Two => cost * 2,
EnqueueNanoBananaProTextToImageNumImages::Three => cost * 3,
EnqueueNanoBananaProTextToImageNumImages::Four => cost * 4,
};
cost as UsdCents
}
}
pub async fn enqueue_nano_banana_pro_text_to_image_webhook<R: IntoUrl>(
args: EnqueueNanoBananaProTextToImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
+5 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "artcraft"
version = "0.6.0"
version = "0.3.0"
description = "Artcraft"
authors = ["you"]
license = ""
@@ -54,7 +54,6 @@ bytes = "1.10.1"
chrono.workspace = true
directories = "6.0.0"
dirs = "6.0.0"
discord-rich-presence = "1.1.0"
futures = "0.3"
image = "0.25.5"
indexmap.workspace = true
@@ -78,6 +77,10 @@ url.workspace = true
# NB: We need to vendor this in so that we can access the cookies inside via the private API
tauri-plugin-http = { path = "../../vendor/tauri-plugin-http" }
# NB: The version of the `discord-rich-presence` crate (0.2.5) last published has non-Send/Sync error
# types and can't do threading. We need to wait for a new version to unpin from Git.
discord-rich-presence = { git = "https://github.com/vionya/discord-rich-presence" }
tauri-plugin-opener = "2"
# # Transparent title bar on MacOS
@@ -1,8 +1,8 @@
use serde_derive::{Deserialize, Serialize};
/// This is a comprehensive list of common aspect ratios you can specify when enqueuing a generation.
/// This is a comprehensive list of common aspect ratios.
/// Not every model will support every aspect ratio.
/// In the case a model doesn't support the aspect ratio, gracefully pick the nearest option.
/// In the case a model doesn't support the aspect ratio, pick the nearest option.
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CommonAspectRatio {
@@ -1,2 +1 @@
pub mod aspect_ratio;
pub mod resolution;
pub mod aspect_ratio;
@@ -1,17 +0,0 @@
use serde_derive::{Deserialize, Serialize};
/// This is a comprehensive list of common resolutions you can specify when enqueuing a generation.
/// Not every model will support every resolution
/// In the case a model doesn't support the resolution, gracefully pick the nearest option.
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CommonResolution {
/// Models: Nano Banana Pro
OneK,
/// Models: Nano Banana Pro
TwoK,
/// Models: (None)
ThreeK,
/// Models: Nano Banana Pro
FourK,
}
@@ -1 +0,0 @@
pub mod resolution_to_artcraft_nano_banana_pro;
@@ -1,12 +0,0 @@
use crate::core::api_adapters::resolution::common_resolution::CommonResolution;
use artcraft_api_defs::generate::image::multi_function::nano_banana_pro_multi_function_image_gen::NanoBananaProMultiFunctionImageGenImageResolution;
pub fn resolution_to_artcraft_nano_banana_pro(resolution: CommonResolution) -> NanoBananaProMultiFunctionImageGenImageResolution {
match resolution {
CommonResolution::OneK => NanoBananaProMultiFunctionImageGenImageResolution::OneK,
CommonResolution::TwoK => NanoBananaProMultiFunctionImageGenImageResolution::TwoK,
// NB: ThreeK doesn't exist in Nano Banana Pro, use FourK as nearest higher resolution
CommonResolution::ThreeK => NanoBananaProMultiFunctionImageGenImageResolution::FourK,
CommonResolution::FourK => NanoBananaProMultiFunctionImageGenImageResolution::FourK,
}
}
@@ -1,2 +0,0 @@
pub mod common_resolution;
pub mod convert;
@@ -18,9 +18,6 @@ pub struct AppPreferencesPayload {
/// Play sounds on events.
pub play_sounds: bool,
/// Key pointing to file; defined in the frontend code.
pub delete_file_sound: Option<String>,
/// Key pointing to file; defined in the frontend code.
/// Defined for enqueue since image enqueue can be async
pub enqueue_success_sound: Option<String>,
@@ -64,7 +61,6 @@ async fn get_prefs(app_prefs: &AppPreferencesManager) -> AnyhowResult<AppPrefere
Ok(AppPreferencesPayload {
preferred_download_directory: prefs.preferred_download_directory,
play_sounds: prefs.play_sounds,
delete_file_sound: prefs.delete_file_sound,
enqueue_success_sound: prefs.enqueue_success_sound,
enqueue_failure_sound: prefs.enqueue_failure_sound,
generation_success_sound: prefs.generation_success_sound,
@@ -28,7 +28,6 @@ pub enum ValueType {
pub enum PreferenceName {
PreferredDownloadDirectory,
PlaySounds,
DeleteFileSound,
EnqueueSuccessSound,
EnqueueFailureSound,
GenerationSuccessSound,
@@ -86,11 +85,6 @@ async fn update_prefs(
return Err(anyhow!("Invalid value: {:?}", request.value)),
}
}
PreferenceName::DeleteFileSound => {
prefs.delete_file_sound = request.value
.map(|val| string_value(&val))
.transpose()?;
}
PreferenceName::EnqueueSuccessSound => {
prefs.enqueue_success_sound = request.value
.map(|val| string_value(&val))
@@ -12,8 +12,6 @@ use crate::core::events::generation_events::generation_enqueue_failure_event::Ge
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::core::events::sendable_event_trait::SendableEvent;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_type::{ArtcraftUsagePage, ArtcraftUsageType};
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
@@ -99,7 +97,6 @@ pub async fn enqueue_image_bg_removal_command(
request: EnqueueImageBgRemovalCommand,
app_data_root: State<'_, AppDataRoot>,
app_env_configs: State<'_, AppEnvConfigs>,
artcraft_usage_tracker: State<'_, ArtcraftUsageTracker>,
provider_priority_store: State<'_, ProviderPriorityStore>,
task_database: State<'_, TaskDatabase>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
@@ -112,7 +109,6 @@ pub async fn enqueue_image_bg_removal_command(
&app,
&app_data_root,
&app_env_configs,
&artcraft_usage_tracker,
&provider_priority_store,
&task_database,
&storyteller_creds_manager,
@@ -161,7 +157,6 @@ pub async fn handle_request(
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
artcraft_usage_tracker: &ArtcraftUsageTracker,
provider_priority_store: &ProviderPriorityStore,
task_database: &TaskDatabase,
storyteller_creds_manager: &StorytellerCredentialManager,
@@ -190,11 +185,6 @@ pub async fn handle_request(
error!("Failed to create task in database: {:?}", err);
// NB: Fail open, but find a way to flag this.
}
if let Err(err) = artcraft_usage_tracker.record_image_generation(1, ArtcraftUsageType::ImageToResult, ArtcraftUsagePage::EditPage) {
// NB: Fail open.
warn!("Failed to report usage: {:?}", err);
}
Ok(success_event)
}
@@ -43,8 +43,6 @@ use storyteller_client::error::storyteller_error::StorytellerError;
use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, Manager, State};
use tokens::tokens::media_files::MediaFileToken;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_type::{ArtcraftUsagePage, ArtcraftUsageType};
/// This is used in the Tauri command bridge.
/// Don't change the serializations without coordinating with the frontend.
@@ -206,7 +204,6 @@ pub async fn enqueue_edit_image_command(
request: EnqueueEditImageCommand,
app_data_root: State<'_, AppDataRoot>,
app_env_configs: State<'_, AppEnvConfigs>,
artcraft_usage_tracker: State<'_, ArtcraftUsageTracker>,
provider_priority_store: State<'_, ProviderPriorityStore>,
task_database: State<'_, TaskDatabase>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
@@ -222,7 +219,6 @@ pub async fn enqueue_edit_image_command(
&app,
&app_data_root,
&app_env_configs,
&artcraft_usage_tracker,
&provider_priority_store,
&task_database,
&storyteller_creds_manager,
@@ -273,7 +269,6 @@ pub async fn handle_request(
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
artcraft_usage_tracker: &ArtcraftUsageTracker,
provider_priority_store: &ProviderPriorityStore,
task_database: &TaskDatabase,
storyteller_creds_manager: &StorytellerCredentialManager,
@@ -347,13 +342,6 @@ pub async fn handle_request(
error!("Failed to create task in database: {:?}", err);
// NB: Fail open, but find a way to flag this.
}
// TODO: We don't know that this is strictly the edit page. It could be the stage. It might also be nice to distinguish inpainting.
let num_images = request.image_count.map(|count| count as u16).unwrap_or(1);
if let Err(err) = artcraft_usage_tracker.record_image_generation(num_images, ArtcraftUsageType::ImageToResult, ArtcraftUsagePage::EditPage) {
// NB: Fail open.
warn!("Failed to report usage: {:?}", err);
}
Ok(success_event)
}
@@ -41,8 +41,6 @@ use storyteller_client::error::storyteller_error::StorytellerError;
use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, Manager, State};
use tokens::tokens::media_files::MediaFileToken;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_type::{ArtcraftUsagePage, ArtcraftUsageType};
#[derive(Deserialize, Debug, Copy, Clone)]
#[serde(rename_all = "snake_case")]
@@ -157,7 +155,6 @@ pub async fn enqueue_image_inpaint_command(
request: EnqueueInpaintImageCommand,
app_data_root: State<'_, AppDataRoot>,
app_env_configs: State<'_, AppEnvConfigs>,
artcraft_usage_tracker: State<'_, ArtcraftUsageTracker>,
provider_priority_store: State<'_, ProviderPriorityStore>,
task_database: State<'_, TaskDatabase>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
@@ -173,7 +170,6 @@ pub async fn enqueue_image_inpaint_command(
&app,
&app_data_root,
&app_env_configs,
&artcraft_usage_tracker,
&provider_priority_store,
&task_database,
&storyteller_creds_manager,
@@ -224,7 +220,6 @@ pub async fn handle_request(
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
artcraft_usage_tracker: &ArtcraftUsageTracker,
provider_priority_store: &ProviderPriorityStore,
task_database: &TaskDatabase,
storyteller_creds_manager: &StorytellerCredentialManager,
@@ -277,13 +272,6 @@ pub async fn handle_request(
// NB: Fail open, but find a way to flag this.
}
// TODO: We don't know that this is strictly the edit page. It could be the stage. It might also be nice to distinguish inpainting.
let num_images = request.image_count.map(|count| count as u16).unwrap_or(1);
if let Err(err) = artcraft_usage_tracker.record_image_generation(num_images, ArtcraftUsageType::ImageToResult, ArtcraftUsagePage::EditPage) {
// NB: Fail open.
warn!("Failed to report usage: {:?}", err);
}
Ok(success_event)
}
@@ -30,8 +30,6 @@ use serde::{Deserialize, Serialize};
use sqlite_tasks::queries::create_task::{create_task, CreateTaskArgs};
use tauri::{AppHandle, State};
use tokens::tokens::media_files::MediaFileToken;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_type::{ArtcraftUsagePage, ArtcraftUsageType};
/// This is used in the Tauri command bridge.
/// Don't change the serializations without coordinating with the frontend.
@@ -99,7 +97,6 @@ pub async fn enqueue_image_to_gaussian_command(
app: AppHandle,
app_data_root: State<'_, AppDataRoot>,
app_env_configs: State<'_, AppEnvConfigs>,
artcraft_usage_tracker: State<'_, ArtcraftUsageTracker>,
task_database: State<'_, TaskDatabase>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
worldlabs_creds_manager: State<'_, WorldlabsCredentialManager>,
@@ -111,7 +108,6 @@ pub async fn enqueue_image_to_gaussian_command(
request,
&app,
&app_data_root,
&artcraft_usage_tracker,
&task_database,
&storyteller_creds_manager,
&worldlabs_creds_manager,
@@ -172,7 +168,6 @@ pub async fn handle_request(
request: EnqueueImageToGaussianRequest,
app: &AppHandle,
app_data_root: &AppDataRoot,
artcraft_usage_tracker: &ArtcraftUsageTracker,
task_database: &TaskDatabase,
storyteller_creds_manager: &StorytellerCredentialManager,
worldlabs_creds_manager: &WorldlabsCredentialManager,
@@ -206,12 +201,7 @@ pub async fn handle_request(
error!("Failed to create task in database: {:?}", err);
// NB: Fail open, but find a way to flag this.
}
if let Err(err) = artcraft_usage_tracker.record_object_generation(1, ArtcraftUsageType::ImageToResult, ArtcraftUsagePage::OtherPage) {
// NB: Fail open.
warn!("Failed to report usage: {:?}", err);
}
Ok(success_event)
}
@@ -19,8 +19,6 @@ use log::{error, info, warn};
use serde_derive::{Deserialize, Serialize};
use tauri::{AppHandle, State};
use tokens::tokens::media_files::MediaFileToken;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_type::{ArtcraftUsagePage, ArtcraftUsageType};
#[derive(Deserialize)]
pub struct EnqueueImageTo3dObjectRequest {
@@ -90,7 +88,6 @@ pub async fn enqueue_image_to_3d_object_command(
request: EnqueueImageTo3dObjectRequest,
app_env_configs: State<'_, AppEnvConfigs>,
app_data_root: State<'_, AppDataRoot>,
artcraft_usage_tracker: State<'_, ArtcraftUsageTracker>,
provider_priority_store: State<'_, ProviderPriorityStore>,
task_database: State<'_, TaskDatabase>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
@@ -104,7 +101,6 @@ pub async fn enqueue_image_to_3d_object_command(
&app,
&app_env_configs,
&app_data_root,
&artcraft_usage_tracker,
&provider_priority_store,
&task_database,
&storyteller_creds_manager,
@@ -175,7 +171,6 @@ pub async fn handle_request(
app: &AppHandle,
app_env_configs: &AppEnvConfigs,
app_data_root: &AppDataRoot,
artcraft_usage_tracker: &ArtcraftUsageTracker,
provider_priority_store: &ProviderPriorityStore,
task_database: &TaskDatabase,
storyteller_creds_manager: &StorytellerCredentialManager,
@@ -208,10 +203,6 @@ pub async fn handle_request(
// NB: Fail open, but find a way to flag this.
}
if let Err(err) = artcraft_usage_tracker.record_object_generation(1, ArtcraftUsageType::ImageToResult, ArtcraftUsagePage::ObjectPage) {
// NB: Fail open.
warn!("Failed to report usage: {:?}", err);
}
Ok(success_event)
}
@@ -14,7 +14,6 @@ use crate::core::events::generation_events::common::GenerationAction;
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
use crate::core::state::task_database::TaskDatabase;
@@ -28,7 +27,6 @@ use log::{error, info, warn};
use serde_derive::{Deserialize, Serialize};
use tauri::{AppHandle, State};
use tokens::tokens::media_files::MediaFileToken;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_type::{ArtcraftUsagePage, ArtcraftUsageType};
/// This is used in the Tauri command bridge.
/// Don't change the serializations without coordinating with the frontend.
@@ -177,7 +175,6 @@ pub async fn enqueue_image_to_video_command(
app: AppHandle,
app_env_configs: State<'_, AppEnvConfigs>,
app_data_root: State<'_, AppDataRoot>,
artcraft_usage_tracker: State<'_, ArtcraftUsageTracker>,
provider_priority_store: State<'_, ProviderPriorityStore>,
task_database: State<'_, TaskDatabase>,
grok_creds_manager: State<'_, GrokCredentialManager>,
@@ -193,7 +190,6 @@ pub async fn enqueue_image_to_video_command(
&app,
&app_env_configs,
&app_data_root,
&artcraft_usage_tracker,
&provider_priority_store,
&task_database,
&grok_creds_manager,
@@ -266,7 +262,6 @@ pub async fn handle_request(
app: &AppHandle,
app_env_configs: &AppEnvConfigs,
app_data_root: &AppDataRoot,
artcraft_usage_tracker: &ArtcraftUsageTracker,
provider_priority_store: &ProviderPriorityStore,
task_database: &TaskDatabase,
grok_creds_manager: &GrokCredentialManager,
@@ -336,10 +331,5 @@ pub async fn handle_request(
// NB: Fail open, but find a way to flag this.
}
if let Err(err) = artcraft_usage_tracker.record_video_generation(1, ArtcraftUsageType::ImageToResult, ArtcraftUsagePage::VideoPage) {
// NB: Fail open.
warn!("Failed to report usage: {:?}", err);
}
Ok(success_event)
}
@@ -62,7 +62,8 @@ pub async fn handle_artcraft_gpt_image_1p5_text_to_image(
prompt: request.prompt.clone(),
image_size: aspect_ratio,
num_images,
image_media_tokens: request.image_media_tokens.clone(),
// Not provided for text-to-image
image_media_tokens: None,
mask_image_token: None,
input_fidelity: None,
// Not provided
@@ -1,7 +1,5 @@
use crate::core::api_adapters::aspect_ratio::common_aspect_ratio::CommonAspectRatio;
use crate::core::api_adapters::aspect_ratio::convert::aspect_ratio_to_artcraft_nano_banana_pro::aspect_ratio_to_artcraft_nano_banana_pro;
use crate::core::api_adapters::resolution::common_resolution::CommonResolution;
use crate::core::api_adapters::resolution::convert::resolution_to_artcraft_nano_banana_pro::resolution_to_artcraft_nano_banana_pro;
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason, ProviderFailureReason};
use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::{EditImageQuality, EditImageResolution, EditImageSize};
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
@@ -63,12 +61,18 @@ pub async fn handle_artcraft_nano_banana_pro_text_to_image(
};
let aspect_ratio = get_aspect_ratio(request);
let resolution = get_resolution(request);
let resolution = request.image_resolution
.map(|image_resolution| match image_resolution {
TextToImageResolution::OneK => NanoBananaProMultiFunctionImageGenImageResolution::OneK,
TextToImageResolution::TwoK => NanoBananaProMultiFunctionImageGenImageResolution::TwoK,
TextToImageResolution::FourK => NanoBananaProMultiFunctionImageGenImageResolution::FourK,
});
let request = NanoBananaProMultiFunctionImageGenRequest {
uuid_idempotency_token,
prompt: request.prompt.clone(),
image_media_tokens: request.image_media_tokens.clone(),
image_media_tokens: None,
num_images,
resolution,
aspect_ratio,
@@ -122,20 +126,3 @@ fn get_aspect_ratio(request: &EnqueueTextToImageRequest) -> Option<NanoBananaPro
None
}
fn get_resolution(request: &EnqueueTextToImageRequest) -> Option<NanoBananaProMultiFunctionImageGenImageResolution> {
if let Some(common_resolution) = request.common_resolution {
return Some(resolution_to_artcraft_nano_banana_pro(common_resolution));
}
if let Some(resolution) = request.image_resolution {
// Deprecated resolution
return Some(match resolution {
TextToImageResolution::OneK => NanoBananaProMultiFunctionImageGenImageResolution::OneK,
TextToImageResolution::TwoK => NanoBananaProMultiFunctionImageGenImageResolution::TwoK,
TextToImageResolution::FourK => NanoBananaProMultiFunctionImageGenImageResolution::FourK,
})
}
None
}
@@ -1,5 +1,3 @@
use crate::core::api_adapters::aspect_ratio::common_aspect_ratio::CommonAspectRatio;
use crate::core::api_adapters::resolution::common_resolution::CommonResolution;
use crate::core::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors;
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason};
use crate::core::commands::enqueue::image_edit::image_edit_models::image_edit_model_to_model_type;
@@ -18,8 +16,6 @@ use crate::core::events::generation_events::common::{GenerationAction, Generatio
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::core::events::sendable_event_trait::SendableEvent;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_type::{ArtcraftUsagePage, ArtcraftUsageType};
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
use crate::core::state::task_database::TaskDatabase;
@@ -38,6 +34,7 @@ use serde::{Deserialize, Serialize};
use sqlite_tasks::queries::create_task::{create_task, CreateTaskArgs};
use tauri::{AppHandle, State};
use tokens::tokens::media_files::MediaFileToken;
use crate::core::api_adapters::aspect_ratio::common_aspect_ratio::CommonAspectRatio;
/// This is used in the Tauri command bridge.
/// Don't change the serializations without coordinating with the frontend.
@@ -89,20 +86,15 @@ pub struct EnqueueTextToImageRequest {
/// Text prompt for the image generation. Required.
pub prompt: Option<String>,
/// Aspect ratio.
#[deprecated(note="use common_aspect_ratio")]
pub aspect_ratio: Option<TextToImageSize>,
/// New field for aspect ratio.
/// Not all models support each of these aspect ratios, but we can choose or interpolate sensibly.
pub common_aspect_ratio: Option<CommonAspectRatio>,
/// New field for resolution.
/// Not all models support each of these resolutions, but we can choose or interpolate sensibly.
pub common_resolution: Option<CommonResolution>,
/// Aspect ratio.
#[deprecated(note="use common_aspect_ratio")]
pub aspect_ratio: Option<TextToImageSize>,
/// Aspect ratio.
#[deprecated(note="use common_resolution")]
pub image_resolution: Option<TextToImageResolution>,
/// The number of images to generate.
@@ -187,7 +179,6 @@ pub async fn enqueue_text_to_image_command(
app: AppHandle,
app_data_root: State<'_, AppDataRoot>,
app_env_configs: State<'_, AppEnvConfigs>,
artcraft_usage_tracker: State<'_, ArtcraftUsageTracker>,
provider_priority_store: State<'_, ProviderPriorityStore>,
task_database: State<'_, TaskDatabase>,
mj_creds_manager: State<'_, MidjourneyCredentialManager>,
@@ -206,7 +197,6 @@ pub async fn enqueue_text_to_image_command(
request,
&app,
&app_data_root,
&artcraft_usage_tracker,
&provider_priority_store,
&task_database,
&mj_creds_manager,
@@ -277,7 +267,6 @@ pub async fn handle_request(
request: EnqueueTextToImageRequest,
app: &AppHandle,
app_data_root: &AppDataRoot,
artcraft_usage_tracker: &ArtcraftUsageTracker,
provider_priority_store: &ProviderPriorityStore,
task_database: &TaskDatabase,
mj_creds_manager: &MidjourneyCredentialManager,
@@ -321,24 +310,6 @@ pub async fn handle_request(
error!("Failed to create task in database: {:?}", err);
// NB: Fail open, but find a way to flag this.
}
let num_images = request.number_images.map(|n| n as u16).unwrap_or(1);
let is_image_to_image = request.image_media_tokens
.as_ref()
.map(|tokens| !tokens.is_empty())
.unwrap_or(false);
let usage_type = if is_image_to_image {
ArtcraftUsageType::ImageToResult
} else {
ArtcraftUsageType::TextToResult
};
if let Err(err) = artcraft_usage_tracker.record_image_generation(num_images, usage_type, ArtcraftUsagePage::ImagePage) {
// NB: Fail open.
warn!("Failed to report usage: {:?}", err);
}
Ok(success_event)
}
@@ -8,7 +8,6 @@ use crate::core::lifecycle::startup::tasks::spawn_sora_task_polling_thread::spaw
use crate::core::lifecycle::startup::tasks::spawn_storyteller_threads::spawn_storyteller_threads;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::artcraft_platform_info::ArtcraftPlatformInfo;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::services::grok::state::grok_credential_manager::GrokCredentialManager;
use crate::services::grok::state::grok_image_prompt_queue::GrokImagePromptQueue;
@@ -30,7 +29,6 @@ pub async fn handle_tauri_startup(
root: AppDataRoot,
app_env_configs: AppEnvConfigs,
artcraft_platform_info: ArtcraftPlatformInfo,
artcraft_usage_tracker: ArtcraftUsageTracker,
storyteller_creds_manager: StorytellerCredentialManager,
sora_credential_manager: SoraCredentialManager,
sora_task_queue: SoraTaskQueue,
@@ -63,7 +61,6 @@ pub async fn handle_tauri_startup(
spawn_storyteller_threads(
&app,
&app_env_configs,
&artcraft_usage_tracker,
&artcraft_platform_info,
&task_database,
&storyteller_creds_manager,
@@ -1,6 +1,5 @@
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::artcraft_platform_info::ArtcraftPlatformInfo;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
use crate::core::state::task_database::TaskDatabase;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use crate::services::storyteller::threads::storyteller_activity_thread::storyteller_activity_thread;
@@ -11,7 +10,6 @@ use tauri::{AppHandle, Manager};
pub fn spawn_storyteller_threads(
app: &AppHandle,
app_env_configs: &AppEnvConfigs,
artcraft_usage_tracker: &ArtcraftUsageTracker,
artcraft_platform_info: &ArtcraftPlatformInfo,
task_database: &TaskDatabase,
storyteller_creds_manager: &StorytellerCredentialManager,
@@ -27,7 +25,6 @@ pub fn spawn_storyteller_threads(
tauri::async_runtime::spawn(storyteller_activity_thread(
app_env_configs.clone(),
artcraft_platform_info.clone(),
artcraft_usage_tracker.clone(),
storyteller_creds_manager.clone(),
));
@@ -10,9 +10,6 @@ pub struct AppPreferences {
/// Play sounds on events.
pub play_sounds: bool,
/// Key pointing to file; defined in the frontend code.
pub delete_file_sound: Option<String>,
/// Key pointing to file; defined in the frontend code.
/// Defined for enqueue since image enqueue can be async
pub enqueue_success_sound: Option<String>,
@@ -43,7 +40,6 @@ impl Default for AppPreferences {
generation_success_sound: Some("special_flower".to_string()),
generation_failure_sound: Some("crumble".to_string()),
generation_enqueue_sound: Some("done".to_string()),
delete_file_sound: Some("trash".to_string()),
}
}
}
@@ -5,9 +5,7 @@ use errors::AnyhowResult;
use serde_derive::{Deserialize, Serialize};
/// Vector clock versioning string rather than semver.
/// - Version 1 - initial version.
/// - Version 2 - added "delete_file_sound", marked optionals "skip_serializing_if"
const CURRENT_VERSION: &str = "2";
const CURRENT_VERSION: &str = "1";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppPreferencesSerializable {
@@ -18,23 +16,15 @@ pub struct AppPreferencesSerializable {
pub preferred_download_directory: Option<PreferredDownloadDirectory>,
/// Play sounds on events.
#[serde(skip_serializing_if = "Option::is_none")]
pub play_sounds: Option<bool>,
/// Key pointing to file; defined in the frontend code.
#[serde(skip_serializing_if = "Option::is_none")]
pub delete_file_sound: Option<String>,
/// Key pointing to file; defined in the frontend code.
#[serde(skip_serializing_if = "Option::is_none")]
pub generation_success_sound: Option<String>,
/// Key pointing to file; defined in the frontend code.
#[serde(skip_serializing_if = "Option::is_none")]
pub generation_failure_sound: Option<String>,
/// Key pointing to file; defined in the frontend code.
#[serde(skip_serializing_if = "Option::is_none")]
pub generation_enqueue_sound: Option<String>,
}
@@ -55,7 +45,6 @@ impl AppPreferencesSerializable {
version: CURRENT_VERSION.to_string(),
preferred_download_directory: Some(preferences.preferred_download_directory.clone()),
play_sounds: Some(preferences.play_sounds),
delete_file_sound: preferences.delete_file_sound.clone(),
generation_success_sound: preferences.generation_success_sound.clone(),
generation_failure_sound: preferences.generation_failure_sound.clone(),
generation_enqueue_sound: preferences.generation_enqueue_sound.clone(),
@@ -72,12 +61,11 @@ impl AppPreferencesSerializable {
if let Some(play_sounds) = self.play_sounds {
preferences.play_sounds = play_sounds;
}
preferences.delete_file_sound = self.delete_file_sound.clone();
preferences.generation_success_sound = self.generation_success_sound.clone();
preferences.generation_failure_sound = self.generation_failure_sound.clone();
preferences.generation_enqueue_sound = self.generation_enqueue_sound.clone();
preferences
}
}
@@ -1,7 +1,9 @@
use crate::core::state::os_platform::OsPlatform;
use crate::version::ARTCRAFT_VERSION;
use chrono::{DateTime, Utc};
// TODO: Centrally configure version.
const ARTCRAFT_VERSION: &str = "0.3.0";
#[derive(Clone, Debug)]
pub struct ArtcraftPlatformInfo {
pub artcraft_version: String,
@@ -1,135 +0,0 @@
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker_data::ArtcraftUsageTrackerData;
use std::sync::{Arc, LockResult, RwLock};
use log::{error, warn};
use crate::core::artcraft_error::ArtcraftError;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_type::{ArtcraftUsagePage, ArtcraftUsageType};
/// Track generation volume so we know which areas to focus efforts on.
/// But also note: https://en.wikipedia.org/wiki/Survivorship_bias
#[derive(Clone)]
pub struct ArtcraftUsageTracker {
data: Arc<RwLock<ArtcraftUsageTrackerData>>,
}
impl ArtcraftUsageTracker {
pub fn new() -> Self {
Self {
data: Arc::new(RwLock::new(ArtcraftUsageTrackerData::new())),
}
}
pub fn record_image_generation(&self, num_images: u16, usage_type: ArtcraftUsageType, page: ArtcraftUsagePage) -> Result<(), ArtcraftError> {
let mut data = self.get()?;
data.total_generation_count += num_images;
data.image_generation_count += num_images;
match usage_type {
ArtcraftUsageType::TextToResult => {
data.text_to_image_count += num_images;
}
ArtcraftUsageType::ImageToResult => {
data.image_to_image_count += num_images;
}
ArtcraftUsageType::Other => {}
}
match page {
ArtcraftUsagePage::ImagePage => {
data.image_page_prompt_count += num_images;
}
ArtcraftUsagePage::EditPage => {
data.edit_page_prompt_count += num_images;
}
ArtcraftUsagePage::StagePage => {
data.stage_page_prompt_count += num_images;
}
ArtcraftUsagePage::OtherPage => {
data.other_page_prompt_count += num_images;
}
ArtcraftUsagePage::VideoPage => {} // NB: Does not generate images.
ArtcraftUsagePage::ObjectPage => {} // NB: Does not generate images.
}
self.set(data)?;
Ok(())
}
pub fn record_video_generation(&self, num_videos: u16, usage_type: ArtcraftUsageType, page: ArtcraftUsagePage) -> Result<(), ArtcraftError> {
let mut data = self.get()?;
data.total_generation_count += num_videos;
data.video_generation_count += num_videos;
match usage_type {
ArtcraftUsageType::TextToResult => {
data.text_to_video_count += num_videos;
}
ArtcraftUsageType::ImageToResult => {
data.image_to_video_count += num_videos;
}
ArtcraftUsageType::Other => {}
}
match page {
ArtcraftUsagePage::VideoPage => {
data.video_page_prompt_count += num_videos;
}
ArtcraftUsagePage::OtherPage => {
data.other_page_prompt_count += num_videos;
}
ArtcraftUsagePage::ImagePage => {} // NB: Does not generate videos.
ArtcraftUsagePage::EditPage => {} // NB: Does not generate videos.
ArtcraftUsagePage::StagePage => {} // NB: Does not generate videos.
ArtcraftUsagePage::ObjectPage => {} // NB: Does not generate videos.
}
self.set(data)?;
Ok(())
}
pub fn record_object_generation(&self, num_objects: u16, usage_type: ArtcraftUsageType, page: ArtcraftUsagePage) -> Result<(), ArtcraftError> {
let mut data = self.get()?;
data.total_generation_count += num_objects;
data.object_generation_count += num_objects;
match usage_type {
ArtcraftUsageType::TextToResult => {
data.text_to_object_count += num_objects;
}
ArtcraftUsageType::ImageToResult => {
data.image_to_object_count += num_objects;
}
ArtcraftUsageType::Other => {}
}
match page {
ArtcraftUsagePage::ObjectPage => {
data.object_page_prompt_count += num_objects;
}
ArtcraftUsagePage::OtherPage => {
data.other_page_prompt_count += num_objects;
}
ArtcraftUsagePage::ImagePage => {} // NB: Does not generate objects.
ArtcraftUsagePage::VideoPage => {} // NB: Does not generate objects.
ArtcraftUsagePage::EditPage => {} // NB: Does not generate objects.
ArtcraftUsagePage::StagePage => {} // NB: Does not generate objects.
}
self.set(data)?;
Ok(())
}
pub fn get(&self) -> Result<ArtcraftUsageTrackerData, ArtcraftError> {
match self.data.read() {
Ok(data) => Ok(data.clone()),
Err(err) => {
error!("Lock read error: {:?}", err);
Err(ArtcraftError::RwLockReadError)
}
}
}
fn set(&self, data: ArtcraftUsageTrackerData) -> Result<(), ArtcraftError> {
match self.data.write() {
Ok(mut lock) => {
*lock = data;
Ok(())
},
Err(err) => {
error!("Lock write error: {:?}", err);
Err(ArtcraftError::RwLockWriteError)
}
}
}
}
@@ -1,46 +0,0 @@
/// Track generation volume so we know which areas to focus efforts on.
/// But also note: https://en.wikipedia.org/wiki/Survivorship_bias
#[derive(Clone)]
pub struct ArtcraftUsageTrackerData {
pub total_generation_count: u16,
pub image_generation_count: u16,
pub video_generation_count: u16,
pub object_generation_count: u16,
pub text_to_image_count: u16,
pub image_to_image_count: u16,
pub text_to_video_count: u16,
pub image_to_video_count: u16,
pub text_to_object_count: u16,
pub image_to_object_count: u16,
pub image_page_prompt_count: u16,
pub video_page_prompt_count: u16,
pub edit_page_prompt_count: u16,
pub stage_page_prompt_count: u16,
pub object_page_prompt_count: u16,
pub other_page_prompt_count: u16,
}
impl ArtcraftUsageTrackerData {
pub fn new() -> Self {
Self {
total_generation_count: 0,
image_generation_count: 0,
video_generation_count: 0,
object_generation_count: 0,
text_to_image_count: 0,
image_to_image_count: 0,
text_to_video_count: 0,
image_to_video_count: 0,
text_to_object_count: 0,
image_to_object_count: 0,
image_page_prompt_count: 0,
video_page_prompt_count: 0,
edit_page_prompt_count: 0,
stage_page_prompt_count: 0,
object_page_prompt_count: 0,
other_page_prompt_count: 0,
}
}
}
@@ -1,18 +0,0 @@
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ArtcraftUsageType {
TextToResult,
ImageToResult,
Other,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ArtcraftUsagePage {
ImagePage,
VideoPage,
EditPage,
StagePage,
ObjectPage,
OtherPage,
}
@@ -1,3 +0,0 @@
pub mod artcraft_usage_tracker;
pub mod artcraft_usage_type;
mod artcraft_usage_tracker_data;
@@ -2,7 +2,6 @@ pub mod app_env_configs;
pub mod app_preferences;
pub mod app_startup_time;
pub mod artcraft_platform_info;
pub mod artcraft_usage_tracker;
pub mod data_dir;
pub mod expanduser;
pub mod os_platform;
-7
View File
@@ -1,6 +1,5 @@
pub mod core;
pub mod services;
pub mod version;
use tauri::Manager;
@@ -73,7 +72,6 @@ use tauri_plugin_dialog;
use tauri_plugin_http;
use tauri_plugin_log::Target;
use tauri_plugin_log::TargetKind;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
@@ -126,9 +124,6 @@ pub fn run() {
let worldlabs_bearer_bridge = WorldlabsBearerBridge::empty();
let worldlabs_bearer_bridge_2 = worldlabs_bearer_bridge.clone();
let artcraft_usage_tracker = ArtcraftUsageTracker::new();
let artcraft_usage_tracker_2 = artcraft_usage_tracker.clone();
println!("Initializing backend runtime...");
@@ -164,7 +159,6 @@ pub fn run() {
root,
env_config,
artcraft_platform_info_2,
artcraft_usage_tracker_2,
storyteller_creds,
sora_creds,
sora_tasks,
@@ -187,7 +181,6 @@ pub fn run() {
.manage(app_env_configs)
.manage(app_preferences)
.manage(artcraft_platform_info)
.manage(artcraft_usage_tracker)
.manage(grok_creds_manager)
.manage(grok_prompt_queue)
.manage(midjourney_creds_manager)
@@ -1,7 +1,6 @@
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::artcraft_platform_info::ArtcraftPlatformInfo;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use crate::core::state::os_platform::OsPlatform;
@@ -23,7 +22,6 @@ use sqlite_tasks::queries::update_task_status::{update_task_status, UpdateTaskAr
use std::time::Instant;
use storyteller_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
use storyteller_client::endpoints::analytics::log_active_user::log_active_user;
use storyteller_client::endpoints::analytics::log_active_user_v2::log_active_user_v2;
use storyteller_client::endpoints::jobs::list_session_jobs::{list_session_jobs, States};
use storyteller_client::endpoints::media_files::upload_image_media_file_from_file::upload_image_media_file_from_file;
use storyteller_client::error::api_error::ApiError;
@@ -31,14 +29,15 @@ use storyteller_client::error::storyteller_error::StorytellerError;
use tauri::AppHandle;
use tokens::tokens::app_session::AppSessionToken;
// TODO: Configure this with the build and increment.
const CLIENT_NAME : &str = "artcraft";
const CLIENT_VERSION : &str = "0.3.0";
const ERROR_SLEEP_MILLIS : u64 = 1_000 * 60 * 3; // 3 minutes;
pub async fn storyteller_activity_thread(
app_env_configs: AppEnvConfigs,
artcraft_platform_info: ArtcraftPlatformInfo,
artcraft_usage_tracker: ArtcraftUsageTracker,
storyteller_creds_manager: StorytellerCredentialManager,
) -> ! {
let startup = Instant::now();
@@ -49,7 +48,6 @@ pub async fn storyteller_activity_thread(
loop {
let res = polling_loop(
&app_env_configs,
&artcraft_usage_tracker,
&storyteller_creds_manager,
startup,
&artcraft_platform_info,
@@ -65,7 +63,6 @@ pub async fn storyteller_activity_thread(
async fn polling_loop(
app_env_configs: &AppEnvConfigs,
artcraft_usage_tracker: &ArtcraftUsageTracker,
storyteller_creds_manager: &StorytellerCredentialManager,
startup: Instant,
artcraft_platform_info: &ArtcraftPlatformInfo,
@@ -88,8 +85,6 @@ async fn polling_loop(
},
};
let usage_data = artcraft_usage_tracker.get()?;
let time_since_startup = Instant::now().duration_since(startup);
let request = LogAppActiveUserRequest {
@@ -99,27 +94,11 @@ async fn polling_loop(
maybe_os_platform: Some(artcraft_platform_info.os_platform.as_str().to_owned()),
maybe_os_version: Some(artcraft_platform_info.os_version.clone()),
maybe_session_duration_seconds: Some(time_since_startup.as_secs()),
total_generation_count: Some(usage_data.total_generation_count),
image_generation_count: Some(usage_data.image_generation_count),
video_generation_count: Some(usage_data.video_generation_count),
object_generation_count: Some(usage_data.object_generation_count),
text_to_image_count: Some(usage_data.text_to_image_count),
image_to_image_count: Some(usage_data.image_to_image_count),
text_to_video_count: Some(usage_data.text_to_video_count),
image_to_video_count: Some(usage_data.image_to_video_count),
text_to_object_count: Some(usage_data.text_to_object_count),
image_to_object_count: Some(usage_data.image_to_object_count),
image_page_prompt_count: Some(usage_data.image_page_prompt_count),
video_page_prompt_count: Some(usage_data.video_page_prompt_count),
edit_page_prompt_count: Some(usage_data.edit_page_prompt_count),
stage_page_prompt_count: Some(usage_data.stage_page_prompt_count),
object_page_prompt_count: Some(usage_data.object_page_prompt_count),
other_page_prompt_count: Some(usage_data.other_page_prompt_count),
};
debug!("Logging active user with storyteller.");
let result = log_active_user_v2(
let result = log_active_user(
&app_env_configs.storyteller_host,
Some(&creds),
request,
-4
View File
@@ -1,4 +0,0 @@
/// The version of the ArtCraft desktop app reported in the UI and via Analytics.
/// This should match the CI builds.
pub const ARTCRAFT_VERSION: &str = "0.6.0";
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2.0.0-rc",
"productName": "ArtCraft",
"version": "0.6.0",
"version": "0.3.0",
"identifier": "ai.artcraft.app",
"build": {
"frontendDist": "../../../frontend/apps/artcraft/dist",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2.0.0-rc",
"productName": "ArtCraft",
"version": "0.6.0",
"version": "0.3.0",
"identifier": "ai.artcraft.app",
"build": {
"frontendDist": "../../../frontend/apps/artcraft/dist",
-40
View File
@@ -1,40 +0,0 @@
# For things that are too application-logic specific to live in generic actix helpers.
[package]
name = "actix_artcraft"
edition = "2024"
version = "0.0.1"
authors = [
"Brandon Thomas <bt@brand.io>",
"Brandon Thomas <echelon@gmail.com>",
]
publish = false
[package.metadata.cargo-udeps.ignore]
normal = ["workspace-build-acceleration"]
[lib]
name = "actix_artcraft"
path = "src/lib.rs"
[dependencies]
# Workspace hack (faster compile times)
workspace-build-acceleration.workspace = true
# Internal
actix_helpers.workspace = true
cookies.workspace = true
enums.workspace = true
errors.workspace = true
http_headers.workspace = true
tokens.workspace = true
# External
actix-http.workspace = true
actix-service.workspace = true
actix-web.workspace = true
log.workspace = true
anyhow.workspace = true
[dev-dependencies]
# (None yet)
-21
View File
@@ -1,21 +0,0 @@
//! actix_artcraft - for actix helpers that need to be shared between crates,
//! but are too application-specific to be in general actix helper crates
//!
// Never allow these
#![forbid(private_bounds)]
#![forbid(private_interfaces)]
#![forbid(unused_must_use)] // NB: It's unsafe to not close/check some things
// Okay to toggle
#![forbid(unreachable_patterns)]
#![forbid(unused_imports)]
#![forbid(unused_mut)]
#![forbid(unused_variables)]
// Always allow
#![allow(dead_code)]
#![allow(non_snake_case)]
pub mod requests;
pub mod sessions;
@@ -1 +0,0 @@
pub mod get_request_signup_source_enum;
+1 -2
View File
@@ -29,8 +29,7 @@ log = { workspace = true }
#symphonia = { version = "0.5.2", features = ["aac", "alac", "flac", "isomp4", "mkv", "mp3", "ogg", "vorbis", "wav"] }
# Opus support still hasn't landed in Symphonia, so we have to use an older version (~0.5) off-master.
# Opus is the browser's default (only) recording codec: https://blog.addpipe.com/audio-constraints-getusermedia/
# symphonia = { git = "https://github.com/pdeljanov/Symphonia.git", rev = "e1a70095c4409478c1ded499fb8dd4afa540fb90", features = ["aac", "alac", "flac", "isomp4", "mkv", "mp3", "ogg", "opus", "vorbis", "wav"] }
symphonia = { path = "../../../vendor/Symphonia/symphonia", features = ["aac", "alac", "flac", "isomp4", "mkv", "mp3", "ogg", "opus", "vorbis", "wav"] }
symphonia = { git = "https://github.com/pdeljanov/Symphonia.git", rev = "e1a70095c4409478c1ded499fb8dd4afa540fb90", features = ["aac", "alac", "flac", "isomp4", "mkv", "mp3", "ogg", "opus", "vorbis", "wav"] }
[dev-dependencies]
# None yet
@@ -2,7 +2,7 @@ use std::collections::HashSet;
use once_cell::sync::Lazy;
const TERMS : &str = include_str!("../../../../../../includes/binary_includes/safety/dictionary_children_terms.txt");
const TERMS : &str = include_str!("../../../../../../includes/binary_includes/dictionary_children_terms.txt");
pub (crate) fn contains_children_keyword(prompt_tokens: &[String]) -> bool {
static TERM_DICTIONARY : Lazy<HashSet<String>> = Lazy::new(|| {
@@ -2,7 +2,7 @@ use std::collections::HashSet;
use once_cell::sync::Lazy;
const TERMS: &str = include_str!("../../../../../../includes/binary_includes/safety/dictionary_potential_minor_terms.txt");
const TERMS: &str = include_str!("../../../../../../includes/binary_includes/dictionary_potential_minor_terms.txt");
pub (crate) fn contains_potential_minor_keyword(prompt_tokens: &[String]) -> bool {
static TERM_DICTIONARY: Lazy<HashSet<String>> = Lazy::new(|| {

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