mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-06 06:14:52 -05:00
Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3dbc540fb2 | ||
|
|
f3943818d9 | ||
|
|
0a99b77a6a | ||
|
|
9cd6685f02 | ||
|
|
89e8346fa6 | ||
|
|
79a85e1b32 | ||
|
|
fe1235a93c | ||
|
|
5217e9ff07 | ||
|
|
bb375e405f | ||
|
|
0a44642c95 | ||
|
|
0b68e04b5f | ||
|
|
a0e6b5721b | ||
|
|
8ffa1de27f | ||
|
|
1f0eb35ff1 | ||
|
|
43ec1ae5d7 | ||
|
|
625ba41423 | ||
|
|
2a62bcc7f8 | ||
|
|
5c92adff8f | ||
|
|
1276df9f29 | ||
|
|
026d356d9c |
@@ -68,9 +68,6 @@ __pycache__/
|
||||
# We use diesel for DB migrations only. We don't care about its codgen.
|
||||
_ignored_schema.rs
|
||||
|
||||
# Diesel lock file
|
||||
.diesel_lock
|
||||
|
||||
# AI Chat Sidecar has its own schema managed by diesel, but we don't care for diesel codegen.
|
||||
crates/programs/aichatbot-sidecar/_ignored_schema.rs
|
||||
|
||||
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE wallets\nSET\n banked_credits = ?,\n version = version + 1\nWHERE token = ?\nLIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "01f6369e0ab7b0fa5b95b4595827dcf15897f3b50cce5b1dc421101b157b7008"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO generic_inference_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n job_type = ?,\n\n maybe_external_third_party = ?,\n maybe_external_third_party_id = ?,\n\n product_category = ?,\n inference_category = ?,\n\n maybe_model_type = NULL,\n maybe_model_token = NULL,\n\n maybe_input_source_token = NULL,\n maybe_input_source_token_type = NULL,\n\n maybe_download_url = NULL,\n maybe_cover_image_media_file_token = NULL,\n\n maybe_prompt_token = ?,\n\n maybe_raw_inference_text = NULL,\n\n maybe_inference_args = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n priority_level = 0,\n is_keepalive_required = FALSE,\n max_duration_seconds = 0,\n\n is_debug_request = FALSE,\n maybe_routing_tag = NULL,\n\n status = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 14
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "33aa8a1cf85bf1b8b68f4e2ab46541f25d7e8bb5eeb503c5ec14cb5e115f69ce"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE wallet_ledger_entries\nSET\n is_refunded = TRUE,\n maybe_linked_refund_ledger_token = ?\nWHERE token = ?\nLIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "6701a7adad65bc351e4600dad3ebf0f5dbdefa7dc9a0f890790cfaf368bf572d"
|
||||
}
|
||||
-102
@@ -1,102 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n wallet_token as `wallet_token: tokens::tokens::wallets::WalletToken`,\n entry_type,\n is_refunded as `is_refunded: bool`,\n credits_delta,\n banked_credits_before,\n banked_credits_after,\n monthly_credits_before,\n monthly_credits_after\nFROM wallet_ledger_entries\nWHERE token = ?\nLIMIT 1\nFOR UPDATE\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "wallet_token: tokens::tokens::wallets::WalletToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "entry_type",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "is_refunded: bool",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "credits_delta",
|
||||
"type_info": {
|
||||
"type": "Long",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 11
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "banked_credits_before",
|
||||
"type_info": {
|
||||
"type": "Long",
|
||||
"flags": "NOT_NULL | UNSIGNED",
|
||||
"char_set": 63,
|
||||
"max_size": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "banked_credits_after",
|
||||
"type_info": {
|
||||
"type": "Long",
|
||||
"flags": "NOT_NULL | UNSIGNED",
|
||||
"char_set": 63,
|
||||
"max_size": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "monthly_credits_before",
|
||||
"type_info": {
|
||||
"type": "Long",
|
||||
"flags": "NOT_NULL | UNSIGNED",
|
||||
"char_set": 63,
|
||||
"max_size": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "monthly_credits_after",
|
||||
"type_info": {
|
||||
"type": "Long",
|
||||
"flags": "NOT_NULL | UNSIGNED",
|
||||
"char_set": 63,
|
||||
"max_size": 10
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "a7365998006f4d40ad26efb42e6958a7aed689562a4c5b8c91cbe0033d0270e7"
|
||||
}
|
||||
+2
-13
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n jobs.maybe_external_third_party_id as `order_id`,\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\n AND jobs.status NOT IN ('complete_success', 'complete_failure')\n AND jobs.maybe_external_third_party_id IS NOT NULL\n\nLIMIT 25000\n ",
|
||||
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n jobs.maybe_external_third_party_id as `order_id`,\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\n AND jobs.status NOT IN ('complete_success', 'complete_failure')\n AND jobs.maybe_external_third_party_id IS NOT NULL\n\nLIMIT 25000\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -72,16 +72,6 @@
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -94,9 +84,8 @@
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "cb33fd483f0fef7ec2a1f5950218f312a19e38c7971b88db949ccaa05bf7b7c4"
|
||||
"hash": "ca96942d8214edb84f163cf40131ceed873209e86718054e1502219fc81d2dc6"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO generic_inference_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n job_type = ?,\n\n maybe_external_third_party = ?,\n maybe_external_third_party_id = ?,\n\n product_category = ?,\n inference_category = ?,\n\n maybe_model_type = NULL,\n maybe_model_token = NULL,\n\n maybe_input_source_token = NULL,\n maybe_input_source_token_type = NULL,\n\n maybe_download_url = NULL,\n maybe_cover_image_media_file_token = NULL,\n\n maybe_prompt_token = ?,\n\n maybe_wallet_ledger_entry_token = ?,\n\n maybe_raw_inference_text = NULL,\n\n maybe_inference_args = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n priority_level = 0,\n is_keepalive_required = FALSE,\n max_duration_seconds = 0,\n\n is_debug_request = FALSE,\n maybe_routing_tag = NULL,\n\n status = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 15
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "ee228ab4a079fde6b3683b6cde2a2384d3def27a5d09baa5caf00a521d5d2bcd"
|
||||
}
|
||||
Generated
+2
-22
@@ -677,11 +677,10 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||
|
||||
[[package]]
|
||||
name = "artcraft"
|
||||
version = "0.8.0"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"artcraft_api_defs",
|
||||
"artcraft_router",
|
||||
"base64 0.22.1",
|
||||
"build_metadata",
|
||||
"bytes",
|
||||
@@ -738,7 +737,6 @@ dependencies = [
|
||||
name = "artcraft_api_defs"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"enums",
|
||||
"enums_public",
|
||||
@@ -751,23 +749,6 @@ dependencies = [
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "artcraft_router"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"artcraft_api_defs",
|
||||
"log",
|
||||
"seedance2pro",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"storyteller_client",
|
||||
"tokens",
|
||||
"tokio",
|
||||
"uuid 1.18.1",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ashpd"
|
||||
version = "0.9.2"
|
||||
@@ -11478,7 +11459,6 @@ dependencies = [
|
||||
"actix_helpers",
|
||||
"anyhow",
|
||||
"artcraft_api_defs",
|
||||
"artcraft_router",
|
||||
"async-stripe 0.26.0",
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
@@ -11540,6 +11520,7 @@ dependencies = [
|
||||
"redis_common",
|
||||
"redis_schema",
|
||||
"regex",
|
||||
"reqwest 0.12.23",
|
||||
"resend-rs",
|
||||
"reusable_types",
|
||||
"rust-s3",
|
||||
@@ -11571,7 +11552,6 @@ dependencies = [
|
||||
"utoipa-swagger-ui",
|
||||
"videos",
|
||||
"workspace-build-acceleration",
|
||||
"wreq",
|
||||
"zip 0.6.6",
|
||||
]
|
||||
|
||||
|
||||
+2
-4
@@ -72,7 +72,6 @@ members = [
|
||||
"crates/lib/files/videos",
|
||||
"crates/lib/http_headers",
|
||||
"crates/lib/idempotency",
|
||||
"crates/lib/artcraft_router",
|
||||
"crates/lib/image_algorithms",
|
||||
"crates/lib/jobs_common",
|
||||
"crates/lib/jwt_light",
|
||||
@@ -208,7 +207,6 @@ workspace-build-acceleration = { path = "crates/workspace-build-acceleration" }
|
||||
# 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" }
|
||||
artcraft_router = { path = "crates/lib/artcraft_router" }
|
||||
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" }
|
||||
@@ -302,7 +300,7 @@ toml = "0.8.23"
|
||||
url = { version = "2.5.4", features = ["serde"] }
|
||||
uuid = "1.18.1"
|
||||
|
||||
# NB: wreq gets past fingerprinting, but we have to deal with platform-specific build issues.
|
||||
# NB(1): wreq gets past fingerprinting, but we have to deal with platform-specific build issues.
|
||||
#
|
||||
# Mac:
|
||||
# brew install --formula cmake pkg-config llvm
|
||||
@@ -313,5 +311,5 @@ uuid = "1.18.1"
|
||||
# choco install llvm -y
|
||||
# choco install nasm -y
|
||||
wreq-util = "3.0.0-rc.10"
|
||||
wreq = { version = "6.0.0-rc.28", features = ["json", "query", "form", "multipart", "cookies", "ws", "stream", "gzip", "brotli", "zstd", "deflate"] }
|
||||
wreq = { version = "6.0.0-rc.28", features = ["json", "query", "multipart", "cookies", "ws", "stream", "gzip", "form"] }
|
||||
|
||||
|
||||
@@ -55,7 +55,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, Seedance 2 / 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 2 / 3 |
|
||||
| Grok | Grok Imagine, Grok Video |
|
||||
| Midjourney | Image Gen (all versions) |
|
||||
| Sora | Sora 1, Sora 2, GPT-Image-1 |
|
||||
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE generic_inference_jobs
|
||||
DROP COLUMN
|
||||
maybe_wallet_ledger_entry_token;
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE generic_inference_jobs
|
||||
ADD COLUMN
|
||||
maybe_wallet_ledger_entry_token VARCHAR(32) DEFAULT NULL;
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE wallet_ledger_entries
|
||||
DROP COLUMN
|
||||
is_refunded;
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE wallet_ledger_entries
|
||||
ADD COLUMN
|
||||
is_refunded BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE wallet_ledger_entries
|
||||
DROP COLUMN
|
||||
maybe_linked_refund_ledger_token;
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE wallet_ledger_entries
|
||||
ADD COLUMN
|
||||
maybe_linked_refund_ledger_token VARCHAR(32) DEFAULT NULL;
|
||||
@@ -182,9 +182,6 @@ CREATE TABLE generic_inference_jobs (
|
||||
is_from_api_user BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
is_for_twitch BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
|
||||
-- If credits were spent for this job, this references the corresponding wallet ledger entry.
|
||||
maybe_wallet_ledger_entry_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- ========== DEVELOPMENT AND DEBUGGING METADATA ==========
|
||||
|
||||
-- If true, the request gets a "debug" flag, which may do different
|
||||
|
||||
@@ -40,12 +40,6 @@ CREATE TABLE wallet_ledger_entries (
|
||||
-- Balance of monthly credits after the event.
|
||||
monthly_credits_after INTEGER UNSIGNED NOT NULL DEFAULT 0,
|
||||
|
||||
-- Whether this ledger entry has been refunded.
|
||||
is_refunded BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
|
||||
-- If this entry was refunded, this points to the refund ledger entry.
|
||||
maybe_linked_refund_ledger_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- ========== RECORD TIMESTAMPS ==========
|
||||
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
@@ -202,7 +202,6 @@ RUN echo -n ${GIT_SHA} > GIT_SHA
|
||||
|
||||
# Copy all the binaries (except those that need a GPU):
|
||||
COPY --from=builder /tmp/target/release/storyteller-web /
|
||||
COPY --from=builder /tmp/target/release/seedance2-pro-job /
|
||||
COPY --from=builder /tmp/target/release/dummy-service /
|
||||
COPY --from=builder /tmp/target/release/analytics-job /
|
||||
COPY --from=builder /tmp/target/release/email-sender-job /
|
||||
|
||||
@@ -36,4 +36,3 @@ serde_json.workspace = true
|
||||
utoipa = { version = "4", features = ["chrono", "actix_extras"] }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow.workspace = true
|
||||
-122
@@ -1,122 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
use enums::common::generation::common_video_resolution::CommonVideoResolution;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
|
||||
pub const ESTIMATE_VIDEO_COST_PATH: &str = "/v1/generate/cost_estimate/video";
|
||||
|
||||
/// Request body for the video cost estimate endpoint.
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct EstimateVideoCostRequest {
|
||||
/// The video model to estimate costs for.
|
||||
pub model: CommonVideoModel,
|
||||
|
||||
/// The provider to route the generation through.
|
||||
pub provider: GenerationProvider,
|
||||
|
||||
/// The type of generation (determines how many input images are involved).
|
||||
/// This is a tagged enum, so it looks like:
|
||||
/// "generation_mode": {"type": "text_to_video"}
|
||||
/// "generation_mode": {"type": "reference_image_to_video", "count": 1}
|
||||
pub generation_mode: GenerationMode,
|
||||
|
||||
/// Optional aspect ratio.
|
||||
pub aspect_ratio: Option<CommonAspectRatio>,
|
||||
|
||||
/// Optional resolution.
|
||||
pub resolution: Option<CommonVideoResolution>,
|
||||
|
||||
/// Duration in seconds.
|
||||
pub duration_seconds: Option<u16>,
|
||||
|
||||
/// Number of videos to generate in parallel.
|
||||
pub video_batch_count: Option<u16>,
|
||||
}
|
||||
|
||||
/// Describes the type of video generation being requested.
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum GenerationMode {
|
||||
TextToVideo,
|
||||
StartFrameToVideo,
|
||||
StartAndEndFrameToVideo,
|
||||
ReferenceImageToVideo { count: u32 },
|
||||
}
|
||||
|
||||
/// Response body for the video cost estimate endpoint.
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct EstimateVideoCostResponse {
|
||||
pub success: bool,
|
||||
|
||||
/// Estimated cost in credits.
|
||||
pub cost_in_credits: Option<u64>,
|
||||
|
||||
/// Estimated cost in USD cents.
|
||||
pub cost_in_usd_cents: Option<u64>,
|
||||
|
||||
/// Whether the generation is free for this user/plan.
|
||||
pub is_free: bool,
|
||||
|
||||
/// Whether the user has unlimited generations.
|
||||
pub is_unlimited: bool,
|
||||
|
||||
/// Whether the user is rate limited.
|
||||
pub is_rate_limited: bool,
|
||||
|
||||
/// Whether the output will have a watermark.
|
||||
pub has_watermark: bool,
|
||||
}
|
||||
|
||||
/// Error response for the video cost estimate endpoint.
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct EstimateVideoCostError {
|
||||
pub success: bool,
|
||||
pub error_type: EstimateVideoCostErrorType,
|
||||
pub error_message: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum EstimateVideoCostErrorType {
|
||||
InvalidProviderForModel,
|
||||
InvalidInput,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_serialization_tagging1() {
|
||||
let request = EstimateVideoCostRequest {
|
||||
model: CommonVideoModel::GrokVideo,
|
||||
provider: GenerationProvider::Artcraft,
|
||||
generation_mode: GenerationMode::TextToVideo,
|
||||
aspect_ratio: None,
|
||||
resolution: None,
|
||||
duration_seconds: None,
|
||||
video_batch_count: None,
|
||||
};
|
||||
let serialized = serde_json::to_string(&request).unwrap();
|
||||
assert!(serialized.contains("\"generation_mode\":{\"type\":\"text_to_video\"}"));
|
||||
//assert_eq!(serialized, "{}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialization_tagging2() {
|
||||
let request = EstimateVideoCostRequest {
|
||||
model: CommonVideoModel::GrokVideo,
|
||||
provider: GenerationProvider::Artcraft,
|
||||
generation_mode: GenerationMode::ReferenceImageToVideo { count: 1 },
|
||||
aspect_ratio: None,
|
||||
resolution: None,
|
||||
duration_seconds: None,
|
||||
video_batch_count: None,
|
||||
};
|
||||
let serialized = serde_json::to_string(&request).unwrap();
|
||||
assert!(serialized.contains("\"generation_mode\":{\"type\":\"reference_image_to_video\",\"count\":1}"));
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod estimate_video_cost;
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod cost_estimate;
|
||||
pub mod image;
|
||||
pub mod object;
|
||||
pub mod video;
|
||||
+2
-19
@@ -33,7 +33,7 @@ pub struct Seedance2p0MultiFunctionVideoGenRequest {
|
||||
pub batch_count: Option<Seedance2p0BatchCount>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Seedance2p0AspectRatio {
|
||||
/// 16:9 landscape (1280x720)
|
||||
@@ -48,7 +48,7 @@ pub enum Seedance2p0AspectRatio {
|
||||
Portrait3x4,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Seedance2p0BatchCount {
|
||||
One,
|
||||
@@ -66,20 +66,3 @@ pub struct Seedance2p0MultiFunctionVideoGenResponse {
|
||||
/// All inference job tokens for the batch (includes `inference_job_token` as the first entry).
|
||||
pub all_inference_job_tokens: Vec<InferenceJobToken>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn aspect_to_string(input: Seedance2p0AspectRatio) -> anyhow::Result<String> {
|
||||
let val = serde_json::to_string(&input)?;
|
||||
Ok(val.replace("\"", ""))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn just_testing_snake_case() -> anyhow::Result<()> {
|
||||
// Ugh, not sure what these are...
|
||||
assert_eq!(aspect_to_string(Seedance2p0AspectRatio::Landscape16x9)?, "landscape16x9");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod parse_multi_cookie_header;
|
||||
pub mod storyteller_avt_cookie;
|
||||
pub mod storyteller_credential_set;
|
||||
pub mod storyteller_session_cookie;
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
use crate::credentials::storyteller_avt_cookie::StorytellerAvtCookie;
|
||||
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use crate::credentials::storyteller_session_cookie::StorytellerSessionCookie;
|
||||
use cookie::Cookie;
|
||||
|
||||
/// Parses a multi-cookie header string (e.g. `"visitor=abc; session=xyz"`) and extracts
|
||||
/// the known Storyteller cookies by name.
|
||||
///
|
||||
/// Returns `Ok(Some(...))` if at least one recognised cookie is present,
|
||||
/// `Ok(None)` if the input is empty or contains no recognised cookies,
|
||||
/// and `Err` if any individual cookie segment fails to parse.
|
||||
pub fn parse_multi_cookie_header(header: &str) -> Result<Option<StorytellerCredentialSet>, cookie::ParseError> {
|
||||
let mut avt: Option<StorytellerAvtCookie> = None;
|
||||
let mut session: Option<StorytellerSessionCookie> = None;
|
||||
|
||||
for segment in header.split(';') {
|
||||
let segment = segment.trim();
|
||||
if segment.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let cookie = Cookie::parse(segment.to_owned())?;
|
||||
if avt.is_none() {
|
||||
avt = StorytellerAvtCookie::maybe_from_cookie(&cookie);
|
||||
}
|
||||
if session.is_none() {
|
||||
session = StorytellerSessionCookie::maybe_from_cookie(&cookie);
|
||||
}
|
||||
}
|
||||
|
||||
if avt.is_none() && session.is_none() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some(StorytellerCredentialSet::initialize(avt, session)))
|
||||
}
|
||||
-5
@@ -1,4 +1,3 @@
|
||||
use crate::credentials::parse_multi_cookie_header::parse_multi_cookie_header;
|
||||
use crate::credentials::storyteller_avt_cookie::StorytellerAvtCookie;
|
||||
use crate::credentials::storyteller_session_cookie::StorytellerSessionCookie;
|
||||
|
||||
@@ -26,10 +25,6 @@ impl StorytellerCredentialSet {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_multi_cookie_header(header: &str) -> Result<Option<Self>, cookie::ParseError> {
|
||||
parse_multi_cookie_header(header)
|
||||
}
|
||||
|
||||
pub fn initialize_with_just_cookie(session: StorytellerSessionCookie) -> Self {
|
||||
Self {
|
||||
avt: None,
|
||||
|
||||
+6
-6
@@ -18,9 +18,11 @@ use enums::no_table::style_transfer::style_transfer_name::StyleTransferName;
|
||||
use enums_public::by_table::model_weights::public_weights_types::PublicWeightsType;
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
use log::debug;
|
||||
use reqwest::multipart::{Form, Part};
|
||||
use reqwest::multipart::Form;
|
||||
use reqwest::Client;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
use tokens::tokens::batch_generations::BatchGenerationToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
@@ -71,13 +73,11 @@ pub async fn legacy_upload_media_file_from_file<P: AsRef<Path>>(
|
||||
.build()
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
|
||||
let file_bytes = std::fs::read(args.path.as_ref())
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
let file_name = args.path.as_ref().file_name()
|
||||
.and_then(|n| n.to_str()).unwrap_or("file").to_string();
|
||||
let mut form = Form::new()
|
||||
.text("uuid_idempotency_token", generate_random_uuid())
|
||||
.part("file", Part::bytes(file_bytes).file_name(file_name));
|
||||
.file("file", args.path)
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
|
||||
//if args.is_intermediate_system_file {
|
||||
// form = form.text("is_intermediate_system_file", "true");
|
||||
|
||||
+6
-6
@@ -18,9 +18,11 @@ use enums::no_table::style_transfer::style_transfer_name::StyleTransferName;
|
||||
use enums_public::by_table::model_weights::public_weights_types::PublicWeightsType;
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
use log::debug;
|
||||
use reqwest::multipart::{Form, Part};
|
||||
use reqwest::multipart::Form;
|
||||
use reqwest::Client;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
use tokens::tokens::batch_generations::BatchGenerationToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
@@ -71,13 +73,11 @@ pub async fn upload_image_media_file_from_file<P: AsRef<Path>>(
|
||||
.build()
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
|
||||
let file_bytes = std::fs::read(args.path.as_ref())
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
let file_name = args.path.as_ref().file_name()
|
||||
.and_then(|n| n.to_str()).unwrap_or("file").to_string();
|
||||
let mut form = Form::new()
|
||||
.text("uuid_idempotency_token", generate_random_uuid())
|
||||
.part("file", Part::bytes(file_bytes).file_name(file_name));
|
||||
.file("file", args.path)
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
|
||||
if args.is_intermediate_system_file {
|
||||
form = form.text("is_intermediate_system_file", "true");
|
||||
|
||||
+6
-6
@@ -15,9 +15,11 @@ use enums::no_table::style_transfer::style_transfer_name::StyleTransferName;
|
||||
use enums_public::by_table::model_weights::public_weights_types::PublicWeightsType;
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
use log::debug;
|
||||
use reqwest::multipart::{Form, Part};
|
||||
use reqwest::multipart::Form;
|
||||
use reqwest::Client;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
use tokens::tokens::batch_generations::BatchGenerationToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
@@ -42,13 +44,11 @@ pub async fn upload_new_engine_asset_from_file<P: AsRef<Path>>(
|
||||
.gzip(true)
|
||||
.build()?;
|
||||
|
||||
let file_bytes = std::fs::read(path.as_ref())?;
|
||||
let file_name = path.as_ref().file_name()
|
||||
.and_then(|n| n.to_str()).unwrap_or("file").to_string();
|
||||
let form = Form::new()
|
||||
.text("uuid_idempotency_token", generate_random_uuid())
|
||||
.text("engine_category", "object") // TODO: Which type should we use?
|
||||
.part("file", Part::bytes(file_bytes).file_name(file_name));
|
||||
.text("engine_category", "object") // TODO: Which type should we use?
|
||||
.file("file", path)
|
||||
.await?;
|
||||
|
||||
let mut request_builder = client.post(url)
|
||||
.header("User-Agent", USER_AGENT)
|
||||
|
||||
+5
-5
@@ -15,9 +15,11 @@ use enums::no_table::style_transfer::style_transfer_name::StyleTransferName;
|
||||
use enums_public::by_table::model_weights::public_weights_types::PublicWeightsType;
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
use log::debug;
|
||||
use reqwest::multipart::{Form, Part};
|
||||
use reqwest::multipart::Form;
|
||||
use reqwest::Client;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
use tokens::tokens::batch_generations::BatchGenerationToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
@@ -72,12 +74,10 @@ pub async fn upload_video_media_file_from_file<P: AsRef<Path>>(
|
||||
.gzip(true)
|
||||
.build()?;
|
||||
|
||||
let file_bytes = std::fs::read(args.path.as_ref())?;
|
||||
let file_name = args.path.as_ref().file_name()
|
||||
.and_then(|n| n.to_str()).unwrap_or("file").to_string();
|
||||
let mut form = Form::new()
|
||||
.text("uuid_idempotency_token", generate_random_uuid())
|
||||
.part("file", Part::bytes(file_bytes).file_name(file_name));
|
||||
.file("file", args.path)
|
||||
.await?;
|
||||
|
||||
if let Some(prompt_token) = &args.maybe_prompt_token {
|
||||
form = form.text("maybe_prompt_token", prompt_token.to_string());
|
||||
|
||||
@@ -48,16 +48,6 @@ pub async fn basic_json_delete_request<Req: Serialize, Res: DeserializeOwned>(
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
let hostname = response.headers().get("x-backend-hostname")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|s| s.to_owned());
|
||||
|
||||
let build_sha = response.headers().get("x-build-sha")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|s| s.to_owned());
|
||||
|
||||
debug!("[server] x-backend-hostname: {:?}, x-build-sha: {:?}", hostname, build_sha);
|
||||
|
||||
let response = filter_bad_response(response)
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::error::storyteller_error::StorytellerError;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
|
||||
use crate::utils::filter_bad_response::filter_bad_response;
|
||||
use log::{debug, info};
|
||||
use log::debug;
|
||||
use reqwest::Client;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -40,16 +40,6 @@ pub async fn basic_json_get_request<Res: DeserializeOwned>(
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
let hostname = response.headers().get("x-backend-hostname")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|s| s.to_owned());
|
||||
|
||||
let build_sha = response.headers().get("x-build-sha")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|s| s.to_owned());
|
||||
|
||||
debug!("[server] x-backend-hostname: {:?}, x-build-sha: {:?}", hostname, build_sha);
|
||||
|
||||
let response = filter_bad_response(response)
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
@@ -48,16 +48,6 @@ pub async fn basic_json_post_request<Req: Serialize, Res: DeserializeOwned>(
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
let hostname = response.headers().get("x-backend-hostname")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|s| s.to_owned());
|
||||
|
||||
let build_sha = response.headers().get("x-build-sha")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|s| s.to_owned());
|
||||
|
||||
debug!("[server] x-backend-hostname: {:?}, x-build-sha: {:?}", hostname, build_sha);
|
||||
|
||||
let response = filter_bad_response(response)
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
-10
@@ -46,16 +46,6 @@ pub async fn basic_query_string_post_request<Res: DeserializeOwned>(
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
let hostname = response.headers().get("x-backend-hostname")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|s| s.to_owned());
|
||||
|
||||
let build_sha = response.headers().get("x-build-sha")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|s| s.to_owned());
|
||||
|
||||
debug!("[server] x-backend-hostname: {:?}, x-build-sha: {:?}", hostname, build_sha);
|
||||
|
||||
let response = filter_bad_response(response)
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
@@ -160,10 +160,10 @@ pub async fn generate_video(args: GenerateVideoArgs<'_>) -> Result<GenerateVideo
|
||||
api_params: ApiParams {
|
||||
prompt: args.prompt,
|
||||
resolution: args.resolution.as_str().to_string(),
|
||||
content_mode: "normal",
|
||||
mode: "normal",
|
||||
model: "seedance-20",
|
||||
duration,
|
||||
mode: video_input_mode,
|
||||
video_input_mode,
|
||||
uploaded_urls,
|
||||
batch_count,
|
||||
},
|
||||
@@ -344,7 +344,7 @@ mod tests {
|
||||
session: &session,
|
||||
prompt: "A dog shakes the glasses off its head. The camera pans out as the shiba shakes. The shiba barks.".to_string(),
|
||||
resolution: Resolution::Landscape16x9,
|
||||
duration_seconds: 5,
|
||||
duration_seconds: 15,
|
||||
batch_count: BatchCount::One,
|
||||
start_frame_url: Some("https://static.seedance2-pro.com/materials/20260219/1771496300184-fb32e08c.jpg".to_string()),
|
||||
end_frame_url: None,
|
||||
@@ -365,7 +365,7 @@ mod tests {
|
||||
let session = test_session()?;
|
||||
let args = GenerateVideoArgs {
|
||||
session: &session,
|
||||
prompt: "The dog in @2 is in the office at @1 without the man. The office is dark and moonlight streams in through the windows. Particles of dust gleam in the moon beams. Suddenly, the dog jumps walks in front of the desk and barks.".to_string(),
|
||||
prompt: "The dog in @2 is in the office at @1 without the man. The office is dark and moonlight streams in through the windows. Particles of dust gleam in the moon beams. Suddenly, the dog jumps on the desk and barks.".to_string(),
|
||||
resolution: Resolution::Standard4x3,
|
||||
duration_seconds: 10,
|
||||
batch_count: BatchCount::One,
|
||||
|
||||
@@ -23,11 +23,11 @@ pub (super) struct BatchRequestJson {
|
||||
pub (super) struct ApiParams {
|
||||
pub prompt: String,
|
||||
pub resolution: String,
|
||||
#[serde(rename = "contentMode")]
|
||||
pub content_mode: &'static str,
|
||||
pub mode: &'static str,
|
||||
pub model: &'static str,
|
||||
pub duration: String,
|
||||
pub mode: &'static str,
|
||||
#[serde(rename = "videoInputMode")]
|
||||
pub video_input_mode: &'static str,
|
||||
#[serde(rename = "uploadedUrls", skip_serializing_if = "Option::is_none")]
|
||||
pub uploaded_urls: Option<Vec<String>>,
|
||||
#[serde(rename = "batchCount", skip_serializing_if = "Option::is_none")]
|
||||
|
||||
@@ -79,9 +79,8 @@ pub struct VideoResult {
|
||||
pub url: String,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
// NB: We don't need these.
|
||||
// /// Width / height ratio (e.g. 1.777… for 16:9). `None` when the server returns null (e.g. width/height are 0).
|
||||
// pub ratio: Option<f64>,
|
||||
/// Width / height ratio (e.g. 1.777… for 16:9).
|
||||
pub ratio: f64,
|
||||
}
|
||||
|
||||
/// The status of one order (one video generation task).
|
||||
@@ -180,7 +179,7 @@ pub async fn poll_orders(args: PollOrdersArgs<'_>) -> Result<PollOrdersResponse,
|
||||
url: r.url,
|
||||
width: r.width,
|
||||
height: r.height,
|
||||
// ratio: r.ratio,
|
||||
ratio: r.ratio,
|
||||
}).collect(),
|
||||
fail_reason: o.fail_reason,
|
||||
created_at: o.created_at,
|
||||
|
||||
@@ -38,5 +38,5 @@ pub(super) struct RawVideoResult {
|
||||
pub url: String,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
// pub ratio: Option<f64>,
|
||||
pub ratio: f64,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "artcraft"
|
||||
version = "0.8.0"
|
||||
version = "0.6.0"
|
||||
description = "Artcraft"
|
||||
authors = ["you"]
|
||||
license = ""
|
||||
@@ -27,7 +27,6 @@ workspace-build-acceleration.workspace = true
|
||||
|
||||
# Internal
|
||||
artcraft_api_defs.workspace = true
|
||||
artcraft_router.workspace = true
|
||||
build_metadata.workspace = true
|
||||
cookie_store.workspace = true
|
||||
enums.workspace = true
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use crate::core::artcraft_error::ArtcraftError;
|
||||
use anyhow::anyhow;
|
||||
use artcraft_router::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use artcraft_router::errors::provider_error::ProviderError;
|
||||
use base64::DecodeError;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::common::model_type::ModelType;
|
||||
@@ -213,16 +211,3 @@ impl From<WorldLabsError> for GenerateError {
|
||||
Self::ProviderFailure(ProviderFailureReason::WorldLabsError(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ArtcraftRouterError> for GenerateError {
|
||||
fn from(value: ArtcraftRouterError) -> Self {
|
||||
match value {
|
||||
ArtcraftRouterError::Client(e) => Self::BadInput(BadInputReason::WrongImageArguments(e.to_string())),
|
||||
ArtcraftRouterError::UnsupportedModel(model) => Self::NotYetImplemented(format!("Unsupported model: {}", model)),
|
||||
ArtcraftRouterError::InvalidInput(msg) => Self::BadInput(BadInputReason::WrongImageArguments(msg)),
|
||||
ArtcraftRouterError::Provider(ProviderError::Storyteller(e)) => {
|
||||
Self::ProviderFailure(ProviderFailureReason::StorytellerError(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+31
-39
@@ -1,59 +1,51 @@
|
||||
use crate::core::commands::enqueue::generate_error::GenerateError;
|
||||
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::EnqueueImageToVideoRequest;
|
||||
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::{EnqueueImageToVideoRequest, SoraOrientation};
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::events::generation_events::common::GenerationModel;
|
||||
use crate::core::events::generation_events::common::{GenerationModel};
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use artcraft_router::api::common_video_model::CommonVideoModel;
|
||||
use artcraft_router::api::image_list_ref::ImageListRef;
|
||||
use artcraft_router::api::image_ref::ImageRef;
|
||||
use artcraft_router::api::provider::Provider;
|
||||
use artcraft_router::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use artcraft_router::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use artcraft_router::client::router_client::RouterClient;
|
||||
use artcraft_router::generate::generate_video::begin_video_generation::begin_video_generation;
|
||||
use artcraft_router::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::{Seedance2p0AspectRatio, Seedance2p0MultiFunctionVideoGenRequest};
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
use log::{error, info};
|
||||
use storyteller_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use storyteller_client::endpoints::generate::video::multi_function::seedance_2p0_multi_function_video_gen::seedance_2p0_multi_function_video_gen;
|
||||
|
||||
pub(super) async fn handle_artcraft_seedance_2p0(
|
||||
pub (super) async fn handle_artcraft_seedance_2p0(
|
||||
request: &EnqueueImageToVideoRequest,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
creds: &StorytellerCredentialSet,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
let client = RouterClient::Artcraft(RouterArtcraftClient::new(
|
||||
app_env_configs.storyteller_host.clone(),
|
||||
creds.clone(),
|
||||
));
|
||||
|
||||
let start_frame = request.image_media_token.as_ref().map(ImageRef::MediaFileToken);
|
||||
let end_frame = request.end_frame_image_media_token.as_ref().map(ImageRef::MediaFileToken);
|
||||
let reference_images = request.reference_image_media_tokens.as_ref().map(ImageListRef::MediaFileTokens);
|
||||
let uuid_idempotency_token = generate_random_uuid();
|
||||
|
||||
let router_request = GenerateVideoRequest {
|
||||
model: CommonVideoModel::Seedance2p0,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: request.prompt.as_deref(),
|
||||
start_frame,
|
||||
end_frame,
|
||||
reference_images,
|
||||
resolution: None,
|
||||
aspect_ratio: request.aspect_ratio,
|
||||
duration_seconds: request.duration_seconds,
|
||||
video_batch_count: request.video_batch_count,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
idempotency_token: None,
|
||||
let aspect_ratio = match request.sora_orientation {
|
||||
Some(SoraOrientation::Portrait) => Seedance2p0AspectRatio::Portrait9x16,
|
||||
Some(SoraOrientation::Landscape) => Seedance2p0AspectRatio::Landscape16x9,
|
||||
None => Seedance2p0AspectRatio::Landscape16x9,
|
||||
};
|
||||
|
||||
let plan = begin_video_generation(&router_request)?;
|
||||
|
||||
info!("Video Generation Plan: {:?}", plan);
|
||||
let request = Seedance2p0MultiFunctionVideoGenRequest {
|
||||
uuid_idempotency_token,
|
||||
prompt: request.prompt.clone(),
|
||||
start_frame_media_token: request.image_media_token.clone(),
|
||||
end_frame_media_token: request.end_frame_image_media_token.clone(),
|
||||
reference_image_media_tokens: None,
|
||||
aspect_ratio: Some(aspect_ratio),
|
||||
duration_seconds: None, // TODO: Parameterize
|
||||
batch_count: None,
|
||||
};
|
||||
|
||||
let response = match plan.generate_video(&client).await {
|
||||
Ok(resp) => {
|
||||
let result = seedance_2p0_multi_function_video_gen(
|
||||
&app_env_configs.storyteller_host,
|
||||
Some(&creds),
|
||||
request,
|
||||
).await;
|
||||
|
||||
let job_token = match result {
|
||||
Ok(enqueued) => {
|
||||
info!("Successfully enqueued.");
|
||||
resp
|
||||
enqueued.inference_job_token
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to enqueue: {:?}", err);
|
||||
@@ -65,6 +57,6 @@ pub(super) async fn handle_artcraft_seedance_2p0(
|
||||
task_type: TaskType::VideoGeneration,
|
||||
model: Some(GenerationModel::Seedance2p0),
|
||||
provider: GenerationProvider::Artcraft,
|
||||
provider_job_id: Some(response.inference_job_token.to_string()),
|
||||
provider_job_id: Some(job_token.to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
-17
@@ -22,7 +22,6 @@ use crate::services::grok::state::grok_credential_manager::GrokCredentialManager
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use artcraft_router::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::ux::tauri_command_caller::TauriCommandCaller;
|
||||
use log::{error, info, warn};
|
||||
@@ -102,10 +101,6 @@ pub struct EnqueueImageToVideoRequest {
|
||||
/// TODO: In the future we may support base64 images, URLs, or file paths here.
|
||||
pub end_frame_image_media_token: Option<MediaFileToken>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// Reference images for the video generation (e.g. character consistency, props, etc.)
|
||||
pub reference_image_media_tokens: Option<Vec<MediaFileToken>>,
|
||||
|
||||
/// Optional.
|
||||
/// Text prompt used to direct the video.
|
||||
pub prompt: Option<String>,
|
||||
@@ -115,18 +110,6 @@ pub struct EnqueueImageToVideoRequest {
|
||||
/// Typically, this costs money when presented as an option, so we default to off.
|
||||
pub generate_audio: Option<bool>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// Aspect ratio for the generated video.
|
||||
pub aspect_ratio: Option<CommonAspectRatio>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// Duration of the generated video in seconds.
|
||||
pub duration_seconds: Option<u16>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// Number of videos to generate in one batch.
|
||||
pub video_batch_count: Option<u16>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// Only for Sora2 model currently.
|
||||
pub sora_orientation: Option<SoraOrientation>,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
|
||||
/// 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.8.0";
|
||||
pub const ARTCRAFT_VERSION: &str = "0.6.0";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2.0.0-rc",
|
||||
"productName": "ArtCraft",
|
||||
"version": "0.8.0",
|
||||
"version": "0.6.0",
|
||||
"identifier": "ai.artcraft.app",
|
||||
"build": {
|
||||
"frontendDist": "../../../frontend/apps/artcraft/dist",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2.0.0-rc",
|
||||
"productName": "ArtCraft",
|
||||
"version": "0.8.0",
|
||||
"version": "0.6.0",
|
||||
"identifier": "ai.artcraft.app",
|
||||
"build": {
|
||||
"frontendDist": "../../../frontend/apps/artcraft/dist",
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
[package]
|
||||
name = "artcraft_router"
|
||||
edition = "2024"
|
||||
version = "0.0.1"
|
||||
publish = false
|
||||
|
||||
[package.metadata.cargo-udeps.ignore]
|
||||
normal = ["workspace-build-acceleration"]
|
||||
|
||||
[lib]
|
||||
name = "artcraft_router"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
# cargo-hakari workspace hack (faster compile times)
|
||||
workspace-build-acceleration.workspace = true
|
||||
|
||||
# Internal
|
||||
artcraft_api_defs.workspace = true
|
||||
seedance2pro.workspace = true
|
||||
storyteller_client.workspace = true
|
||||
tokens.workspace = true
|
||||
|
||||
# External
|
||||
anyhow.workspace = true
|
||||
log.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
tokio.workspace = true
|
||||
uuid.workspace = true
|
||||
@@ -1,42 +0,0 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
/// This is a comprehensive list of common aspect ratios you can specify when enqueuing a generation.
|
||||
/// Not every model will support every aspect ratio.
|
||||
/// In the case a model doesn't support the aspect ratio, gracefully pick the nearest option.
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CommonAspectRatio {
|
||||
// Auto (eg. for image editing to use the source; used by Nano Banana Pro edit image, but not text-to-image)
|
||||
Auto,
|
||||
|
||||
// Square
|
||||
Square,
|
||||
|
||||
// Wide
|
||||
WideThreeByTwo,
|
||||
WideFourByThree,
|
||||
WideFiveByFour,
|
||||
WideSixteenByNine,
|
||||
WideTwentyOneByNine,
|
||||
|
||||
// Tall
|
||||
TallTwoByThree,
|
||||
TallThreeByFour,
|
||||
TallFourByFive,
|
||||
TallNineBySixteen,
|
||||
TallNineByTwentyOne,
|
||||
|
||||
// Imprecise semantic values that we probably remap to other meanings
|
||||
// on a model-by-model basis.
|
||||
Wide,
|
||||
Tall,
|
||||
|
||||
// Auto values that bake in resolution
|
||||
// These are from the Seedream models
|
||||
Auto2k,
|
||||
Auto4k,
|
||||
|
||||
// Defined aspect ratios that bake in resolution
|
||||
// These are from the Seedream models
|
||||
SquareHd,
|
||||
}
|
||||
@@ -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 CommonVideoResolution {
|
||||
/// Models: Nano Banana Pro
|
||||
OneK,
|
||||
/// Models: Nano Banana Pro
|
||||
TwoK,
|
||||
/// Models: (None)
|
||||
ThreeK,
|
||||
/// Models: Nano Banana Pro
|
||||
FourK,
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
/// Common video models supported by the router.
|
||||
/// Not all models are available through all providers.
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CommonVideoModel {
|
||||
#[serde(rename = "grok_video")]
|
||||
GrokVideo,
|
||||
|
||||
#[serde(rename = "kling_1p6_pro")]
|
||||
Kling16Pro,
|
||||
|
||||
#[serde(rename = "kling_2p1_pro")]
|
||||
Kling21Pro,
|
||||
|
||||
#[serde(rename = "kling_2p1_master")]
|
||||
Kling21Master,
|
||||
|
||||
#[serde(rename = "kling_2p5_turbo_pro")]
|
||||
Kling2p5TurboPro,
|
||||
|
||||
#[serde(rename = "kling_2p6_pro")]
|
||||
Kling2p6Pro,
|
||||
|
||||
#[serde(rename = "seedance_1p0_lite")]
|
||||
Seedance10Lite,
|
||||
|
||||
#[serde(rename = "seedance_2p0")]
|
||||
Seedance2p0,
|
||||
|
||||
#[serde(rename = "sora_2")]
|
||||
Sora2,
|
||||
|
||||
#[serde(rename = "sora_2_pro")]
|
||||
Sora2Pro,
|
||||
|
||||
#[serde(rename = "veo_2")]
|
||||
Veo2,
|
||||
|
||||
#[serde(rename = "veo_3")]
|
||||
Veo3,
|
||||
|
||||
#[serde(rename = "veo_3_fast")]
|
||||
Veo3Fast,
|
||||
|
||||
#[serde(rename = "veo_3p1")]
|
||||
Veo3p1,
|
||||
|
||||
#[serde(rename = "veo_3p1_fast")]
|
||||
Veo3p1Fast,
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum ImageListRef<'a> {
|
||||
MediaFileTokens(&'a Vec<MediaFileToken>),
|
||||
Urls(&'a Vec<String>),
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum ImageRef<'a> {
|
||||
MediaFileToken(&'a MediaFileToken),
|
||||
Url(&'a str),
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
pub mod common_aspect_ratio;
|
||||
pub mod common_resolution;
|
||||
pub mod common_video_model;
|
||||
pub mod image_list_ref;
|
||||
pub mod image_ref;
|
||||
pub mod provider;
|
||||
@@ -1,8 +0,0 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
/// The provider to route a generation request to.
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Provider {
|
||||
Artcraft,
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
pub mod multi_router_client;
|
||||
pub mod multi_router_client_builder;
|
||||
pub mod request_mismatch_mitigation_strategy;
|
||||
pub mod router_artcraft_client;
|
||||
pub mod router_client;
|
||||
@@ -1,13 +0,0 @@
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::client_error::{ClientError, ClientType};
|
||||
|
||||
pub struct MultiRouterClient {
|
||||
pub(crate) artcraft_client: Option<RouterArtcraftClient>,
|
||||
}
|
||||
|
||||
impl MultiRouterClient {
|
||||
pub fn get_artcraft_client_ref(&self) -> Result<&RouterArtcraftClient, ClientError> {
|
||||
self.artcraft_client.as_ref()
|
||||
.ok_or(ClientError::ClientNotConfigured(ClientType::Artcraft))
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::client::multi_router_client::MultiRouterClient;
|
||||
|
||||
pub struct MultiRouterClientBuilder {
|
||||
artcraft_client: Option<RouterArtcraftClient>,
|
||||
}
|
||||
|
||||
impl MultiRouterClientBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
artcraft_client: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_artcraft_client(mut self, client: RouterArtcraftClient) -> Self {
|
||||
self.artcraft_client = Some(client);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> MultiRouterClient {
|
||||
MultiRouterClient {
|
||||
artcraft_client: self.artcraft_client,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum RequestMismatchMitigationStrategy {
|
||||
/// Upgrade the request to pay more.
|
||||
PayMoreUpgrade,
|
||||
|
||||
/// Downgrade the request to pay less
|
||||
PayLessDowngrade,
|
||||
|
||||
/// Fail the request.
|
||||
ErrorOut,
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
use storyteller_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use storyteller_client::utils::api_host::ApiHost;
|
||||
|
||||
pub struct RouterArtcraftClient {
|
||||
pub(crate) api_host: ApiHost,
|
||||
pub(crate) credentials: StorytellerCredentialSet,
|
||||
}
|
||||
|
||||
impl RouterArtcraftClient {
|
||||
pub fn new(api_host: ApiHost, credentials: StorytellerCredentialSet) -> Self {
|
||||
Self { api_host, credentials }
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
use crate::client::multi_router_client::MultiRouterClient;
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::client_error::ClientError;
|
||||
|
||||
pub enum RouterClient {
|
||||
Multi(MultiRouterClient),
|
||||
Artcraft(RouterArtcraftClient),
|
||||
}
|
||||
|
||||
impl RouterClient {
|
||||
pub fn get_artcraft_client_ref(&self) -> Result<&RouterArtcraftClient, ClientError> {
|
||||
match self {
|
||||
RouterClient::Artcraft(client) => Ok(client),
|
||||
RouterClient::Multi(multi) => multi.get_artcraft_client_ref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ArtcraftRouterError {
|
||||
/// A client configuration error.
|
||||
Client(ClientError),
|
||||
|
||||
/// The requested model is not yet supported by the router.
|
||||
UnsupportedModel(String),
|
||||
|
||||
/// Invalid or missing input arguments.
|
||||
InvalidInput(String),
|
||||
|
||||
/// An error from an underlying provider.
|
||||
Provider(ProviderError),
|
||||
}
|
||||
|
||||
impl Error for ArtcraftRouterError {}
|
||||
|
||||
impl Display for ArtcraftRouterError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Client(e) => write!(f, "Client error: {}", e),
|
||||
Self::UnsupportedModel(model) => write!(f, "Unsupported model: {}", model),
|
||||
Self::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
|
||||
Self::Provider(e) => write!(f, "Provider error: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ClientError> for ArtcraftRouterError {
|
||||
fn from(error: ClientError) -> Self {
|
||||
Self::Client(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ProviderError> for ArtcraftRouterError {
|
||||
fn from(error: ProviderError) -> Self {
|
||||
Self::Provider(error)
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ClientType {
|
||||
Artcraft,
|
||||
}
|
||||
|
||||
impl Display for ClientType {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Artcraft => write!(f, "Artcraft"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ClientError {
|
||||
/// The requested client is not configured on the RouterClient.
|
||||
ClientNotConfigured(ClientType),
|
||||
|
||||
/// The model does not support the given option value.
|
||||
/// `field` is the request field name, `value` is what was requested.
|
||||
ModelDoesNotSupportOption { field: &'static str, value: String },
|
||||
|
||||
/// The caller requested zero generations, which is never valid.
|
||||
UserRequestedZeroGenerations,
|
||||
|
||||
/// ArtCraft only accepts media tokens for image inputs, not raw URLs.
|
||||
ArtcraftOnlySupportsMediaTokens,
|
||||
}
|
||||
|
||||
impl Error for ClientError {}
|
||||
|
||||
impl Display for ClientError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::ClientNotConfigured(client_type) => {
|
||||
write!(f, "{} client is not configured on the RouterClient", client_type)
|
||||
}
|
||||
Self::ModelDoesNotSupportOption { field, value } => {
|
||||
write!(f, "Model does not support '{}' for field '{}'", value, field)
|
||||
}
|
||||
Self::UserRequestedZeroGenerations => {
|
||||
write!(f, "Cannot request zero generations")
|
||||
}
|
||||
Self::ArtcraftOnlySupportsMediaTokens => {
|
||||
write!(f, "ArtCraft only supports media tokens for image inputs; upload the image first to obtain a media token")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
pub mod artcraft_router_error;
|
||||
pub mod client_error;
|
||||
pub mod provider_error;
|
||||
@@ -1,24 +0,0 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use storyteller_client::error::storyteller_error::StorytellerError;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ProviderError {
|
||||
Storyteller(StorytellerError),
|
||||
}
|
||||
|
||||
impl Error for ProviderError {}
|
||||
|
||||
impl Display for ProviderError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Storyteller(e) => write!(f, "Storyteller provider error: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StorytellerError> for ProviderError {
|
||||
fn from(error: StorytellerError) -> Self {
|
||||
Self::Storyteller(error)
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
use crate::api::common_video_model::CommonVideoModel;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance2p0::plan_generate_video_artcraft_seedance2p0;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
|
||||
/// Read the video generation request, construct a plan, then yield a means to execute it.
|
||||
pub fn begin_video_generation<'a>(
|
||||
request: &'a GenerateVideoRequest<'a>,
|
||||
) -> Result<VideoGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
match request.provider {
|
||||
Provider::Artcraft => match request.model {
|
||||
CommonVideoModel::Seedance2p0 => {
|
||||
plan_generate_video_artcraft_seedance2p0(request).map(VideoGenerationPlan::ArtcraftSeedance2p0)
|
||||
}
|
||||
_ => Err(ArtcraftRouterError::UnsupportedModel(format!("{:?}", request.model))),
|
||||
},
|
||||
}
|
||||
}
|
||||
-97
@@ -1,97 +0,0 @@
|
||||
use artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::Seedance2p0BatchCount;
|
||||
use seedance2pro::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use seedance2pro::requests::generate_video::generate_video::{BatchCount, GenerateVideoArgs, Resolution};
|
||||
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance2p0::PlanArtcraftSeedance2p0;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
|
||||
pub (crate) fn estimate_video_cost_artcraft_seedance2p0(
|
||||
plan: &PlanArtcraftSeedance2p0<'_>,
|
||||
) -> VideoGenerationCostEstimate {
|
||||
let duration_seconds = plan.duration_seconds.unwrap_or(5).clamp(4, 15);
|
||||
|
||||
let batch_count = match plan.batch_count {
|
||||
Seedance2p0BatchCount::One => BatchCount::One,
|
||||
Seedance2p0BatchCount::Two => BatchCount::Two,
|
||||
Seedance2p0BatchCount::Four => BatchCount::Four,
|
||||
};
|
||||
|
||||
// TODO: Make a better client that doesn't require this.
|
||||
// A dummy session is sufficient — cost estimation does not make any network calls.
|
||||
let dummy_session = Seedance2ProSession::from_cookies_string(String::new());
|
||||
|
||||
let args = GenerateVideoArgs {
|
||||
session: &dummy_session,
|
||||
prompt: String::new(),
|
||||
resolution: Resolution::Square1x1,
|
||||
duration_seconds,
|
||||
batch_count,
|
||||
|
||||
// NB: These do not contribute to costs in the Seedance2 integration
|
||||
start_frame_url: None,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: None,
|
||||
};
|
||||
|
||||
let cost_in_usd_cents = args.estimate_cost_in_usd_cents();
|
||||
|
||||
VideoGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::common_video_model::CommonVideoModel;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_video::begin_video_generation::begin_video_generation;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
|
||||
fn estimate_usd_cents(duration_seconds: u16, video_batch_count: u16) -> u64 {
|
||||
let request = GenerateVideoRequest {
|
||||
model: CommonVideoModel::Seedance2p0,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
start_frame: None,
|
||||
end_frame: None,
|
||||
reference_images: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
video_batch_count: Some(video_batch_count),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
idempotency_token: None,
|
||||
};
|
||||
begin_video_generation(&request)
|
||||
.expect("begin_video_generation should succeed")
|
||||
.estimate_costs()
|
||||
.cost_in_usd_cents
|
||||
.expect("cost_in_usd_cents should be present")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_estimate_cost_usd_cents() {
|
||||
// Batch 1:
|
||||
assert_eq!(estimate_usd_cents(4, 1), 64);
|
||||
assert_eq!(estimate_usd_cents(5, 1), 80);
|
||||
assert_eq!(estimate_usd_cents(6, 1), 96);
|
||||
assert_eq!(estimate_usd_cents(7, 1), 112);
|
||||
assert_eq!(estimate_usd_cents(15, 1), 240);
|
||||
|
||||
// Batch 2 = 2×
|
||||
assert_eq!(estimate_usd_cents(4, 2), 128);
|
||||
assert_eq!(estimate_usd_cents(5, 2), 160);
|
||||
assert_eq!(estimate_usd_cents(15, 2), 480);
|
||||
|
||||
// Batch 4 = 3× (not 4×)
|
||||
assert_eq!(estimate_usd_cents(4, 4), 192);
|
||||
assert_eq!(estimate_usd_cents(5, 4), 240);
|
||||
assert_eq!(estimate_usd_cents(15, 4), 720);
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod estimate_video_cost_artcraft_seedance2p0;
|
||||
@@ -1 +0,0 @@
|
||||
pub (crate) mod artcraft;
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance2p0::PlanArtcraftSeedance2p0;
|
||||
use crate::generate::generate_video::video_generation_plan::GenerateVideoResponse;
|
||||
use artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::Seedance2p0MultiFunctionVideoGenRequest;
|
||||
use storyteller_client::endpoints::generate::video::multi_function::seedance_2p0_multi_function_video_gen::seedance_2p0_multi_function_video_gen;
|
||||
|
||||
pub async fn execute_artcraft_seedance2p0(
|
||||
plan: &PlanArtcraftSeedance2p0<'_>,
|
||||
artcraft_client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
let request = Seedance2p0MultiFunctionVideoGenRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
start_frame_media_token: plan.start_frame.map(|t| t.to_owned()),
|
||||
end_frame_media_token: plan.end_frame.map(|t| t.to_owned()),
|
||||
reference_image_media_tokens: plan.reference_images.map(|tokens| tokens.to_owned()),
|
||||
aspect_ratio: plan.aspect_ratio,
|
||||
duration_seconds: plan.duration_seconds,
|
||||
batch_count: Some(plan.batch_count),
|
||||
};
|
||||
|
||||
let response = seedance_2p0_multi_function_video_gen(
|
||||
&artcraft_client.api_host,
|
||||
Some(&artcraft_client.credentials),
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
|
||||
Ok(GenerateVideoResponse {
|
||||
inference_job_token: response.inference_job_token,
|
||||
all_inference_job_tokens: response.all_inference_job_tokens,
|
||||
})
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod generate_video_artcraft_seedance2p0;
|
||||
@@ -1 +0,0 @@
|
||||
pub mod artcraft;
|
||||
@@ -1,60 +0,0 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_resolution::CommonVideoResolution;
|
||||
use crate::api::common_video_model::CommonVideoModel;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
|
||||
pub struct GenerateVideoRequest<'a> {
|
||||
/// Which model to use.
|
||||
pub model: CommonVideoModel,
|
||||
|
||||
/// Which provider to use.
|
||||
pub provider: Provider,
|
||||
|
||||
/// The prompt for the video generation
|
||||
pub prompt: Option<&'a str>,
|
||||
|
||||
// /// Some models support negative prompts
|
||||
// pub negative_prompt: Option<String>,
|
||||
|
||||
/// Starting keyframe (optional).
|
||||
pub start_frame: Option<ImageRef<'a>>,
|
||||
|
||||
/// Ending keyframe (optional).
|
||||
pub end_frame: Option<ImageRef<'a>>,
|
||||
|
||||
/// Reference images (optional).
|
||||
pub reference_images: Option<ImageListRef<'a>>,
|
||||
|
||||
/// The resolution to use
|
||||
pub resolution: Option<CommonVideoResolution>,
|
||||
|
||||
/// The aspect ratio to use
|
||||
pub aspect_ratio: Option<CommonAspectRatio>,
|
||||
|
||||
/// How many seconds to generate.
|
||||
pub duration_seconds: Option<u16>,
|
||||
|
||||
/// How many videos to generate.
|
||||
pub video_batch_count: Option<u16>,
|
||||
|
||||
// /// Whether to turn on/off audio.
|
||||
// /// Not all models support audio, not all models have a choice.
|
||||
// pub generate_audio: Option<bool>,
|
||||
|
||||
/// If the request is a mismatch with the (model/provider), how to mitigate it.
|
||||
pub request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy,
|
||||
|
||||
/// Some providers support idempotency.
|
||||
/// If not supplied, we'll generate one for the required providers.
|
||||
pub idempotency_token: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl <'a> GenerateVideoRequest<'a> {
|
||||
pub fn get_or_generate_idempotency_token(&self) -> String {
|
||||
self.idempotency_token.map(|t| t.to_string())
|
||||
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string())
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
pub mod begin_video_generation;
|
||||
pub mod cost;
|
||||
pub mod execute;
|
||||
pub mod generate_video_request;
|
||||
pub mod plan;
|
||||
pub mod video_generation_cost_estimate;
|
||||
pub mod video_generation_plan;
|
||||
@@ -1 +0,0 @@
|
||||
pub mod plan_generate_video_artcraft_seedance2p0;
|
||||
-183
@@ -1,183 +0,0 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::{
|
||||
Seedance2p0AspectRatio, Seedance2p0BatchCount,
|
||||
};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanArtcraftSeedance2p0<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
pub start_frame: Option<&'a MediaFileToken>,
|
||||
pub end_frame: Option<&'a MediaFileToken>,
|
||||
pub reference_images: Option<&'a Vec<MediaFileToken>>,
|
||||
pub aspect_ratio: Option<Seedance2p0AspectRatio>,
|
||||
pub duration_seconds: Option<u8>,
|
||||
pub batch_count: Seedance2p0BatchCount,
|
||||
pub idempotency_token: String,
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_artcraft_seedance2p0<'a>(
|
||||
request: &'a GenerateVideoRequest<'a>,
|
||||
) -> Result<PlanArtcraftSeedance2p0<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
let start_frame = resolve_image_ref(request.start_frame)?;
|
||||
let end_frame = resolve_image_ref(request.end_frame)?;
|
||||
let reference_images = resolve_image_list_ref(request.reference_images)?;
|
||||
|
||||
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let batch_count = plan_batch_count(request.video_batch_count, strategy)?;
|
||||
let duration_seconds = plan_duration(request.duration_seconds, strategy)?;
|
||||
|
||||
Ok(PlanArtcraftSeedance2p0 {
|
||||
prompt: request.prompt,
|
||||
start_frame,
|
||||
end_frame,
|
||||
reference_images,
|
||||
aspect_ratio,
|
||||
duration_seconds,
|
||||
batch_count,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_image_ref<'a>(
|
||||
image_ref: Option<ImageRef<'a>>,
|
||||
) -> Result<Option<&'a MediaFileToken>, ArtcraftRouterError> {
|
||||
match image_ref {
|
||||
None => Ok(None),
|
||||
Some(ImageRef::MediaFileToken(t)) => Ok(Some(t)),
|
||||
Some(ImageRef::Url(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_image_list_ref<'a>(
|
||||
image_list_ref: Option<ImageListRef<'a>>,
|
||||
) -> Result<Option<&'a Vec<MediaFileToken>>, ArtcraftRouterError> {
|
||||
match image_list_ref {
|
||||
None => Ok(None),
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
|
||||
Some(ImageListRef::Urls(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Supported aspect ratios and their AR values (width / height):
|
||||
// Portrait9x16 = 0.5625, Portrait3x4 = 0.75, Square1x1 = 1.0, Standard4x3 = 1.33, Landscape16x9 = 1.78
|
||||
//
|
||||
// All supported ratios cost the same, so PayMoreUpgrade and PayLessDowngrade both
|
||||
// select the nearest match rather than rounding in a specific direction.
|
||||
fn plan_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<Seedance2p0AspectRatio>, ArtcraftRouterError> {
|
||||
match aspect_ratio {
|
||||
// No preference or auto — let the model decide
|
||||
None
|
||||
| Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(None),
|
||||
|
||||
// Direct mappings
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => {
|
||||
Ok(Some(Seedance2p0AspectRatio::Landscape16x9))
|
||||
}
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => {
|
||||
Ok(Some(Seedance2p0AspectRatio::Portrait9x16))
|
||||
}
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => {
|
||||
Ok(Some(Seedance2p0AspectRatio::Square1x1))
|
||||
}
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(Seedance2p0AspectRatio::Standard4x3)),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(Seedance2p0AspectRatio::Portrait3x4)),
|
||||
|
||||
// Mismatches — apply strategy
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(Some(nearest_aspect_ratio(unsupported)))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Pick the nearest supported aspect ratio by AR value (width / height).
|
||||
/// All Seedance2p0 aspect ratios cost the same, so this is used for both upgrade and downgrade.
|
||||
fn nearest_aspect_ratio(aspect_ratio: CommonAspectRatio) -> Seedance2p0AspectRatio {
|
||||
match aspect_ratio {
|
||||
CommonAspectRatio::WideFiveByFour => Seedance2p0AspectRatio::Standard4x3, // 1.25, nearest 1.33
|
||||
CommonAspectRatio::WideThreeByTwo => Seedance2p0AspectRatio::Standard4x3, // 1.50, nearest 1.33
|
||||
CommonAspectRatio::WideTwentyOneByNine => Seedance2p0AspectRatio::Landscape16x9, // 2.33, nearest 1.78
|
||||
CommonAspectRatio::TallFourByFive => Seedance2p0AspectRatio::Portrait3x4, // 0.80, nearest 0.75
|
||||
CommonAspectRatio::TallTwoByThree => Seedance2p0AspectRatio::Portrait3x4, // 0.67, nearest 0.75
|
||||
CommonAspectRatio::TallNineByTwentyOne => Seedance2p0AspectRatio::Portrait9x16, // 0.43, nearest 0.56
|
||||
_ => Seedance2p0AspectRatio::Square1x1,
|
||||
}
|
||||
}
|
||||
|
||||
// Seedance2p0 supports batch counts of 1, 2, and 4 only.
|
||||
fn plan_batch_count(
|
||||
video_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Seedance2p0BatchCount, ArtcraftRouterError> {
|
||||
let count = video_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(Seedance2p0BatchCount::One),
|
||||
2 => Ok(Seedance2p0BatchCount::Two),
|
||||
4 => Ok(Seedance2p0BatchCount::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "video_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade => {
|
||||
// Round up: 3 → Four; anything > 4 clamps to Four
|
||||
Ok(if count < 4 { Seedance2p0BatchCount::Four } else { Seedance2p0BatchCount::Four })
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
// Round down: 3 → Two; anything > 4 clamps to Four (max available)
|
||||
Ok(if count < 4 { Seedance2p0BatchCount::Two } else { Seedance2p0BatchCount::Four })
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Seedance2p0 supports duration of 4–15 seconds.
|
||||
fn plan_duration(
|
||||
duration_seconds: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<u8>, ArtcraftRouterError> {
|
||||
const MIN: u16 = 4;
|
||||
const MAX: u16 = 15;
|
||||
match duration_seconds {
|
||||
None => Ok(None),
|
||||
Some(d) if d >= MIN && d <= MAX => Ok(Some(d as u8)),
|
||||
Some(d) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "duration_seconds",
|
||||
value: format!("{}", d),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Some(d.clamp(MIN, MAX) as u8)),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod artcraft;
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
/// The result of estimating the cost of a video generation plan.
|
||||
pub struct VideoGenerationCostEstimate {
|
||||
pub cost_in_credits: Option<u64>,
|
||||
pub cost_in_usd_cents: Option<u64>,
|
||||
pub is_free: bool,
|
||||
pub is_unlimited: bool,
|
||||
pub is_rate_limited: bool,
|
||||
pub has_watermark: bool,
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_video::cost::artcraft::estimate_video_cost_artcraft_seedance2p0::estimate_video_cost_artcraft_seedance2p0;
|
||||
use crate::generate::generate_video::execute::artcraft::generate_video_artcraft_seedance2p0::execute_artcraft_seedance2p0;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance2p0::PlanArtcraftSeedance2p0;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GenerateVideoResponse {
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
pub all_inference_job_tokens: Vec<InferenceJobToken>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum VideoGenerationPlan<'a> {
|
||||
ArtcraftSeedance2p0(PlanArtcraftSeedance2p0<'a>),
|
||||
}
|
||||
|
||||
impl<'a> VideoGenerationPlan<'a> {
|
||||
pub async fn generate_video(
|
||||
&self,
|
||||
client: &RouterClient,
|
||||
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
match self {
|
||||
VideoGenerationPlan::ArtcraftSeedance2p0(plan) => {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
execute_artcraft_seedance2p0(plan, artcraft_client).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_costs(&self) -> VideoGenerationCostEstimate {
|
||||
match self {
|
||||
VideoGenerationPlan::ArtcraftSeedance2p0(plan) => {
|
||||
estimate_video_cost_artcraft_seedance2p0(plan)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod generate_video;
|
||||
@@ -1,4 +0,0 @@
|
||||
pub mod api;
|
||||
pub mod client;
|
||||
pub mod errors;
|
||||
pub mod generate;
|
||||
@@ -1,233 +0,0 @@
|
||||
use artcraft_router::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use artcraft_router::api::common_video_model::CommonVideoModel;
|
||||
use artcraft_router::api::image_ref::ImageRef;
|
||||
use artcraft_router::api::provider::Provider;
|
||||
use artcraft_router::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use artcraft_router::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use artcraft_router::client::router_client::RouterClient;
|
||||
use artcraft_router::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use artcraft_router::errors::client_error::ClientError;
|
||||
use artcraft_router::generate::generate_video::begin_video_generation::begin_video_generation;
|
||||
use artcraft_router::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use artcraft_router::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::{
|
||||
Seedance2p0AspectRatio, Seedance2p0BatchCount,
|
||||
};
|
||||
use storyteller_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use storyteller_client::credentials::storyteller_session_cookie::StorytellerSessionCookie;
|
||||
use storyteller_client::utils::api_host::ApiHost;
|
||||
|
||||
fn get_artcraft_client() -> RouterClient {
|
||||
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/artcraft_cookies.txt")
|
||||
.expect("Failed to read /Users/bt/Artcraft/credentials/artcraft_cookies.txt");
|
||||
let cookies = cookies.trim().to_string();
|
||||
let credentials = StorytellerCredentialSet::parse_multi_cookie_header(&cookies)
|
||||
.expect("Failed to parse cookies")
|
||||
.expect("No credentials found");
|
||||
RouterClient::Artcraft(RouterArtcraftClient::new(ApiHost::Storyteller, credentials))
|
||||
}
|
||||
|
||||
fn base_request() -> GenerateVideoRequest<'static> {
|
||||
GenerateVideoRequest {
|
||||
model: CommonVideoModel::Seedance2p0,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: Some("a cat in space"),
|
||||
start_frame: None,
|
||||
end_frame: None,
|
||||
reference_images: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: None,
|
||||
video_batch_count: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
idempotency_token: None,
|
||||
}
|
||||
}
|
||||
|
||||
mod plan_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_direct_16x9() {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
..base_request()
|
||||
};
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = begin_video_generation(&request).unwrap();
|
||||
assert!(matches!(plan.aspect_ratio, Some(Seedance2p0AspectRatio::Landscape16x9)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_direct_9x16() {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::TallNineBySixteen),
|
||||
..base_request()
|
||||
};
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = begin_video_generation(&request).unwrap();
|
||||
assert!(matches!(plan.aspect_ratio, Some(Seedance2p0AspectRatio::Portrait9x16)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_direct_square() {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
..base_request()
|
||||
};
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = begin_video_generation(&request).unwrap();
|
||||
assert!(matches!(plan.aspect_ratio, Some(Seedance2p0AspectRatio::Square1x1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_nearest_match_both_strategies() {
|
||||
// WideThreeByTwo (AR 1.5) is nearest to Standard4x3 (1.33), not Landscape16x9 (1.78)
|
||||
for strategy in [
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
] {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideThreeByTwo),
|
||||
request_mismatch_mitigation_strategy: strategy,
|
||||
..base_request()
|
||||
};
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = begin_video_generation(&request).unwrap();
|
||||
assert!(
|
||||
matches!(plan.aspect_ratio, Some(Seedance2p0AspectRatio::Standard4x3)),
|
||||
"Expected Standard4x3 for WideThreeByTwo with {:?}", strategy,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_error_out_on_unsupported() {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideThreeByTwo),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_request()
|
||||
};
|
||||
let result = begin_video_generation(&request);
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_zero_is_always_error() {
|
||||
for strategy in [
|
||||
RequestMismatchMitigationStrategy::ErrorOut,
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
] {
|
||||
let request = GenerateVideoRequest {
|
||||
video_batch_count: Some(0),
|
||||
request_mismatch_mitigation_strategy: strategy,
|
||||
..base_request()
|
||||
};
|
||||
let result = begin_video_generation(&request);
|
||||
assert!(
|
||||
matches!(result, Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))),
|
||||
"Expected UserRequestedZeroGenerations with {:?}", strategy,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_direct_mapping() {
|
||||
let req = GenerateVideoRequest { video_batch_count: Some(1), ..base_request() };
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = begin_video_generation(&req).unwrap();
|
||||
assert!(matches!(plan.batch_count, Seedance2p0BatchCount::One));
|
||||
|
||||
let req = GenerateVideoRequest { video_batch_count: Some(2), ..base_request() };
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = begin_video_generation(&req).unwrap();
|
||||
assert!(matches!(plan.batch_count, Seedance2p0BatchCount::Two));
|
||||
|
||||
let req = GenerateVideoRequest { video_batch_count: Some(4), ..base_request() };
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = begin_video_generation(&req).unwrap();
|
||||
assert!(matches!(plan.batch_count, Seedance2p0BatchCount::Four));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_three_upgrade_rounds_to_four() {
|
||||
let request = GenerateVideoRequest {
|
||||
video_batch_count: Some(3),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
..base_request()
|
||||
};
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = begin_video_generation(&request).unwrap();
|
||||
assert!(matches!(plan.batch_count, Seedance2p0BatchCount::Four));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_three_downgrade_rounds_to_two() {
|
||||
let request = GenerateVideoRequest {
|
||||
video_batch_count: Some(3),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
..base_request()
|
||||
};
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = begin_video_generation(&request).unwrap();
|
||||
assert!(matches!(plan.batch_count, Seedance2p0BatchCount::Two));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_image_ref_returns_error() {
|
||||
let request = GenerateVideoRequest {
|
||||
start_frame: Some(ImageRef::Url("https://example.com/image.jpg")),
|
||||
..base_request()
|
||||
};
|
||||
let result = begin_video_generation(&request);
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
mod real_requests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real API request and incurs cost
|
||||
async fn test_text_to_video_seedance_2p0() {
|
||||
let client = get_artcraft_client();
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
video_batch_count: Some(1),
|
||||
prompt: Some("a cat walking through a cyberpunk city at night"),
|
||||
..base_request()
|
||||
};
|
||||
|
||||
let plan = begin_video_generation(&request).unwrap();
|
||||
let result = plan.generate_video(&client).await;
|
||||
|
||||
println!("Result: {:?}", result);
|
||||
let response = result.expect("generate_video request failed");
|
||||
println!("Job token: {:?}", response.inference_job_token);
|
||||
println!("All job tokens: {:?}", response.all_inference_job_tokens);
|
||||
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real API request and incurs cost
|
||||
async fn test_text_to_video_seedance_2p0_batch_two() {
|
||||
let client = get_artcraft_client();
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
video_batch_count: Some(2),
|
||||
prompt: Some("a dog surfing a wave, cinematic"),
|
||||
..base_request()
|
||||
};
|
||||
|
||||
let plan = begin_video_generation(&request).unwrap();
|
||||
let result = plan.generate_video(&client).await;
|
||||
|
||||
println!("Result: {:?}", result);
|
||||
let response = result.expect("generate_video request failed");
|
||||
println!("Job tokens ({} total):", response.all_inference_job_tokens.len());
|
||||
for token in &response.all_inference_job_tokens {
|
||||
println!(" {:?}", token);
|
||||
}
|
||||
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
|
||||
}
|
||||
}
|
||||
@@ -22,5 +22,8 @@ workspace-build-acceleration.workspace = true
|
||||
once_cell.workspace = true
|
||||
wreq.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
wreq = { workspace = true, features = ["prefix-symbols"] }
|
||||
|
||||
[dev-dependencies]
|
||||
# (None)
|
||||
|
||||
@@ -16,11 +16,11 @@ path = "src/lib.rs"
|
||||
normal = ["workspace-build-acceleration"]
|
||||
|
||||
[dependencies]
|
||||
# cargo-hakari workspace hack (faster compile times)
|
||||
workspace-build-acceleration.workspace = true
|
||||
|
||||
# Internal
|
||||
errors.workspace = true
|
||||
errors = { path = "../errors" }
|
||||
|
||||
# Workspace hack (faster compile times)
|
||||
workspace-build-acceleration = { version = "0.1", path = "../../workspace-build-acceleration" }
|
||||
|
||||
# External
|
||||
# (None yet)
|
||||
|
||||
-7
@@ -13,7 +13,6 @@ use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::prompts::PromptToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
use tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken;
|
||||
|
||||
use crate::errors::database_query_error::DatabaseQueryError;
|
||||
use crate::payloads::generic_inference_args::generic_inference_args::GenericInferenceArgs;
|
||||
@@ -33,8 +32,6 @@ pub struct InsertGenericInferenceForSeedance2ProWithAprioriJobTokenArgs<'e, 'c,
|
||||
|
||||
pub maybe_prompt_token: Option<&'e PromptToken>,
|
||||
|
||||
pub maybe_wallet_ledger_entry_token: Option<&'e WalletLedgerEntryToken>,
|
||||
|
||||
pub maybe_creator_user_token: Option<&'e UserToken>,
|
||||
pub maybe_avt_token: Option<&'e AnonymousVisitorTrackingToken>,
|
||||
pub creator_ip_address: &'e str,
|
||||
@@ -85,8 +82,6 @@ SET
|
||||
|
||||
maybe_prompt_token = ?,
|
||||
|
||||
maybe_wallet_ledger_entry_token = ?,
|
||||
|
||||
maybe_raw_inference_text = NULL,
|
||||
|
||||
maybe_inference_args = ?,
|
||||
@@ -118,8 +113,6 @@ SET
|
||||
|
||||
args.maybe_prompt_token.map(|t| t.to_string()),
|
||||
|
||||
args.maybe_wallet_ledger_entry_token.map(|t| t.to_string()),
|
||||
|
||||
serialized_args_payload,
|
||||
|
||||
args.maybe_creator_user_token.map(|t| t.to_string()),
|
||||
|
||||
+1
-7
@@ -9,7 +9,6 @@ use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::prompts::PromptToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
use tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken;
|
||||
|
||||
/// A Seedance2Pro job that is waiting for a result from the external API.
|
||||
#[derive(Debug)]
|
||||
@@ -25,8 +24,6 @@ pub struct PendingSeedance2ProJob {
|
||||
pub creator_set_visibility: Visibility,
|
||||
|
||||
pub maybe_prompt_token: Option<PromptToken>,
|
||||
|
||||
pub maybe_wallet_ledger_entry_token: Option<WalletLedgerEntryToken>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@@ -38,7 +35,6 @@ struct RawRecord {
|
||||
creator_ip_address: String,
|
||||
creator_set_visibility: Visibility,
|
||||
maybe_prompt_token: Option<PromptToken>,
|
||||
maybe_wallet_ledger_entry_token: Option<WalletLedgerEntryToken>,
|
||||
}
|
||||
|
||||
/// Returns all non-terminal Seedance2Pro jobs that have an associated order_id.
|
||||
@@ -53,8 +49,7 @@ SELECT
|
||||
jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,
|
||||
jobs.creator_ip_address,
|
||||
jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,
|
||||
jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,
|
||||
jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`
|
||||
jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`
|
||||
|
||||
FROM generic_inference_jobs as jobs
|
||||
|
||||
@@ -89,7 +84,6 @@ LIMIT 25000
|
||||
creator_ip_address: record.creator_ip_address,
|
||||
creator_set_visibility: record.creator_set_visibility,
|
||||
maybe_prompt_token: record.maybe_prompt_token,
|
||||
maybe_wallet_ledger_entry_token: record.maybe_wallet_ledger_entry_token,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
+24
-18
@@ -21,7 +21,7 @@ pub (crate) struct InsertWalletLedgerEntry<'a> {
|
||||
/// An optional reference for this entry. Could be an internal ID (eg. job) or an
|
||||
/// external ID (eg. Stripe payment intent, invoice, etc.)
|
||||
pub maybe_entity_ref: Option<String>,
|
||||
|
||||
|
||||
/// Change in credits (positive or negative) across banked and/or monthly credits.
|
||||
pub credits_delta: i64,
|
||||
|
||||
@@ -40,35 +40,41 @@ pub (crate) struct InsertWalletLedgerEntry<'a> {
|
||||
|
||||
impl <'a> InsertWalletLedgerEntry<'a> {
|
||||
|
||||
pub async fn upsert_with_pool(&'a self, mysql_pool: &MySqlPool) -> AnyhowResult<WalletLedgerEntryToken> {
|
||||
pub async fn upsert(&'a self, mysql_pool: &MySqlPool) -> AnyhowResult<()> {
|
||||
let mut conn = mysql_pool.acquire().await?;
|
||||
self.upsert_with_connection(&mut conn).await
|
||||
}
|
||||
|
||||
pub async fn upsert_with_connection(&'a self, mysql_connection: &mut PoolConnection<MySql>) -> AnyhowResult<WalletLedgerEntryToken> {
|
||||
let ledger_token = WalletLedgerEntryToken::generate();
|
||||
let query_result = self.query_with_token(&ledger_token).execute(&mut **mysql_connection).await;
|
||||
pub async fn upsert_with_connection(&'a self, mysql_connection: &mut PoolConnection<MySql>) -> AnyhowResult<()> {
|
||||
let query = self.query();
|
||||
let query_result = query.execute(&mut **mysql_connection).await;
|
||||
|
||||
match query_result {
|
||||
Ok(_) => Ok(ledger_token),
|
||||
Err(err) => Err(anyhow!("Error upserting wallet ledger entry record: {:?}", err)),
|
||||
}
|
||||
let _record_id = match query_result {
|
||||
Ok(res) => res.last_insert_id(),
|
||||
Err(err) => return Err(anyhow!("Error upserting wallet ledger entry record: {:?}", err)),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn upsert_with_transaction(&'a self, transaction: &mut Transaction<'_, MySql>) -> Result<WalletLedgerEntryToken, sqlx::Error> {
|
||||
let ledger_token = WalletLedgerEntryToken::generate();
|
||||
let query_result = self.query_with_token(&ledger_token).execute(&mut **transaction).await;
|
||||
pub async fn upsert_with_transaction(&'a self, transaction: &mut Transaction<'_, MySql>) -> Result<(), sqlx::Error> {
|
||||
let query = self.query();
|
||||
let query_result = query.execute(&mut **transaction).await;
|
||||
|
||||
match query_result {
|
||||
Ok(_) => Ok(ledger_token),
|
||||
let _record_id = match query_result {
|
||||
Ok(res) => res.last_insert_id(),
|
||||
Err(err) => {
|
||||
error!("Error upserting wallet ledger entry record: {:?}", err);
|
||||
Err(err)
|
||||
return Err(err);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn query_with_token(&self, ledger_token: &WalletLedgerEntryToken) -> Query<MySql, MySqlArguments> {
|
||||
fn query(&self) -> Query<MySql, MySqlArguments> {
|
||||
let token = WalletLedgerEntryToken::generate().to_string();
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO wallet_ledger_entries
|
||||
@@ -83,7 +89,7 @@ SET
|
||||
monthly_credits_before = ?,
|
||||
monthly_credits_after = ?
|
||||
"#,
|
||||
ledger_token.as_str(),
|
||||
token.as_str(),
|
||||
self.wallet_token.as_str(),
|
||||
self.entry_type.to_str(),
|
||||
self.maybe_entity_ref,
|
||||
|
||||
+2
-3
@@ -52,12 +52,11 @@ pub async fn add_durable_banked_balance_to_wallet(
|
||||
monthly_credits_after: wallet.monthly_credits,
|
||||
};
|
||||
|
||||
let wallet_ledger_entry_token = record.upsert_with_transaction(transaction).await?;
|
||||
record.upsert_with_transaction(transaction).await?;
|
||||
|
||||
|
||||
Ok(WalletUpdateSummary {
|
||||
wallet_token: wallet.token,
|
||||
wallet_ledger_entry_token,
|
||||
token: wallet.token,
|
||||
namespace: wallet.namespace,
|
||||
owner_user_token: wallet.owner_user_token,
|
||||
|
||||
|
||||
@@ -4,6 +4,5 @@ pub mod create_new_artcraft_wallet_for_owner_user;
|
||||
pub mod find_primary_wallet_for_owner;
|
||||
pub mod find_primary_wallet_token_for_owner;
|
||||
pub mod refill_monthly_credits_balance_on_wallet;
|
||||
pub mod refund;
|
||||
pub mod spend;
|
||||
pub mod wallet_update_summary;
|
||||
+2
-3
@@ -51,12 +51,11 @@ pub async fn refill_monthly_credits_balance_on_wallet(
|
||||
banked_credits_after: wallet.banked_credits,
|
||||
};
|
||||
|
||||
let wallet_ledger_entry_token = record.upsert_with_transaction(transaction).await?;
|
||||
record.upsert_with_transaction(transaction).await?;
|
||||
|
||||
|
||||
Ok(WalletUpdateSummary {
|
||||
wallet_token: wallet.token,
|
||||
wallet_ledger_entry_token,
|
||||
token: wallet.token,
|
||||
namespace: wallet.namespace,
|
||||
owner_user_token: wallet.owner_user_token,
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod try_to_refund_ledger_entry;
|
||||
pub mod wallet_refund_error;
|
||||
-219
@@ -1,219 +0,0 @@
|
||||
use log::info;
|
||||
use sqlx::MySql;
|
||||
|
||||
use enums::by_table::wallet_ledger_entries::wallet_ledger_entry_type::WalletLedgerEntryType;
|
||||
use tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken;
|
||||
use tokens::tokens::wallets::WalletToken;
|
||||
|
||||
use crate::queries::wallet_ledger_entries::internal_insert_wallet_ledger_entry::InsertWalletLedgerEntry;
|
||||
use crate::queries::wallets::internal_select_wallet_balance_for_update::internal_select_wallet_balance_for_update;
|
||||
use crate::queries::wallets::refund::wallet_refund_error::WalletRefundError;
|
||||
|
||||
pub enum WalletRefundOutcome {
|
||||
/// The refund was successfully applied.
|
||||
Refunded(WalletRefundSummary),
|
||||
|
||||
/// The ledger entry was already marked as refunded; no action was taken.
|
||||
/// This is not an error — it is safe to call refund multiple times.
|
||||
AlreadyRefunded,
|
||||
}
|
||||
|
||||
pub struct WalletRefundSummary {
|
||||
pub wallet_token: WalletToken,
|
||||
pub original_ledger_entry_token: WalletLedgerEntryToken,
|
||||
pub refund_ledger_entry_token: WalletLedgerEntryToken,
|
||||
pub refund_amount: u64,
|
||||
pub banked_credits_before: u64,
|
||||
pub banked_credits_after: u64,
|
||||
}
|
||||
|
||||
/// Refund a wallet ledger entry, crediting the amount back as banked (durable) credits.
|
||||
///
|
||||
/// NB: BE VERY CAREFUL WITH THIS FUNCTION!
|
||||
/// It locks both the ledger entry row and the wallet row for the duration of the transaction.
|
||||
///
|
||||
/// We always refund into banked credits rather than monthly credits. Refunding into monthly
|
||||
/// credits near a billing cycle cutoff could create race conditions with the monthly refill job,
|
||||
/// so banked credits are the safer, permanent choice.
|
||||
pub async fn try_to_refund_ledger_entry(
|
||||
ledger_entry_token: &WalletLedgerEntryToken,
|
||||
transaction: &mut sqlx::Transaction<'_, MySql>,
|
||||
) -> Result<WalletRefundOutcome, WalletRefundError> {
|
||||
|
||||
// Step 1: Lock the original ledger entry for the duration of this transaction.
|
||||
let original_entry = select_ledger_entry_for_update(ledger_entry_token, transaction).await?;
|
||||
|
||||
// Step 2: Guard — bail out gracefully if it was already refunded.
|
||||
if original_entry.is_refunded {
|
||||
return Ok(WalletRefundOutcome::AlreadyRefunded);
|
||||
}
|
||||
|
||||
// Step 3: Guard — only deduct-type entries may be refunded.
|
||||
let is_deduct_type = matches!(
|
||||
original_entry.entry_type,
|
||||
WalletLedgerEntryType::DeductMixed
|
||||
| WalletLedgerEntryType::DeductBanked
|
||||
| WalletLedgerEntryType::DeductMonthly,
|
||||
);
|
||||
|
||||
if !is_deduct_type {
|
||||
return Err(WalletRefundError::NotADeductEntry(original_entry.entry_type));
|
||||
}
|
||||
|
||||
// Step 4: Lock the wallet row so our balance arithmetic is atomic.
|
||||
let wallet = internal_select_wallet_balance_for_update(
|
||||
&original_entry.wallet_token,
|
||||
transaction,
|
||||
).await.map_err(|_| WalletRefundError::WalletNotFound)?;
|
||||
|
||||
// credits_delta was negative for a deduction (e.g. -100). The refund amount is its magnitude.
|
||||
let refund_amount = original_entry.credits_delta.unsigned_abs();
|
||||
|
||||
let banked_credits_before = wallet.banked_credits;
|
||||
let banked_credits_after = banked_credits_before.saturating_add(refund_amount);
|
||||
|
||||
// Step 5: Insert the refund ledger entry.
|
||||
//
|
||||
// We record the original ledger entry token as the entity_ref so there is a clear audit trail
|
||||
// linking the refund back to the original spend. The new entry itself has is_refunded = false
|
||||
// because it is the refund, not the thing being refunded.
|
||||
let refund_ledger_record = InsertWalletLedgerEntry {
|
||||
wallet_token: &original_entry.wallet_token,
|
||||
entry_type: WalletLedgerEntryType::RefundBanked,
|
||||
maybe_entity_ref: Some(ledger_entry_token.to_string()),
|
||||
|
||||
credits_delta: refund_amount as i64,
|
||||
|
||||
banked_credits_before,
|
||||
banked_credits_after,
|
||||
|
||||
// Monthly credits are untouched by a banked refund.
|
||||
monthly_credits_before: wallet.monthly_credits,
|
||||
monthly_credits_after: wallet.monthly_credits,
|
||||
};
|
||||
|
||||
let refund_ledger_entry_token = refund_ledger_record
|
||||
.upsert_with_transaction(transaction)
|
||||
.await?;
|
||||
|
||||
// Step 6: Credit the wallet's banked balance.
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE wallets
|
||||
SET
|
||||
banked_credits = ?,
|
||||
version = version + 1
|
||||
WHERE token = ?
|
||||
LIMIT 1
|
||||
"#,
|
||||
banked_credits_after,
|
||||
original_entry.wallet_token.as_str(),
|
||||
).execute(&mut **transaction).await?;
|
||||
|
||||
// Step 7: Mark the original entry as refunded and link it to the new refund record.
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE wallet_ledger_entries
|
||||
SET
|
||||
is_refunded = TRUE,
|
||||
maybe_linked_refund_ledger_token = ?
|
||||
WHERE token = ?
|
||||
LIMIT 1
|
||||
"#,
|
||||
refund_ledger_entry_token.as_str(),
|
||||
ledger_entry_token.as_str(),
|
||||
).execute(&mut **transaction).await?;
|
||||
|
||||
info!(
|
||||
"Refunded ledger entry {} → new refund entry {}; wallet {} banked credits: {} → {}",
|
||||
ledger_entry_token.as_str(),
|
||||
refund_ledger_entry_token.as_str(),
|
||||
original_entry.wallet_token.as_str(),
|
||||
banked_credits_before,
|
||||
banked_credits_after,
|
||||
);
|
||||
|
||||
Ok(WalletRefundOutcome::Refunded(WalletRefundSummary {
|
||||
wallet_token: original_entry.wallet_token,
|
||||
original_ledger_entry_token: ledger_entry_token.clone(),
|
||||
refund_ledger_entry_token,
|
||||
refund_amount,
|
||||
banked_credits_before,
|
||||
banked_credits_after,
|
||||
}))
|
||||
}
|
||||
|
||||
// ===== Internal helpers =====
|
||||
|
||||
struct LedgerEntryForUpdate {
|
||||
wallet_token: WalletToken,
|
||||
entry_type: WalletLedgerEntryType,
|
||||
is_refunded: bool,
|
||||
/// Negative for deductions, positive for credits.
|
||||
credits_delta: i64,
|
||||
banked_credits_before: u64,
|
||||
banked_credits_after: u64,
|
||||
monthly_credits_before: u64,
|
||||
monthly_credits_after: u64,
|
||||
}
|
||||
|
||||
/// SELECT ... FOR UPDATE on wallet_ledger_entries.
|
||||
/// Locks the row for the duration of the enclosing transaction.
|
||||
async fn select_ledger_entry_for_update(
|
||||
ledger_entry_token: &WalletLedgerEntryToken,
|
||||
transaction: &mut sqlx::Transaction<'_, MySql>,
|
||||
) -> Result<LedgerEntryForUpdate, WalletRefundError> {
|
||||
|
||||
struct RawLedgerEntryForUpdate {
|
||||
wallet_token: WalletToken,
|
||||
entry_type: String,
|
||||
is_refunded: bool,
|
||||
credits_delta: i32,
|
||||
banked_credits_before: u32,
|
||||
banked_credits_after: u32,
|
||||
monthly_credits_before: u32,
|
||||
monthly_credits_after: u32,
|
||||
}
|
||||
|
||||
let raw = sqlx::query_as!(
|
||||
RawLedgerEntryForUpdate,
|
||||
r#"
|
||||
SELECT
|
||||
wallet_token as `wallet_token: tokens::tokens::wallets::WalletToken`,
|
||||
entry_type,
|
||||
is_refunded as `is_refunded: bool`,
|
||||
credits_delta,
|
||||
banked_credits_before,
|
||||
banked_credits_after,
|
||||
monthly_credits_before,
|
||||
monthly_credits_after
|
||||
FROM wallet_ledger_entries
|
||||
WHERE token = ?
|
||||
LIMIT 1
|
||||
FOR UPDATE
|
||||
"#,
|
||||
ledger_entry_token.as_str(),
|
||||
)
|
||||
.fetch_one(&mut **transaction)
|
||||
.await
|
||||
.map_err(|e| match e {
|
||||
sqlx::Error::RowNotFound => WalletRefundError::LedgerEntryNotFound,
|
||||
err => WalletRefundError::SqlxError(err),
|
||||
})?;
|
||||
|
||||
let entry_type = WalletLedgerEntryType::from_str(&raw.entry_type)
|
||||
.map_err(|_| WalletRefundError::SqlxError(sqlx::Error::Decode(
|
||||
format!("unknown wallet_ledger_entry entry_type: {:?}", raw.entry_type).into()
|
||||
)))?;
|
||||
|
||||
Ok(LedgerEntryForUpdate {
|
||||
wallet_token: raw.wallet_token,
|
||||
entry_type,
|
||||
is_refunded: raw.is_refunded,
|
||||
credits_delta: raw.credits_delta as i64,
|
||||
banked_credits_before: raw.banked_credits_before as u64,
|
||||
banked_credits_after: raw.banked_credits_after as u64,
|
||||
monthly_credits_before: raw.monthly_credits_before as u64,
|
||||
monthly_credits_after: raw.monthly_credits_after as u64,
|
||||
})
|
||||
}
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
use crate::errors::select_exactly_one_error::SelectExactlyOneError;
|
||||
use enums::by_table::wallet_ledger_entries::wallet_ledger_entry_type::WalletLedgerEntryType;
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum WalletRefundError {
|
||||
/// The wallet ledger entry to refund was not found.
|
||||
LedgerEntryNotFound,
|
||||
|
||||
/// The wallet referenced by the ledger entry was not found.
|
||||
WalletNotFound,
|
||||
|
||||
/// The ledger entry is not a deduct-type entry; only DeductMixed, DeductBanked,
|
||||
/// and DeductMonthly entries can be refunded.
|
||||
NotADeductEntry(WalletLedgerEntryType),
|
||||
|
||||
/// Error selecting a record.
|
||||
SelectError(SelectExactlyOneError),
|
||||
|
||||
/// Underlying database error.
|
||||
SqlxError(sqlx::Error),
|
||||
}
|
||||
|
||||
impl Error for WalletRefundError {}
|
||||
|
||||
impl Display for WalletRefundError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
WalletRefundError::LedgerEntryNotFound => write!(f, "Ledger entry not found"),
|
||||
WalletRefundError::WalletNotFound => write!(f, "Wallet not found"),
|
||||
WalletRefundError::NotADeductEntry(entry_type) => {
|
||||
write!(f, "Cannot refund a non-deduct ledger entry: {:?}", entry_type)
|
||||
}
|
||||
WalletRefundError::SelectError(err) => write!(f, "Database select error: {}", err),
|
||||
WalletRefundError::SqlxError(err) => write!(f, "Database error: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SelectExactlyOneError> for WalletRefundError {
|
||||
fn from(err: SelectExactlyOneError) -> Self {
|
||||
WalletRefundError::SelectError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sqlx::Error> for WalletRefundError {
|
||||
fn from(err: sqlx::Error) -> Self {
|
||||
WalletRefundError::SqlxError(err)
|
||||
}
|
||||
}
|
||||
+2
-3
@@ -115,11 +115,10 @@ pub async fn try_to_spend_wallet_balance(
|
||||
banked_credits_after: updated_banked_balance
|
||||
};
|
||||
|
||||
let wallet_ledger_entry_token = record.upsert_with_transaction(transaction).await?;
|
||||
record.upsert_with_transaction(transaction).await?;
|
||||
|
||||
Ok(WalletUpdateSummary {
|
||||
wallet_token: wallet.token,
|
||||
wallet_ledger_entry_token,
|
||||
token: wallet.token,
|
||||
namespace: wallet.namespace,
|
||||
owner_user_token: wallet.owner_user_token,
|
||||
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
use enums::common::payments_namespace::PaymentsNamespace;
|
||||
use tokens::tokens::users::UserToken;
|
||||
use tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken;
|
||||
use tokens::tokens::wallets::WalletToken;
|
||||
|
||||
pub struct WalletUpdateSummary {
|
||||
pub wallet_token: WalletToken,
|
||||
|
||||
pub wallet_ledger_entry_token: WalletLedgerEntryToken,
|
||||
pub token: WalletToken,
|
||||
|
||||
pub namespace: PaymentsNamespace,
|
||||
|
||||
|
||||
+1
-20
@@ -36,18 +36,6 @@ pub enum WalletLedgerEntryType {
|
||||
/// Deduct monthly credits
|
||||
#[serde(rename = "deduct_monthly")]
|
||||
DeductMonthly,
|
||||
|
||||
/// Refund banked credits
|
||||
#[serde(rename = "refund_banked")]
|
||||
RefundBanked,
|
||||
|
||||
// TODO: No clean way to do "mixed" refunds yet, and if we
|
||||
// refund close to the cutoff it might be unfair. Let's
|
||||
// just not do monthly refunds yet and instead credit our
|
||||
// users with banked credits.
|
||||
// /// Refund monthly credits
|
||||
// #[serde(rename = "refund_monthly")]
|
||||
// RefundMonthly,
|
||||
}
|
||||
|
||||
// TODO(bt, 2022-12-21): This desperately needs MySQL integration tests!
|
||||
@@ -65,7 +53,6 @@ impl WalletLedgerEntryType {
|
||||
Self::DeductMixed => "deduct_mixed",
|
||||
Self::DeductBanked => "deduct_banked",
|
||||
Self::DeductMonthly => "deduct_monthly",
|
||||
Self::RefundBanked => "refund_banked",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +64,6 @@ impl WalletLedgerEntryType {
|
||||
"deduct_mixed" => Ok(Self::DeductMixed),
|
||||
"deduct_banked" => Ok(Self::DeductBanked),
|
||||
"deduct_monthly" => Ok(Self::DeductMonthly),
|
||||
"refund_banked" => Ok(Self::RefundBanked),
|
||||
_ => Err(format!("invalid value: {:?}", value)),
|
||||
}
|
||||
}
|
||||
@@ -92,7 +78,6 @@ impl WalletLedgerEntryType {
|
||||
Self::DeductMixed,
|
||||
Self::DeductBanked,
|
||||
Self::DeductMonthly,
|
||||
Self::RefundBanked,
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -113,7 +98,6 @@ mod tests {
|
||||
assert_serialization(WalletLedgerEntryType::DeductMixed, "deduct_mixed");
|
||||
assert_serialization(WalletLedgerEntryType::DeductBanked, "deduct_banked");
|
||||
assert_serialization(WalletLedgerEntryType::DeductMonthly, "deduct_monthly");
|
||||
assert_serialization(WalletLedgerEntryType::RefundBanked, "refund_banked");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +112,6 @@ mod tests {
|
||||
assert_eq!(WalletLedgerEntryType::DeductMixed.to_str(), "deduct_mixed");
|
||||
assert_eq!(WalletLedgerEntryType::DeductBanked.to_str(), "deduct_banked");
|
||||
assert_eq!(WalletLedgerEntryType::DeductMonthly.to_str(), "deduct_monthly");
|
||||
assert_eq!(WalletLedgerEntryType::RefundBanked.to_str(), "refund_banked");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -139,7 +122,6 @@ mod tests {
|
||||
assert_eq!(WalletLedgerEntryType::from_str("deduct_mixed").unwrap(), WalletLedgerEntryType::DeductMixed);
|
||||
assert_eq!(WalletLedgerEntryType::from_str("deduct_banked").unwrap(), WalletLedgerEntryType::DeductBanked);
|
||||
assert_eq!(WalletLedgerEntryType::from_str("deduct_monthly").unwrap(), WalletLedgerEntryType::DeductMonthly);
|
||||
assert_eq!(WalletLedgerEntryType::from_str("refund_banked").unwrap(), WalletLedgerEntryType::RefundBanked);
|
||||
assert!(WalletLedgerEntryType::from_str("foo").is_err());
|
||||
}
|
||||
}
|
||||
@@ -150,14 +132,13 @@ mod tests {
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
let mut variants = WalletLedgerEntryType::all_variants();
|
||||
assert_eq!(variants.len(), 7);
|
||||
assert_eq!(variants.len(), 6);
|
||||
assert_eq!(variants.pop_first(), Some(WalletLedgerEntryType::Create));
|
||||
assert_eq!(variants.pop_first(), Some(WalletLedgerEntryType::CreditBanked));
|
||||
assert_eq!(variants.pop_first(), Some(WalletLedgerEntryType::CreditMonthly));
|
||||
assert_eq!(variants.pop_first(), Some(WalletLedgerEntryType::DeductMixed));
|
||||
assert_eq!(variants.pop_first(), Some(WalletLedgerEntryType::DeductBanked));
|
||||
assert_eq!(variants.pop_first(), Some(WalletLedgerEntryType::DeductMonthly));
|
||||
assert_eq!(variants.pop_first(), Some(WalletLedgerEntryType::RefundBanked));
|
||||
assert_eq!(variants.pop_first(), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Common aspect ratios for video generation.
|
||||
/// Mirrors artcraft_router::api::common_aspect_ratio::CommonAspectRatio.
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CommonAspectRatio {
|
||||
Auto,
|
||||
Square,
|
||||
WideThreeByTwo,
|
||||
WideFourByThree,
|
||||
WideFiveByFour,
|
||||
WideSixteenByNine,
|
||||
WideTwentyOneByNine,
|
||||
TallTwoByThree,
|
||||
TallThreeByFour,
|
||||
TallFourByFive,
|
||||
TallNineBySixteen,
|
||||
TallNineByTwentyOne,
|
||||
Wide,
|
||||
Tall,
|
||||
Auto2k,
|
||||
Auto4k,
|
||||
SquareHd,
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Video models available for generation.
|
||||
/// Mirrors artcraft_router::api::common_video_model::CommonVideoModel.
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CommonVideoModel {
|
||||
#[serde(rename = "grok_video")]
|
||||
GrokVideo,
|
||||
|
||||
#[serde(rename = "kling_1p6_pro")]
|
||||
Kling16Pro,
|
||||
|
||||
#[serde(rename = "kling_2p1_pro")]
|
||||
Kling21Pro,
|
||||
|
||||
#[serde(rename = "kling_2p1_master")]
|
||||
Kling21Master,
|
||||
|
||||
#[serde(rename = "kling_2p5_turbo_pro")]
|
||||
Kling2p5TurboPro,
|
||||
|
||||
#[serde(rename = "kling_2p6_pro")]
|
||||
Kling2p6Pro,
|
||||
|
||||
#[serde(rename = "seedance_1p0_lite")]
|
||||
Seedance10Lite,
|
||||
|
||||
#[serde(rename = "seedance_2p0")]
|
||||
Seedance2p0,
|
||||
|
||||
#[serde(rename = "sora_2")]
|
||||
Sora2,
|
||||
|
||||
#[serde(rename = "sora_2_pro")]
|
||||
Sora2Pro,
|
||||
|
||||
#[serde(rename = "veo_2")]
|
||||
Veo2,
|
||||
|
||||
#[serde(rename = "veo_3")]
|
||||
Veo3,
|
||||
|
||||
#[serde(rename = "veo_3_fast")]
|
||||
Veo3Fast,
|
||||
|
||||
#[serde(rename = "veo_3p1")]
|
||||
Veo3p1,
|
||||
|
||||
#[serde(rename = "veo_3p1_fast")]
|
||||
Veo3p1Fast,
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Common video resolutions.
|
||||
/// Mirrors artcraft_router::api::common_resolution::CommonVideoResolution.
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CommonVideoResolution {
|
||||
OneK,
|
||||
TwoK,
|
||||
ThreeK,
|
||||
FourK,
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
pub mod common_aspect_ratio;
|
||||
pub mod common_video_model;
|
||||
pub mod common_video_resolution;
|
||||
@@ -1,10 +1,3 @@
|
||||
//! This is an important enum.
|
||||
//!
|
||||
//! It's used in storyteller-web inference generation, the cost estimate handler,
|
||||
//! the ArtCraft Tauri app, and more.
|
||||
//!
|
||||
//! Do not change the values here without cause or care.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use crate::error::enum_error::EnumError;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user