mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-03 13:02:37 -05:00
Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42694bd361 | ||
|
|
33909e0420 | ||
|
|
963521d87b | ||
|
|
4323743fe7 | ||
|
|
752fad12b3 | ||
|
|
99b831c3b0 | ||
|
|
4a1d1acddd | ||
|
|
47d6ac6841 | ||
|
|
689bda8bf3 | ||
|
|
7e36149d13 | ||
|
|
2bbeb3de78 | ||
|
|
eeacbf6719 | ||
|
|
af763669cc | ||
|
|
7ff78c6d94 | ||
|
|
588478da73 | ||
|
|
5cb7b80b44 |
@@ -60,8 +60,14 @@ then the story, then the fine print.
|
||||
`use artcraft_router::...::CommonResolution as CommonResolutionRouter;`
|
||||
This avoids long fully-qualified paths in function signatures and bodies.
|
||||
|
||||
## MySQL
|
||||
|
||||
- MySQL tables: primary key `id` columns must be `BIGINT(20) UNSIGNED`. Do not use signed IDs.
|
||||
- Always create tables with `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin`.
|
||||
|
||||
## TypeScript / Frontend
|
||||
|
||||
- TypeScript with React, Vite, Zustand, Three.js
|
||||
- Nx monorepo under `crates/frontend/`
|
||||
- Two spaces for indentation
|
||||
|
||||
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n uir.id,\n uir.impersonator_user_token as `impersonator_user_token: UserToken`,\n impersonator.username as impersonator_username,\n impersonator.display_name as impersonator_display_name,\n uir.impersonated_user_token as `impersonated_user_token: UserToken`,\n impersonated.username as impersonated_username,\n impersonated.display_name as impersonated_display_name,\n uir.is_redeemed as `is_redeemed: bool`,\n (uir.expires_at < NOW()) as `is_expired: bool`,\n uir.expires_at,\n uir.created_at,\n uir.updated_at\nFROM user_impersonation_requests uir\nJOIN users impersonator ON impersonator.token = uir.impersonator_user_token\nJOIN users impersonated ON impersonated.token = uir.impersonated_user_token\nWHERE uir.impersonated_user_token = ?\n AND uir.id < ?\nORDER BY uir.id DESC\nLIMIT ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
|
||||
"char_set": 63,
|
||||
"max_size": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "impersonator_user_token: UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "impersonator_username",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "impersonator_display_name",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "impersonated_user_token: UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "impersonated_username",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "impersonated_display_name",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "is_redeemed: bool",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "is_expired: bool",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "NOT_NULL | BINARY",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "expires_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | BINARY | TIMESTAMP",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "updated_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "27cebaa36f0ec873f86cdf8fa994d499db1a5cc84d2c6fceb3587145eb2f9bec"
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n uir.id,\n uir.impersonator_user_token as `impersonator_user_token: UserToken`,\n impersonator.username as impersonator_username,\n impersonator.display_name as impersonator_display_name,\n uir.impersonated_user_token as `impersonated_user_token: UserToken`,\n impersonated.username as impersonated_username,\n impersonated.display_name as impersonated_display_name,\n uir.is_redeemed as `is_redeemed: bool`,\n (uir.expires_at < NOW()) as `is_expired: bool`,\n uir.expires_at,\n uir.created_at,\n uir.updated_at\nFROM user_impersonation_requests uir\nJOIN users impersonator ON impersonator.token = uir.impersonator_user_token\nJOIN users impersonated ON impersonated.token = uir.impersonated_user_token\nWHERE uir.impersonated_user_token = ?\nORDER BY uir.id DESC\nLIMIT ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
|
||||
"char_set": 63,
|
||||
"max_size": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "impersonator_user_token: UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "impersonator_username",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "impersonator_display_name",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "impersonated_user_token: UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "impersonated_username",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "impersonated_display_name",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "is_redeemed: bool",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "is_expired: bool",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "NOT_NULL | BINARY",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "expires_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | BINARY | TIMESTAMP",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "updated_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "381e13ab37a2f4ce39bd6db9d90202c301091376108f645a9bb5264a9cd113d7"
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n sal.id,\n sal.token as `token: StaffAuditLogToken`,\n sal.audit_action,\n sal.maybe_entity_type,\n sal.maybe_entity_token,\n target_user.username as maybe_target_username,\n target_user.display_name as maybe_target_display_name,\n sal.staff_user_token as `staff_user_token: UserToken`,\n staff_user.username as staff_username,\n staff_user.display_name as staff_display_name,\n sal.staff_ip_address,\n sal.created_at\nFROM staff_audit_logs sal\nLEFT JOIN users target_user ON target_user.token = sal.maybe_entity_token\nLEFT JOIN users staff_user ON staff_user.token = sal.staff_user_token\nORDER BY sal.id DESC\nLIMIT ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
|
||||
"char_set": 63,
|
||||
"max_size": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "token: StaffAuditLogToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "audit_action",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "maybe_entity_type",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "maybe_entity_token",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "maybe_target_username",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "maybe_target_display_name",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "staff_user_token: UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "staff_username",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "staff_display_name",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "staff_ip_address",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 160
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "3846628765bd34ea2ce01e15a0db732121ce473c5c542d0e971ad29b2b421193"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE users\nSET\n is_banned = ?,\n maybe_mod_comments = ?,\n maybe_mod_user_token = ?,\n version = version + 1\n\nWHERE users.token = ?\nLIMIT 1\n ",
|
||||
"query": "\nUPDATE users\nSET\n is_banned = ?,\n maybe_mod_comments = ?,\n maybe_mod_user_token = ?,\n version = version + 1\n\nWHERE users.token = ?\nLIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -8,5 +8,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "eab69a8d679b2dcfb06a9ccc79851f6c693d0c372142ff0e7ded89c95e4bcf44"
|
||||
"hash": "3d746dcb479005f1f4ae5e157a5ace7626eda4849d44c6c2d7079fd346eb4edc"
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n uir.id,\n uir.impersonator_user_token as `impersonator_user_token: UserToken`,\n impersonator.username as impersonator_username,\n impersonator.display_name as impersonator_display_name,\n uir.impersonated_user_token as `impersonated_user_token: UserToken`,\n impersonated.username as impersonated_username,\n impersonated.display_name as impersonated_display_name,\n uir.is_redeemed as `is_redeemed: bool`,\n (uir.expires_at < NOW()) as `is_expired: bool`,\n uir.expires_at,\n uir.created_at,\n uir.updated_at\nFROM user_impersonation_requests uir\nJOIN users impersonator ON impersonator.token = uir.impersonator_user_token\nJOIN users impersonated ON impersonated.token = uir.impersonated_user_token\nWHERE uir.id < ?\nORDER BY uir.id DESC\nLIMIT ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
|
||||
"char_set": 63,
|
||||
"max_size": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "impersonator_user_token: UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "impersonator_username",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "impersonator_display_name",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "impersonated_user_token: UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "impersonated_username",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "impersonated_display_name",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "is_redeemed: bool",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "is_expired: bool",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "NOT_NULL | BINARY",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "expires_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | BINARY | TIMESTAMP",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "updated_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "5d45897a5d4c326c8d17f105cae81db7f5878c197b55e8ea61c225e739d11234"
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n sal.id,\n sal.token as `token: StaffAuditLogToken`,\n sal.audit_action,\n sal.maybe_entity_type,\n sal.maybe_entity_token,\n target_user.username as maybe_target_username,\n target_user.display_name as maybe_target_display_name,\n sal.staff_user_token as `staff_user_token: UserToken`,\n staff_user.username as staff_username,\n staff_user.display_name as staff_display_name,\n sal.staff_ip_address,\n sal.created_at\nFROM staff_audit_logs sal\nLEFT JOIN users target_user ON target_user.token = sal.maybe_entity_token\nLEFT JOIN users staff_user ON staff_user.token = sal.staff_user_token\nWHERE sal.id < ?\nORDER BY sal.id DESC\nLIMIT ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
|
||||
"char_set": 63,
|
||||
"max_size": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "token: StaffAuditLogToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "audit_action",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "maybe_entity_type",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "maybe_entity_token",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "maybe_target_username",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "maybe_target_display_name",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "staff_user_token: UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "staff_username",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "staff_display_name",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "staff_ip_address",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 160
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "9132f832752ea40736bedf4729c76af7fe019cc69ed5cc280fce87e68c3f4846"
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n uir.id,\n uir.impersonator_user_token as `impersonator_user_token: UserToken`,\n impersonator.username as impersonator_username,\n impersonator.display_name as impersonator_display_name,\n uir.impersonated_user_token as `impersonated_user_token: UserToken`,\n impersonated.username as impersonated_username,\n impersonated.display_name as impersonated_display_name,\n uir.is_redeemed as `is_redeemed: bool`,\n (uir.expires_at < NOW()) as `is_expired: bool`,\n uir.expires_at,\n uir.created_at,\n uir.updated_at\nFROM user_impersonation_requests uir\nJOIN users impersonator ON impersonator.token = uir.impersonator_user_token\nJOIN users impersonated ON impersonated.token = uir.impersonated_user_token\nORDER BY uir.id DESC\nLIMIT ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
|
||||
"char_set": 63,
|
||||
"max_size": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "impersonator_user_token: UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "impersonator_username",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "impersonator_display_name",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "impersonated_user_token: UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "impersonated_username",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "impersonated_display_name",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "is_redeemed: bool",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "is_expired: bool",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "NOT_NULL | BINARY",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "expires_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | BINARY | TIMESTAMP",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "updated_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "e72b3dfdd9df4ee10a696faa4dfc3fb8addc9d2b88017f503c339fb616bef6f1"
|
||||
}
|
||||
Generated
+13
@@ -770,6 +770,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"artcraft_api_defs",
|
||||
"artcraft_client",
|
||||
"enums",
|
||||
"fal_client",
|
||||
"log",
|
||||
"muapi_client",
|
||||
@@ -7750,6 +7751,17 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
||||
|
||||
[[package]]
|
||||
name = "opaque_cursors"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"magic-crypt",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "open"
|
||||
version = "5.3.3"
|
||||
@@ -11269,6 +11281,7 @@ dependencies = [
|
||||
"mimetypes",
|
||||
"mysql_queries",
|
||||
"once_cell",
|
||||
"opaque_cursors",
|
||||
"pager",
|
||||
"password",
|
||||
"primitives",
|
||||
|
||||
@@ -73,6 +73,7 @@ members = [
|
||||
"crates/lib/http_headers",
|
||||
"crates/lib/jobs_common",
|
||||
"crates/lib/jwt_light",
|
||||
"crates/lib/opaque_cursors",
|
||||
"crates/lib/jwt_signer",
|
||||
"crates/lib/logging",
|
||||
"crates/lib/markdown",
|
||||
@@ -252,6 +253,7 @@ images = { path = "crates/lib/files/images" }
|
||||
jobs_common = { path = "crates/lib/jobs_common" }
|
||||
jwt_light = { path = "crates/lib/jwt_light" }
|
||||
jwt_signer = { path = "crates/lib/jwt_signer" }
|
||||
opaque_cursors = { path = "crates/lib/opaque_cursors" }
|
||||
memory_caching = { path = "crates/lib/caching/memory_caching" }
|
||||
memory_store = { path = "crates/lib/caching/memory_store" }
|
||||
midjourney_client = { path = "crates/api_clients/midjourney_client" }
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
-- Recreate with the original signed BIGINT id.
|
||||
CREATE TABLE user_impersonation_requests (
|
||||
|
||||
id BIGINT(20) NOT NULL AUTO_INCREMENT,
|
||||
|
||||
token VARCHAR(32) NOT NULL,
|
||||
|
||||
impersonated_user_token VARCHAR(32) NOT NULL,
|
||||
|
||||
impersonator_user_token VARCHAR(32) NOT NULL,
|
||||
|
||||
user_impersonation_token VARCHAR(32) NOT NULL,
|
||||
|
||||
is_redeemed BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
|
||||
ip_address_creation VARCHAR(40) NOT NULL,
|
||||
|
||||
ip_address_redemption VARCHAR(40) DEFAULT NULL,
|
||||
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY (token),
|
||||
UNIQUE KEY (user_impersonation_token),
|
||||
KEY index_impersonated_user_token (impersonated_user_token),
|
||||
KEY index_impersonator_user_token (impersonator_user_token)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
DROP TABLE IF EXISTS user_impersonation_requests;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
DROP TABLE IF EXISTS user_impersonation_requests;
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
CREATE TABLE user_impersonation_requests (
|
||||
|
||||
-- Not used for anything except replication.
|
||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
|
||||
-- Internal "primary key"
|
||||
token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- The user being impersonated.
|
||||
impersonated_user_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- The staff user who initiated the impersonation.
|
||||
impersonator_user_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- The actual secret that must be presented to redeem the impersonation.
|
||||
user_impersonation_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- Whether this request has been redeemed (used to create a session).
|
||||
is_redeemed BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
|
||||
-- IP address of the staff user who created the request.
|
||||
ip_address_creation VARCHAR(40) NOT NULL,
|
||||
|
||||
-- IP address from which the request was redeemed, if redeemed.
|
||||
ip_address_redemption VARCHAR(40) DEFAULT NULL,
|
||||
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
-- The request expires after this time if not redeemed.
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
|
||||
-- INDICES --
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY (token),
|
||||
UNIQUE KEY (user_impersonation_token),
|
||||
KEY index_impersonated_user_token (impersonated_user_token),
|
||||
KEY index_impersonator_user_token (impersonator_user_token)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
@@ -0,0 +1,31 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
-- Recreate with the original signed BIGINT id.
|
||||
CREATE TABLE staff_audit_logs (
|
||||
|
||||
id BIGINT(20) NOT NULL AUTO_INCREMENT,
|
||||
|
||||
token VARCHAR(32) NOT NULL,
|
||||
|
||||
audit_action VARCHAR(32) NOT NULL,
|
||||
|
||||
maybe_entity_type VARCHAR(16) DEFAULT NULL,
|
||||
|
||||
maybe_entity_token VARCHAR(32) NOT NULL,
|
||||
|
||||
staff_user_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
staff_ip_address VARCHAR(40) NOT NULL,
|
||||
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY (token),
|
||||
KEY index_maybe_entity_type (maybe_entity_type),
|
||||
KEY index_maybe_entity_token (maybe_entity_token),
|
||||
KEY index_staff_user_token (staff_user_token),
|
||||
KEY index_created_at (created_at)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
DROP TABLE IF EXISTS staff_audit_logs;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
DROP TABLE IF EXISTS staff_audit_logs;
|
||||
@@ -0,0 +1,39 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
CREATE TABLE staff_audit_logs (
|
||||
|
||||
-- Not used for anything except replication.
|
||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
|
||||
-- Effective "primary key" of the audit log item (in case we add a UI or notes)
|
||||
token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- The type of action taken
|
||||
audit_action VARCHAR(32) NOT NULL,
|
||||
|
||||
-- The type of entity acted upon (if any)
|
||||
maybe_entity_type VARCHAR(16) DEFAULT NULL,
|
||||
|
||||
-- The token of the entity mutated (if any)
|
||||
maybe_entity_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- The staff user who performed the action.
|
||||
staff_user_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- For abuse tracking.
|
||||
-- Wide enough for IPv4/6
|
||||
staff_ip_address VARCHAR(40) NOT NULL,
|
||||
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- INDICES --
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY (token),
|
||||
KEY index_maybe_entity_type (maybe_entity_type),
|
||||
KEY index_maybe_entity_token (maybe_entity_token),
|
||||
KEY index_staff_user_token (staff_user_token),
|
||||
KEY index_created_at (created_at)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
@@ -0,0 +1,38 @@
|
||||
# SQL Migrations
|
||||
|
||||
Incremental schema changes applied in order by timestamp prefix.
|
||||
|
||||
## Naming Convention
|
||||
|
||||
Migration directories follow the format:
|
||||
|
||||
```
|
||||
YYYY-MM-DD-HHMMSS-NNNN_description_goes_here/
|
||||
up.sql
|
||||
down.sql
|
||||
```
|
||||
|
||||
These are created with the `diesel` tool, and in particular:
|
||||
|
||||
> `diesel migration generate description_goes_here`
|
||||
|
||||
- `NNNN` is a zero-padded sequence number for ordering multiple migrations on the same timestamp.
|
||||
- `up.sql` applies the migration forward.
|
||||
- `down.sql` reverses it.
|
||||
|
||||
## Table Conventions
|
||||
|
||||
- Primary key `id` columns must be `BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT`. Do not use signed IDs.
|
||||
- Always create tables with `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin`.
|
||||
- All SQL files should start with the standard IntelliJ noinspection comments:
|
||||
```sql
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
- Every `up.sql` must have a corresponding `down.sql` that cleanly reverses the migration.
|
||||
- For `CREATE TABLE` migrations, the `down.sql` is typically `DROP TABLE IF EXISTS table_name;`.
|
||||
- For `ALTER TABLE` migrations, the `down.sql` reverses the alteration (e.g. `DROP COLUMN`).
|
||||
@@ -0,0 +1,26 @@
|
||||
# Squashed / Materialized Schemas
|
||||
|
||||
Fully materialized table definitions representing the current state of each table
|
||||
after all migrations have been applied. These are not executed — they exist as
|
||||
human-readable documentation of the current schema.
|
||||
|
||||
## When to Update
|
||||
|
||||
Update the corresponding squashed file whenever a migration changes a table's schema
|
||||
(adding columns, changing types, dropping and recreating, etc.).
|
||||
|
||||
## Table Conventions
|
||||
|
||||
- Primary key `id` columns must be `BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT`. Do not use signed IDs.
|
||||
- Always create tables with `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin`.
|
||||
- All SQL files should start with the standard IntelliJ noinspection comments:
|
||||
```sql
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
```
|
||||
|
||||
## File Organization
|
||||
|
||||
Files are named after their table (e.g. `users.sql`, `media_files.sql`).
|
||||
Some tables are grouped into subdirectories (e.g. `user_tables/`).
|
||||
@@ -5,7 +5,7 @@
|
||||
CREATE TABLE staff_audit_logs (
|
||||
|
||||
-- Not used for anything except replication.
|
||||
id BIGINT(20) NOT NULL AUTO_INCREMENT,
|
||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
|
||||
-- Effective "primary key" of the audit log item (in case we add a UI or notes)
|
||||
token VARCHAR(32) NOT NULL,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
CREATE TABLE user_impersonation_requests (
|
||||
|
||||
-- Not used for anything except replication.
|
||||
id BIGINT(20) NOT NULL AUTO_INCREMENT,
|
||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
|
||||
-- Internal "primary key"
|
||||
token VARCHAR(32) NOT NULL,
|
||||
|
||||
+2
-1
@@ -16,7 +16,8 @@ pub struct GenerateVeo2ImageToVideoRequest {
|
||||
|
||||
/// Optional text prompt.
|
||||
pub prompt: Option<String>,
|
||||
|
||||
|
||||
/// **NB(bt): ASPECT RATIO IS ACTUALLY NOT USED IN VEO 2 IMAGE-TO-VIDEO**
|
||||
/// Optional: aspect ratio of the generated video.
|
||||
pub aspect_ratio: Option<GenerateVeo2AspectRatio>,
|
||||
|
||||
|
||||
+11
@@ -21,6 +21,9 @@ pub struct GenerateVeo3FastImageToVideoRequest {
|
||||
pub resolution: Option<GenerateVeo3FastResolution>,
|
||||
|
||||
/// Optional: aspect ratio of the generated video.
|
||||
pub aspect_ratio: Option<GenerateVeo3FastAspectRatio>,
|
||||
|
||||
/// Optional: duration of the generated video.
|
||||
pub duration: Option<GenerateVeo3FastDuration>,
|
||||
|
||||
/// Optional: Generate an audio track along with the video
|
||||
@@ -34,6 +37,14 @@ pub enum GenerateVeo3FastResolution {
|
||||
TenEightyP, // 1080p
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateVeo3FastAspectRatio {
|
||||
Auto,
|
||||
SixteenByNine,
|
||||
NineBySixteen,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GenerateVeo3FastDuration {
|
||||
|
||||
+4
@@ -42,6 +42,10 @@ pub struct GenerateVeo3ImageToVideoRequest {
|
||||
pub enum GenerateVeo3AspectRatio {
|
||||
WideSixteenNine, // 16:9
|
||||
TallNineSixteen, // 9:16
|
||||
|
||||
/// NB: DO NOT REMOVE. This is an unsupported resolution for Veo 3, but we can't
|
||||
/// impact existing callers by removing it. The handler will convert this to a
|
||||
/// supported resolution or 400 the request instead.
|
||||
Square, // 1:1
|
||||
}
|
||||
|
||||
|
||||
+4
@@ -120,6 +120,9 @@ pub struct OmniGenImageModelDetails {
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub batch_size_default: Option<u16>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub is_disabled: Option<bool>,
|
||||
}
|
||||
|
||||
impl Default for OmniGenImageModelDetails {
|
||||
@@ -145,6 +148,7 @@ impl Default for OmniGenImageModelDetails {
|
||||
batch_size_max: None,
|
||||
batch_size_options: None,
|
||||
batch_size_default: None,
|
||||
is_disabled: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -163,6 +163,9 @@ pub struct OmniGenVideoModelDetails {
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub batch_size_default: Option<u16>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub is_disabled: Option<bool>,
|
||||
}
|
||||
|
||||
impl Default for OmniGenVideoModelDetails {
|
||||
@@ -202,6 +205,7 @@ impl Default for OmniGenVideoModelDetails {
|
||||
batch_size_max: None,
|
||||
batch_size_options: None,
|
||||
batch_size_default: None,
|
||||
is_disabled: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ pub mod analytics;
|
||||
pub mod credits;
|
||||
pub mod generate;
|
||||
pub mod jobs;
|
||||
pub mod omni_gen;
|
||||
pub mod media_files;
|
||||
pub mod prompts;
|
||||
pub mod stripe_artcraft;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pub mod video;
|
||||
+1
@@ -0,0 +1 @@
|
||||
pub mod omni_gen_video;
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use crate::error::storyteller_error::StorytellerError;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
use crate::utils::basic_json_post_request::basic_json_post_request;
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
use artcraft_api_defs::omni_gen::generate_response::omni_gen_video_generate_response::OmniGenVideoGenerateResponse;
|
||||
|
||||
pub const OMNI_GEN_VIDEO_GENERATE_PATH: &str = "/v1/omni_gen/generate/video";
|
||||
|
||||
pub async fn omni_gen_video_generate(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: OmniGenVideoCostAndGenerateRequest,
|
||||
) -> Result<OmniGenVideoGenerateResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
OMNI_GEN_VIDEO_GENERATE_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod generate;
|
||||
@@ -17,6 +17,7 @@ workspace-build-acceleration.workspace = true
|
||||
|
||||
# Internal
|
||||
artcraft_api_defs.workspace = true
|
||||
enums.workspace = true
|
||||
artcraft_client.workspace = true
|
||||
fal_client.workspace = true
|
||||
muapi_client.workspace = true
|
||||
|
||||
+57
-16
@@ -6,31 +6,72 @@ use crate::generate::generate_video::generate_video_response::{
|
||||
};
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_veo_2::PlanArtcraftVeo2;
|
||||
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2ImageToVideoRequest;
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
use artcraft_client::endpoints::generate::video::generate_veo_2_image_to_video::generate_veo_2_image_to_video;
|
||||
use artcraft_client::endpoints::omni_gen::generate::video::omni_gen_video::omni_gen_video_generate;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
|
||||
pub async fn execute_artcraft_veo_2(
|
||||
plan: &PlanArtcraftVeo2<'_>,
|
||||
artcraft_client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
let request = GenerateVeo2ImageToVideoRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
media_file_token: Some(plan.start_frame.to_owned()),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
aspect_ratio: plan.aspect_ratio.clone(),
|
||||
duration: plan.duration.clone(),
|
||||
let inference_job_token = match plan.start_frame {
|
||||
Some(media_file_token) => {
|
||||
// Image-to-video: use the legacy dedicated endpoint.
|
||||
let request = GenerateVeo2ImageToVideoRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
media_file_token: Some(media_file_token.to_owned()),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
aspect_ratio: plan.aspect_ratio.clone(),
|
||||
duration: plan.duration.clone(),
|
||||
};
|
||||
|
||||
let response = generate_veo_2_image_to_video(
|
||||
&artcraft_client.api_host,
|
||||
Some(&artcraft_client.credentials),
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
|
||||
response.inference_job_token
|
||||
}
|
||||
None => {
|
||||
// Text-to-video: use the omni-gen unified endpoint.
|
||||
let request = OmniGenVideoCostAndGenerateRequest {
|
||||
idempotency_token: Some(plan.idempotency_token.clone()),
|
||||
model: Some(CommonVideoModel::Veo2),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
negative_prompt: None,
|
||||
start_frame_image_media_token: None,
|
||||
end_frame_image_media_token: None,
|
||||
reference_image_media_tokens: None,
|
||||
reference_video_media_tokens: None,
|
||||
reference_audio_media_tokens: None,
|
||||
reference_character_tokens: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
duration_seconds: None,
|
||||
video_batch_count: None,
|
||||
generate_audio: None,
|
||||
};
|
||||
|
||||
let response = omni_gen_video_generate(
|
||||
&artcraft_client.api_host,
|
||||
Some(&artcraft_client.credentials),
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
|
||||
response.inference_job_token
|
||||
}
|
||||
};
|
||||
|
||||
let response = generate_veo_2_image_to_video(
|
||||
&artcraft_client.api_host,
|
||||
Some(&artcraft_client.credentials),
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
|
||||
let all_tokens = vec![response.inference_job_token.clone()];
|
||||
let all_tokens = vec![inference_job_token.clone()];
|
||||
Ok(GenerateVideoResponse::Artcraft(ArtcraftVideoResponsePayload {
|
||||
inference_job_token: response.inference_job_token,
|
||||
inference_job_token,
|
||||
all_inference_job_tokens: all_tokens,
|
||||
}))
|
||||
}
|
||||
|
||||
+59
-18
@@ -6,33 +6,74 @@ use crate::generate::generate_video::generate_video_response::{
|
||||
};
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_veo_3::PlanArtcraftVeo3;
|
||||
use artcraft_api_defs::generate::video::generate_veo_3_image_to_video::GenerateVeo3ImageToVideoRequest;
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
use artcraft_client::endpoints::generate::video::generate_veo_3_image_to_video::generate_veo_3_image_to_video;
|
||||
use artcraft_client::endpoints::omni_gen::generate::video::omni_gen_video::omni_gen_video_generate;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
|
||||
pub async fn execute_artcraft_veo_3(
|
||||
plan: &PlanArtcraftVeo3<'_>,
|
||||
artcraft_client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
let request = GenerateVeo3ImageToVideoRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
media_file_token: Some(plan.start_frame.to_owned()),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
aspect_ratio: plan.aspect_ratio,
|
||||
resolution: plan.resolution,
|
||||
duration: plan.duration,
|
||||
generate_audio: plan.generate_audio,
|
||||
let inference_job_token = match plan.start_frame {
|
||||
Some(media_file_token) => {
|
||||
// Image-to-video: use the legacy dedicated endpoint.
|
||||
let request = GenerateVeo3ImageToVideoRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
media_file_token: Some(media_file_token.to_owned()),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
aspect_ratio: plan.aspect_ratio,
|
||||
resolution: plan.resolution,
|
||||
duration: plan.duration,
|
||||
generate_audio: plan.generate_audio,
|
||||
};
|
||||
|
||||
let response = generate_veo_3_image_to_video(
|
||||
&artcraft_client.api_host,
|
||||
Some(&artcraft_client.credentials),
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
|
||||
response.inference_job_token
|
||||
}
|
||||
None => {
|
||||
// Text-to-video: use the omni-gen unified endpoint.
|
||||
let request = OmniGenVideoCostAndGenerateRequest {
|
||||
idempotency_token: Some(plan.idempotency_token.clone()),
|
||||
model: Some(CommonVideoModel::Veo3),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
negative_prompt: None,
|
||||
start_frame_image_media_token: None,
|
||||
end_frame_image_media_token: None,
|
||||
reference_image_media_tokens: None,
|
||||
reference_video_media_tokens: None,
|
||||
reference_audio_media_tokens: None,
|
||||
reference_character_tokens: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
duration_seconds: None,
|
||||
video_batch_count: None,
|
||||
generate_audio: plan.generate_audio,
|
||||
};
|
||||
|
||||
let response = omni_gen_video_generate(
|
||||
&artcraft_client.api_host,
|
||||
Some(&artcraft_client.credentials),
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
|
||||
response.inference_job_token
|
||||
}
|
||||
};
|
||||
|
||||
let response = generate_veo_3_image_to_video(
|
||||
&artcraft_client.api_host,
|
||||
Some(&artcraft_client.credentials),
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
|
||||
let all_tokens = vec![response.inference_job_token.clone()];
|
||||
let all_tokens = vec![inference_job_token.clone()];
|
||||
Ok(GenerateVideoResponse::Artcraft(ArtcraftVideoResponsePayload {
|
||||
inference_job_token: response.inference_job_token,
|
||||
inference_job_token,
|
||||
all_inference_job_tokens: all_tokens,
|
||||
}))
|
||||
}
|
||||
|
||||
+2
-1
@@ -14,8 +14,9 @@ pub async fn execute_artcraft_veo_3_fast(
|
||||
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
let request = GenerateVeo3FastImageToVideoRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
media_file_token: Some(plan.start_frame.to_owned()),
|
||||
media_file_token: plan.start_frame.map(|t| t.to_owned()),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
aspect_ratio: plan.aspect_ratio,
|
||||
resolution: plan.resolution,
|
||||
duration: plan.duration,
|
||||
generate_audio: plan.generate_audio,
|
||||
|
||||
+35
-13
@@ -4,28 +4,50 @@ use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_video::generate_video_response::{
|
||||
FalVideoResponsePayload, GenerateVideoResponse,
|
||||
};
|
||||
use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_2::PlanFalVeo2;
|
||||
use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_2::{
|
||||
FalVeo2Mode, PlanFalVeo2,
|
||||
};
|
||||
use fal_client::requests::webhook::video::image::enqueue_veo_2_image_to_video_webhook::{
|
||||
enqueue_veo_2_image_to_video_webhook, Veo2Args,
|
||||
enqueue_veo_2_image_to_video_webhook, Veo2Args, Veo2AspectRatio,
|
||||
};
|
||||
use fal_client::requests::webhook::video::text::enqueue_veo_2_text_to_video_webhook::{
|
||||
enqueue_veo_2_text_to_video_webhook, Veo2TextToVideoArgs,
|
||||
};
|
||||
|
||||
pub async fn execute_fal_veo_2(
|
||||
plan: &PlanFalVeo2,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
let args = Veo2Args {
|
||||
image_url: plan.start_frame_url.as_str(),
|
||||
prompt: plan.prompt.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
duration: plan.duration,
|
||||
aspect_ratio: plan.aspect_ratio,
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
let webhook_response = match &plan.mode {
|
||||
FalVeo2Mode::TextToVideo => {
|
||||
let args = Veo2TextToVideoArgs {
|
||||
prompt: plan.prompt.as_str(),
|
||||
negative_prompt: plan.negative_prompt.as_deref(),
|
||||
api_key: &fal_client.api_key,
|
||||
duration: plan.duration,
|
||||
aspect_ratio: plan.aspect_ratio.unwrap_or(Veo2AspectRatio::Auto),
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
};
|
||||
enqueue_veo_2_text_to_video_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
}
|
||||
FalVeo2Mode::ImageToVideo { image_url } => {
|
||||
// Image-to-video does not support aspect_ratio — the output
|
||||
// inherits the source image's aspect ratio.
|
||||
let args = Veo2Args {
|
||||
image_url: image_url.as_str(),
|
||||
prompt: plan.prompt.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
duration: plan.duration,
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
};
|
||||
enqueue_veo_2_image_to_video_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
}
|
||||
};
|
||||
|
||||
let webhook_response = enqueue_veo_2_image_to_video_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
|
||||
|
||||
Ok(GenerateVideoResponse::Fal(FalVideoResponsePayload {
|
||||
request_id: webhook_response.request_id,
|
||||
gateway_request_id: webhook_response.gateway_request_id,
|
||||
|
||||
+92
-16
@@ -4,30 +4,106 @@ use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_video::generate_video_response::{
|
||||
FalVideoResponsePayload, GenerateVideoResponse,
|
||||
};
|
||||
use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_3::PlanFalVeo3;
|
||||
use fal_client::requests::webhook::video::image::enqueue_veo_3_image_to_video_webhook::{
|
||||
enqueue_veo_3_image_to_video_webhook, Veo3Args,
|
||||
use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_3::{
|
||||
FalVeo3Duration, FalVeo3I2vAspectRatio, FalVeo3Mode, FalVeo3Resolution,
|
||||
FalVeo3T2vAspectRatio, PlanFalVeo3,
|
||||
};
|
||||
use fal_client::requests::webhook::video::image::enqueue_veo_3_image_to_video_webhook::{
|
||||
enqueue_veo_3_image_to_video_webhook, Veo3Args, Veo3I2vAspectRatio, Veo3I2vDuration,
|
||||
Veo3I2vResolution,
|
||||
};
|
||||
use fal_client::requests::webhook::video::text::enqueue_veo_3_text_to_video_webhook::{
|
||||
enqueue_veo_3_text_to_video_webhook, Veo3T2vAspectRatio, Veo3T2vDuration, Veo3T2vResolution,
|
||||
Veo3TextToVideoArgs,
|
||||
};
|
||||
|
||||
fn to_i2v_duration(d: FalVeo3Duration) -> Veo3I2vDuration {
|
||||
match d {
|
||||
FalVeo3Duration::Default => Veo3I2vDuration::Default,
|
||||
FalVeo3Duration::FourSeconds => Veo3I2vDuration::FourSeconds,
|
||||
FalVeo3Duration::SixSeconds => Veo3I2vDuration::SixSeconds,
|
||||
FalVeo3Duration::EightSeconds => Veo3I2vDuration::EightSeconds,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_t2v_duration(d: FalVeo3Duration) -> Veo3T2vDuration {
|
||||
match d {
|
||||
FalVeo3Duration::Default => Veo3T2vDuration::Default,
|
||||
FalVeo3Duration::FourSeconds => Veo3T2vDuration::FourSeconds,
|
||||
FalVeo3Duration::SixSeconds => Veo3T2vDuration::SixSeconds,
|
||||
FalVeo3Duration::EightSeconds => Veo3T2vDuration::EightSeconds,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_i2v_resolution(r: FalVeo3Resolution) -> Veo3I2vResolution {
|
||||
match r {
|
||||
FalVeo3Resolution::Default => Veo3I2vResolution::Default,
|
||||
FalVeo3Resolution::SevenTwentyP => Veo3I2vResolution::SevenTwentyP,
|
||||
FalVeo3Resolution::TenEightyP => Veo3I2vResolution::TenEightyP,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_t2v_resolution(r: FalVeo3Resolution) -> Veo3T2vResolution {
|
||||
match r {
|
||||
FalVeo3Resolution::Default => Veo3T2vResolution::Default,
|
||||
FalVeo3Resolution::SevenTwentyP => Veo3T2vResolution::SevenTwentyP,
|
||||
FalVeo3Resolution::TenEightyP => Veo3T2vResolution::TenEightyP,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_t2v_aspect_ratio(ar: FalVeo3T2vAspectRatio) -> Veo3T2vAspectRatio {
|
||||
match ar {
|
||||
FalVeo3T2vAspectRatio::Default => Veo3T2vAspectRatio::Default,
|
||||
FalVeo3T2vAspectRatio::WideSixteenNine => Veo3T2vAspectRatio::WideSixteenNine,
|
||||
FalVeo3T2vAspectRatio::TallNineSixteen => Veo3T2vAspectRatio::TallNineSixteen,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_i2v_aspect_ratio(ar: FalVeo3I2vAspectRatio) -> Veo3I2vAspectRatio {
|
||||
match ar {
|
||||
FalVeo3I2vAspectRatio::Auto => Veo3I2vAspectRatio::Auto,
|
||||
FalVeo3I2vAspectRatio::WideSixteenNine => Veo3I2vAspectRatio::WideSixteenNine,
|
||||
FalVeo3I2vAspectRatio::TallNineSixteen => Veo3I2vAspectRatio::TallNineSixteen,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn execute_fal_veo_3(
|
||||
plan: &PlanFalVeo3,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
let args = Veo3Args {
|
||||
image_url: plan.start_frame_url.as_str(),
|
||||
prompt: plan.prompt.as_str(),
|
||||
duration: plan.duration,
|
||||
aspect_ratio: plan.aspect_ratio,
|
||||
resolution: plan.resolution,
|
||||
generate_audio: plan.generate_audio,
|
||||
api_key: &fal_client.api_key,
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
let webhook_response = match &plan.mode {
|
||||
FalVeo3Mode::TextToVideo => {
|
||||
let args = Veo3TextToVideoArgs {
|
||||
prompt: plan.prompt.as_str(),
|
||||
negative_prompt: plan.negative_prompt.as_deref(),
|
||||
api_key: &fal_client.api_key,
|
||||
duration: to_t2v_duration(plan.duration),
|
||||
aspect_ratio: plan.t2v_aspect_ratio.map(to_t2v_aspect_ratio).unwrap_or(Veo3T2vAspectRatio::Default),
|
||||
resolution: to_t2v_resolution(plan.resolution),
|
||||
generate_audio: plan.generate_audio,
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
};
|
||||
enqueue_veo_3_text_to_video_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
}
|
||||
FalVeo3Mode::ImageToVideo { image_url } => {
|
||||
let args = Veo3Args {
|
||||
image_url: image_url.as_str(),
|
||||
prompt: plan.prompt.as_str(),
|
||||
duration: to_i2v_duration(plan.duration),
|
||||
aspect_ratio: plan.i2v_aspect_ratio.map(to_i2v_aspect_ratio).unwrap_or(Veo3I2vAspectRatio::Auto),
|
||||
resolution: to_i2v_resolution(plan.resolution),
|
||||
generate_audio: plan.generate_audio,
|
||||
api_key: &fal_client.api_key,
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
};
|
||||
enqueue_veo_3_image_to_video_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
}
|
||||
};
|
||||
|
||||
let webhook_response = enqueue_veo_3_image_to_video_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
|
||||
|
||||
Ok(GenerateVideoResponse::Fal(FalVideoResponsePayload {
|
||||
request_id: webhook_response.request_id,
|
||||
gateway_request_id: webhook_response.gateway_request_id,
|
||||
|
||||
+1
@@ -16,6 +16,7 @@ pub async fn execute_fal_veo_3_fast(
|
||||
let args = Veo3FastArgs {
|
||||
prompt: plan.prompt.as_str(),
|
||||
image_url: plan.start_frame_url.as_str(),
|
||||
aspect_ratio: plan.aspect_ratio,
|
||||
duration: plan.duration,
|
||||
api_key: &fal_client.api_key,
|
||||
resolution: plan.resolution,
|
||||
|
||||
+19
-12
@@ -13,7 +13,10 @@ use tokens::tokens::media_files::MediaFileToken;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanArtcraftVeo2<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
pub start_frame: &'a MediaFileToken,
|
||||
/// Required for execution. May be None on the cost path when the omni-gen
|
||||
/// distillation hydrated the media token to a URL (which the Artcraft
|
||||
/// provider can't use but cost estimation doesn't need).
|
||||
pub start_frame: Option<&'a MediaFileToken>,
|
||||
pub aspect_ratio: Option<GenerateVeo2AspectRatio>,
|
||||
pub duration: Option<GenerateVeo2Duration>,
|
||||
pub idempotency_token: String,
|
||||
@@ -25,16 +28,13 @@ pub fn plan_generate_video_artcraft_veo_2<'a>(
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
let start_frame = match request.start_frame {
|
||||
Some(ImageRef::MediaFileToken(t)) => t,
|
||||
Some(ImageRef::Url(_)) => {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
}
|
||||
None => {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "start_frame",
|
||||
value: "Veo 2 requires a starting frame".to_string(),
|
||||
}))
|
||||
}
|
||||
Some(ImageRef::MediaFileToken(t)) => Some(t),
|
||||
// Omni-gen distillation hydrates media tokens to URLs before running the
|
||||
// Artcraft cost path. Cost only depends on duration, so URL-form refs are
|
||||
// accepted and dropped.
|
||||
Some(ImageRef::Url(_)) => None,
|
||||
// No start_frame = text-to-video mode.
|
||||
None => None,
|
||||
};
|
||||
|
||||
if request.end_frame.is_some() {
|
||||
@@ -44,7 +44,14 @@ pub fn plan_generate_video_artcraft_veo_2<'a>(
|
||||
}));
|
||||
}
|
||||
|
||||
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
// Aspect ratio only applies to text-to-video; image-to-video inherits
|
||||
// the source frame's aspect ratio.
|
||||
let is_image_mode = request.start_frame.is_some();
|
||||
let aspect_ratio = if is_image_mode {
|
||||
None
|
||||
} else {
|
||||
plan_aspect_ratio(request.aspect_ratio, strategy)?
|
||||
};
|
||||
let duration = plan_duration(request.duration_seconds, strategy)?;
|
||||
|
||||
Ok(VideoGenerationPlan::ArtcraftVeo2(PlanArtcraftVeo2 {
|
||||
|
||||
+40
-17
@@ -14,10 +14,16 @@ use tokens::tokens::media_files::MediaFileToken;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanArtcraftVeo3<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
pub start_frame: &'a MediaFileToken,
|
||||
/// Required for image-to-video. None for text-to-video or when the
|
||||
/// omni-gen distillation hydrated the token to a URL.
|
||||
pub start_frame: Option<&'a MediaFileToken>,
|
||||
pub aspect_ratio: Option<GenerateVeo3AspectRatio>,
|
||||
pub resolution: Option<GenerateVeo3Resolution>,
|
||||
pub duration: Option<GenerateVeo3Duration>,
|
||||
/// The raw requested duration in seconds, preserved for cost estimation.
|
||||
/// The `duration` field above maps everything to `EightSeconds` for the
|
||||
/// legacy endpoint, but cost should reflect the actual requested value.
|
||||
pub duration_seconds_raw: Option<u16>,
|
||||
pub generate_audio: Option<bool>,
|
||||
pub idempotency_token: String,
|
||||
}
|
||||
@@ -28,16 +34,13 @@ pub fn plan_generate_video_artcraft_veo_3<'a>(
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
let start_frame = match request.start_frame {
|
||||
Some(ImageRef::MediaFileToken(t)) => t,
|
||||
Some(ImageRef::Url(_)) => {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
}
|
||||
None => {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "start_frame",
|
||||
value: "Veo 3 requires a starting frame".to_string(),
|
||||
}))
|
||||
}
|
||||
Some(ImageRef::MediaFileToken(t)) => Some(t),
|
||||
// Omni-gen distillation hydrates media tokens to URLs before running the
|
||||
// Artcraft cost path. Cost only depends on duration/audio, so URL-form
|
||||
// refs are accepted and dropped.
|
||||
Some(ImageRef::Url(_)) => None,
|
||||
// No start_frame = text-to-video mode.
|
||||
None => None,
|
||||
};
|
||||
|
||||
if request.end_frame.is_some() {
|
||||
@@ -47,7 +50,14 @@ pub fn plan_generate_video_artcraft_veo_3<'a>(
|
||||
}));
|
||||
}
|
||||
|
||||
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
// Aspect ratio only applies to text-to-video; image-to-video inherits
|
||||
// the source frame's aspect ratio.
|
||||
let is_image_mode = request.start_frame.is_some();
|
||||
let aspect_ratio = if is_image_mode {
|
||||
None
|
||||
} else {
|
||||
plan_aspect_ratio(request.aspect_ratio, strategy)?
|
||||
};
|
||||
let resolution = plan_resolution(request.resolution, strategy)?;
|
||||
let duration = plan_duration(request.duration_seconds, strategy)?;
|
||||
|
||||
@@ -57,6 +67,7 @@ pub fn plan_generate_video_artcraft_veo_3<'a>(
|
||||
aspect_ratio,
|
||||
resolution,
|
||||
duration,
|
||||
duration_seconds_raw: request.duration_seconds,
|
||||
generate_audio: request.generate_audio,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
}))
|
||||
@@ -112,14 +123,19 @@ fn plan_resolution(
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy storyteller endpoint for Veo 3 only exposes EightSeconds.
|
||||
// The legacy storyteller endpoint for Veo 3 only exposes EightSeconds, but
|
||||
// the artcraft plan stores the raw duration_seconds from the omni request so
|
||||
// that cost estimation works for all supported durations (4/6/8).
|
||||
fn plan_duration(
|
||||
duration_seconds: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<GenerateVeo3Duration>, ArtcraftRouterError> {
|
||||
match duration_seconds {
|
||||
None => Ok(None),
|
||||
Some(8) => Ok(Some(GenerateVeo3Duration::EightSeconds)),
|
||||
// The api_defs enum only has EightSeconds; all durations map to it for
|
||||
// the legacy endpoint. The actual seconds are tracked via
|
||||
// `duration_seconds_raw` below for cost estimation.
|
||||
Some(4) | Some(6) | Some(8) => Ok(Some(GenerateVeo3Duration::EightSeconds)),
|
||||
Some(other) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
@@ -127,18 +143,25 @@ fn plan_duration(
|
||||
value: format!("{}", other),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Some(GenerateVeo3Duration::EightSeconds)),
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(Some(GenerateVeo3Duration::EightSeconds)),
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Some(GenerateVeo3Duration::EightSeconds)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
impl PlanArtcraftVeo3<'_> {
|
||||
/// Mirrors the Fal client default (8s) used by the legacy generate handler.
|
||||
/// Duration in seconds for cost estimation. Uses the raw requested
|
||||
/// duration, clamped to {4, 6, 8}, defaulting to 8s.
|
||||
pub fn duration_seconds_for_cost(&self) -> u64 {
|
||||
8
|
||||
match self.duration_seconds_raw {
|
||||
Some(s) if s <= 4 => 4,
|
||||
Some(s) if s <= 6 => 6,
|
||||
_ => 8, // None, 7, 8, or above → 8s
|
||||
}
|
||||
}
|
||||
|
||||
/// Legacy Veo 3 generate handler defaults `generate_audio` to false.
|
||||
/// The omni path defaults to true (matching the fal plan builder).
|
||||
pub fn generate_audio_for_cost(&self) -> bool {
|
||||
self.generate_audio.unwrap_or(false)
|
||||
}
|
||||
|
||||
+36
-12
@@ -1,3 +1,4 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
@@ -6,14 +7,15 @@ use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use artcraft_api_defs::generate::video::generate_veo_3_fast_image_to_video::{
|
||||
GenerateVeo3FastDuration, GenerateVeo3FastResolution,
|
||||
GenerateVeo3FastAspectRatio, GenerateVeo3FastDuration, GenerateVeo3FastResolution,
|
||||
};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanArtcraftVeo3Fast<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
pub start_frame: &'a MediaFileToken,
|
||||
pub start_frame: Option<&'a MediaFileToken>,
|
||||
pub aspect_ratio: Option<GenerateVeo3FastAspectRatio>,
|
||||
pub resolution: Option<GenerateVeo3FastResolution>,
|
||||
pub duration: Option<GenerateVeo3FastDuration>,
|
||||
pub generate_audio: Option<bool>,
|
||||
@@ -26,16 +28,9 @@ pub fn plan_generate_video_artcraft_veo_3_fast<'a>(
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
let start_frame = match request.start_frame {
|
||||
Some(ImageRef::MediaFileToken(t)) => t,
|
||||
Some(ImageRef::Url(_)) => {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
}
|
||||
None => {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "start_frame",
|
||||
value: "Veo 3 Fast requires a starting frame".to_string(),
|
||||
}))
|
||||
}
|
||||
Some(ImageRef::MediaFileToken(t)) => Some(t),
|
||||
Some(ImageRef::Url(_)) => None,
|
||||
None => None,
|
||||
};
|
||||
|
||||
if request.end_frame.is_some() {
|
||||
@@ -45,12 +40,14 @@ pub fn plan_generate_video_artcraft_veo_3_fast<'a>(
|
||||
}));
|
||||
}
|
||||
|
||||
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let resolution = plan_resolution(request.resolution, strategy)?;
|
||||
let duration = plan_duration(request.duration_seconds, strategy)?;
|
||||
|
||||
Ok(VideoGenerationPlan::ArtcraftVeo3Fast(PlanArtcraftVeo3Fast {
|
||||
prompt: request.prompt,
|
||||
start_frame,
|
||||
aspect_ratio,
|
||||
resolution,
|
||||
duration,
|
||||
generate_audio: request.generate_audio,
|
||||
@@ -58,6 +55,33 @@ pub fn plan_generate_video_artcraft_veo_3_fast<'a>(
|
||||
}))
|
||||
}
|
||||
|
||||
fn plan_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<GenerateVeo3FastAspectRatio>, ArtcraftRouterError> {
|
||||
use GenerateVeo3FastAspectRatio as Ar;
|
||||
match aspect_ratio {
|
||||
None
|
||||
| Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto3k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(Some(Ar::Auto)),
|
||||
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Some(Ar::SixteenByNine)),
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Some(Ar::NineBySixteen)),
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Some(Ar::Auto)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_resolution(
|
||||
resolution: Option<CommonResolution>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
|
||||
+32
-18
@@ -9,11 +9,22 @@ use fal_client::requests::webhook::video::image::enqueue_veo_2_image_to_video_we
|
||||
Veo2AspectRatio, Veo2Duration,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum FalVeo2Mode {
|
||||
TextToVideo,
|
||||
ImageToVideo {
|
||||
image_url: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanFalVeo2 {
|
||||
pub prompt: String,
|
||||
pub start_frame_url: String,
|
||||
pub aspect_ratio: Veo2AspectRatio,
|
||||
pub negative_prompt: Option<String>,
|
||||
pub mode: FalVeo2Mode,
|
||||
/// Only set for text-to-video. Image-to-video inherits the source frame's
|
||||
/// aspect ratio and doesn't accept this parameter.
|
||||
pub aspect_ratio: Option<Veo2AspectRatio>,
|
||||
pub duration: Veo2Duration,
|
||||
}
|
||||
|
||||
@@ -22,35 +33,37 @@ pub fn plan_generate_video_fal_veo_2<'a>(
|
||||
) -> Result<VideoGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
let start_frame_url = resolve_required_image_url(request.start_frame, "start_frame")?;
|
||||
if request.end_frame.is_some() {
|
||||
return Err(unsupported("end_frame", "Veo 2 does not support an ending frame"));
|
||||
}
|
||||
|
||||
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let mode = match request.start_frame {
|
||||
Some(ImageRef::Url(url)) => FalVeo2Mode::ImageToVideo {
|
||||
image_url: url.to_string(),
|
||||
},
|
||||
Some(ImageRef::MediaFileToken(_)) => {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls));
|
||||
}
|
||||
None => FalVeo2Mode::TextToVideo,
|
||||
};
|
||||
|
||||
// Aspect ratio only applies to text-to-video; image-to-video inherits
|
||||
// the source frame's aspect ratio.
|
||||
let aspect_ratio = match &mode {
|
||||
FalVeo2Mode::TextToVideo => Some(plan_aspect_ratio(request.aspect_ratio, strategy)?),
|
||||
FalVeo2Mode::ImageToVideo { .. } => None,
|
||||
};
|
||||
let duration = plan_duration(request.duration_seconds, strategy)?;
|
||||
|
||||
Ok(VideoGenerationPlan::FalVeo2(PlanFalVeo2 {
|
||||
prompt: request.prompt.unwrap_or("").to_string(),
|
||||
start_frame_url,
|
||||
negative_prompt: request.negative_prompt.map(|s| s.to_string()),
|
||||
mode,
|
||||
aspect_ratio,
|
||||
duration,
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_required_image_url(
|
||||
image_ref: Option<ImageRef<'_>>,
|
||||
field: &'static str,
|
||||
) -> Result<String, ArtcraftRouterError> {
|
||||
match image_ref {
|
||||
Some(ImageRef::Url(url)) => Ok(url.to_string()),
|
||||
Some(ImageRef::MediaFileToken(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
}
|
||||
None => Err(unsupported(field, "Veo 2 requires a starting frame")),
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
@@ -60,6 +73,7 @@ fn plan_aspect_ratio(
|
||||
None
|
||||
| Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto3k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(Ar::Auto),
|
||||
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Ar::WideSixteenNine),
|
||||
|
||||
+117
-38
@@ -7,16 +7,65 @@ use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use fal_client::requests::webhook::video::image::enqueue_veo_3_image_to_video_webhook::{
|
||||
Veo3AspectRatio, Veo3Duration, Veo3Resolution,
|
||||
Veo3I2vAspectRatio, Veo3I2vDuration, Veo3I2vResolution,
|
||||
};
|
||||
use fal_client::requests::webhook::video::text::enqueue_veo_3_text_to_video_webhook::{
|
||||
Veo3T2vAspectRatio, Veo3T2vDuration, Veo3T2vResolution,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum FalVeo3Mode {
|
||||
TextToVideo,
|
||||
ImageToVideo {
|
||||
image_url: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Router-level duration shared between both modes.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalVeo3Duration {
|
||||
Default,
|
||||
FourSeconds,
|
||||
SixSeconds,
|
||||
EightSeconds,
|
||||
}
|
||||
|
||||
/// Router-level resolution shared between both modes.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalVeo3Resolution {
|
||||
Default,
|
||||
SevenTwentyP,
|
||||
TenEightyP,
|
||||
}
|
||||
|
||||
/// Router-level aspect ratio for text-to-video only (no Auto, no Square).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalVeo3T2vAspectRatio {
|
||||
Default,
|
||||
WideSixteenNine,
|
||||
TallNineSixteen,
|
||||
}
|
||||
|
||||
/// Router-level aspect ratio for image-to-video only (has Auto, no Square).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalVeo3I2vAspectRatio {
|
||||
Auto,
|
||||
WideSixteenNine,
|
||||
TallNineSixteen,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanFalVeo3 {
|
||||
pub prompt: String,
|
||||
pub start_frame_url: String,
|
||||
pub aspect_ratio: Veo3AspectRatio,
|
||||
pub resolution: Veo3Resolution,
|
||||
pub duration: Veo3Duration,
|
||||
pub negative_prompt: Option<String>,
|
||||
pub mode: FalVeo3Mode,
|
||||
/// Only set for text-to-video. Image-to-video inherits the source frame's
|
||||
/// aspect ratio and uses `i2v_aspect_ratio` instead.
|
||||
pub t2v_aspect_ratio: Option<FalVeo3T2vAspectRatio>,
|
||||
/// Only set for image-to-video.
|
||||
pub i2v_aspect_ratio: Option<FalVeo3I2vAspectRatio>,
|
||||
pub resolution: FalVeo3Resolution,
|
||||
pub duration: FalVeo3Duration,
|
||||
pub generate_audio: bool,
|
||||
}
|
||||
|
||||
@@ -25,58 +74,88 @@ pub fn plan_generate_video_fal_veo_3<'a>(
|
||||
) -> Result<VideoGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
let start_frame_url = resolve_required_image_url(request.start_frame)?;
|
||||
if request.end_frame.is_some() {
|
||||
return Err(unsupported("end_frame", "Veo 3 does not support an ending frame"));
|
||||
}
|
||||
|
||||
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let mode = match request.start_frame {
|
||||
Some(ImageRef::Url(url)) => FalVeo3Mode::ImageToVideo {
|
||||
image_url: url.to_string(),
|
||||
},
|
||||
Some(ImageRef::MediaFileToken(_)) => {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls));
|
||||
}
|
||||
None => FalVeo3Mode::TextToVideo,
|
||||
};
|
||||
|
||||
let (t2v_aspect_ratio, i2v_aspect_ratio) = match &mode {
|
||||
FalVeo3Mode::TextToVideo => {
|
||||
(Some(plan_t2v_aspect_ratio(request.aspect_ratio, strategy)?), None)
|
||||
}
|
||||
FalVeo3Mode::ImageToVideo { .. } => {
|
||||
(None, Some(plan_i2v_aspect_ratio(request.aspect_ratio, strategy)?))
|
||||
}
|
||||
};
|
||||
let resolution = plan_resolution(request.resolution, strategy)?;
|
||||
let duration = plan_duration(request.duration_seconds, strategy)?;
|
||||
let generate_audio = request.generate_audio.unwrap_or(true);
|
||||
|
||||
Ok(VideoGenerationPlan::FalVeo3(PlanFalVeo3 {
|
||||
prompt: request.prompt.unwrap_or("").to_string(),
|
||||
start_frame_url,
|
||||
aspect_ratio,
|
||||
negative_prompt: request.negative_prompt.map(|s| s.to_string()),
|
||||
mode,
|
||||
t2v_aspect_ratio,
|
||||
i2v_aspect_ratio,
|
||||
resolution,
|
||||
duration,
|
||||
generate_audio,
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_required_image_url(
|
||||
image_ref: Option<ImageRef<'_>>,
|
||||
) -> Result<String, ArtcraftRouterError> {
|
||||
match image_ref {
|
||||
Some(ImageRef::Url(url)) => Ok(url.to_string()),
|
||||
Some(ImageRef::MediaFileToken(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
}
|
||||
None => Err(unsupported("start_frame", "Veo 3 requires a starting frame")),
|
||||
/// Text-to-video: only 16:9 and 9:16 (no Auto, no Square).
|
||||
fn plan_t2v_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<FalVeo3T2vAspectRatio, ArtcraftRouterError> {
|
||||
use FalVeo3T2vAspectRatio as Ar;
|
||||
match aspect_ratio {
|
||||
None => Ok(Ar::Default),
|
||||
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Ar::WideSixteenNine),
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Ar::TallNineSixteen),
|
||||
|
||||
// Everything else (Auto*, Square, unsupported) → Default (16:9).
|
||||
Some(_) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(unsupported("aspect_ratio", &format!("{:?}", aspect_ratio.unwrap())))
|
||||
}
|
||||
_ => Ok(Ar::Default),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_aspect_ratio(
|
||||
/// Image-to-video: Auto, 16:9, 9:16 (no Square).
|
||||
fn plan_i2v_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Veo3AspectRatio, ArtcraftRouterError> {
|
||||
use Veo3AspectRatio as Ar;
|
||||
) -> Result<FalVeo3I2vAspectRatio, ArtcraftRouterError> {
|
||||
use FalVeo3I2vAspectRatio as Ar;
|
||||
match aspect_ratio {
|
||||
None
|
||||
| Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(Ar::Default),
|
||||
| Some(CommonAspectRatio::Auto3k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(Ar::Auto),
|
||||
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Ar::Square),
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Ar::WideSixteenNine),
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Ar::TallNineSixteen),
|
||||
|
||||
Some(unsupported_ar) => match strategy {
|
||||
// Everything else (Square, unsupported) → Auto.
|
||||
Some(_) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(unsupported("aspect_ratio", &format!("{:?}", unsupported_ar)))
|
||||
Err(unsupported("aspect_ratio", &format!("{:?}", aspect_ratio.unwrap())))
|
||||
}
|
||||
_ => Ok(Ar::Default),
|
||||
_ => Ok(Ar::Auto),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -84,8 +163,8 @@ fn plan_aspect_ratio(
|
||||
fn plan_resolution(
|
||||
resolution: Option<CommonResolution>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Veo3Resolution, ArtcraftRouterError> {
|
||||
use Veo3Resolution as R;
|
||||
) -> Result<FalVeo3Resolution, ArtcraftRouterError> {
|
||||
use FalVeo3Resolution as R;
|
||||
match resolution {
|
||||
None => Ok(R::Default),
|
||||
Some(CommonResolution::SevenTwentyP) => Ok(R::SevenTwentyP),
|
||||
@@ -103,18 +182,18 @@ fn plan_resolution(
|
||||
fn plan_duration(
|
||||
duration_seconds: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Veo3Duration, ArtcraftRouterError> {
|
||||
) -> Result<FalVeo3Duration, ArtcraftRouterError> {
|
||||
match duration_seconds {
|
||||
None => Ok(Veo3Duration::Default),
|
||||
Some(4) => Ok(Veo3Duration::FourSeconds),
|
||||
Some(6) => Ok(Veo3Duration::SixSeconds),
|
||||
Some(8) => Ok(Veo3Duration::EightSeconds),
|
||||
None => Ok(FalVeo3Duration::Default),
|
||||
Some(4) => Ok(FalVeo3Duration::FourSeconds),
|
||||
Some(6) => Ok(FalVeo3Duration::SixSeconds),
|
||||
Some(8) => Ok(FalVeo3Duration::EightSeconds),
|
||||
Some(other) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(unsupported("duration_seconds", &format!("{}", other)))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(Veo3Duration::EightSeconds),
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Veo3Duration::FourSeconds),
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(FalVeo3Duration::EightSeconds),
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(FalVeo3Duration::FourSeconds),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -129,9 +208,9 @@ fn unsupported(field: &'static str, value: &str) -> ArtcraftRouterError {
|
||||
impl PlanFalVeo3 {
|
||||
pub fn duration_seconds_for_cost(&self) -> u64 {
|
||||
match self.duration {
|
||||
Veo3Duration::Default | Veo3Duration::EightSeconds => 8,
|
||||
Veo3Duration::SixSeconds => 6,
|
||||
Veo3Duration::FourSeconds => 4,
|
||||
FalVeo3Duration::Default | FalVeo3Duration::EightSeconds => 8,
|
||||
FalVeo3Duration::SixSeconds => 6,
|
||||
FalVeo3Duration::FourSeconds => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+29
-1
@@ -5,14 +5,16 @@ use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use fal_client::requests::webhook::video::image::enqueue_veo_3_fast_image_to_video_webhook::{
|
||||
Veo3FastDuration, Veo3FastResolution,
|
||||
Veo3FastAspectRatio, Veo3FastDuration, Veo3FastResolution,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanFalVeo3Fast {
|
||||
pub prompt: String,
|
||||
pub start_frame_url: String,
|
||||
pub aspect_ratio: Veo3FastAspectRatio,
|
||||
pub resolution: Veo3FastResolution,
|
||||
pub duration: Veo3FastDuration,
|
||||
pub generate_audio: bool,
|
||||
@@ -28,6 +30,7 @@ pub fn plan_generate_video_fal_veo_3_fast<'a>(
|
||||
return Err(unsupported("end_frame", "Veo 3 Fast does not support an ending frame"));
|
||||
}
|
||||
|
||||
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let resolution = plan_resolution(request.resolution, strategy)?;
|
||||
let duration = plan_duration(request.duration_seconds, strategy)?;
|
||||
let generate_audio = request.generate_audio.unwrap_or(true);
|
||||
@@ -35,6 +38,7 @@ pub fn plan_generate_video_fal_veo_3_fast<'a>(
|
||||
Ok(VideoGenerationPlan::FalVeo3Fast(PlanFalVeo3Fast {
|
||||
prompt: request.prompt.unwrap_or("").to_string(),
|
||||
start_frame_url,
|
||||
aspect_ratio,
|
||||
resolution,
|
||||
duration,
|
||||
generate_audio,
|
||||
@@ -53,6 +57,30 @@ fn resolve_required_image_url(
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Veo3FastAspectRatio, ArtcraftRouterError> {
|
||||
use Veo3FastAspectRatio as Ar;
|
||||
match aspect_ratio {
|
||||
None
|
||||
| Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto3k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(Ar::Auto),
|
||||
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Ar::WideSixteenNine),
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Ar::TallNineSixteen),
|
||||
|
||||
Some(unsupported_ar) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(unsupported("aspect_ratio", &format!("{:?}", unsupported_ar)))
|
||||
}
|
||||
_ => Ok(Ar::Auto),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_resolution(
|
||||
resolution: Option<CommonResolution>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
|
||||
-4
@@ -7,10 +7,6 @@ pub struct Veo2ImageToVideoInput {
|
||||
|
||||
pub prompt: String,
|
||||
|
||||
/// Options: "auto", "16:9", "9:16"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub aspect_ratio: Option<String>,
|
||||
|
||||
/// Options: "4s", "6s", "8s"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub duration: Option<String>,
|
||||
|
||||
+4
@@ -7,6 +7,10 @@ pub struct Veo3FastImageToVideoInput {
|
||||
|
||||
pub prompt: String,
|
||||
|
||||
/// Options: "auto", "16:9", "9:16"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub aspect_ratio: Option<String>,
|
||||
|
||||
/// Options: "720p", "1080p"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub resolution: Option<String>,
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
use crate::requests::api::fal_request::FalRequest;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
pub struct Veo2TextToVideoInput {
|
||||
pub prompt: String,
|
||||
|
||||
/// Options: "16:9", "9:16", "1:1"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub aspect_ratio: Option<String>,
|
||||
|
||||
/// Options: "5s", "6s", "7s", "8s"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub duration: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub negative_prompt: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Veo2TextToVideoVideoFile {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Veo2TextToVideoOutput {
|
||||
pub video: Veo2TextToVideoVideoFile,
|
||||
}
|
||||
|
||||
pub fn veo_2_text_to_video(
|
||||
params: Veo2TextToVideoInput,
|
||||
) -> FalRequest<Veo2TextToVideoInput, Veo2TextToVideoOutput> {
|
||||
FalRequest::new("fal-ai/veo2", params)
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
use crate::requests::api::fal_request::FalRequest;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
pub struct Veo3TextToVideoInput {
|
||||
pub prompt: String,
|
||||
|
||||
/// Options: "16:9", "9:16"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub aspect_ratio: Option<String>,
|
||||
|
||||
/// Options: "720p", "1080p"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub resolution: Option<String>,
|
||||
|
||||
/// Options: "4s", "6s", "8s"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub duration: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub generate_audio: Option<bool>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub negative_prompt: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Veo3TextToVideoVideoFile {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Veo3TextToVideoOutput {
|
||||
pub video: Veo3TextToVideoVideoFile,
|
||||
}
|
||||
|
||||
pub fn veo_3_text_to_video(
|
||||
params: Veo3TextToVideoInput,
|
||||
) -> FalRequest<Veo3TextToVideoInput, Veo3TextToVideoOutput> {
|
||||
FalRequest::new("fal-ai/veo3", params)
|
||||
}
|
||||
@@ -7,5 +7,7 @@ pub mod http_pixverse_v5_text_to_video;
|
||||
pub mod http_seedance_1p5_pro_text_to_video;
|
||||
pub mod http_sora_2_pro_text_to_video;
|
||||
pub mod http_sora_2_text_to_video;
|
||||
pub mod http_veo_2_text_to_video;
|
||||
pub mod http_veo_3_text_to_video;
|
||||
pub mod http_veo_3p1_fast_text_to_video;
|
||||
pub mod http_veo_3p1_text_to_video;
|
||||
|
||||
+12
-19
@@ -1,21 +1,22 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
|
||||
use crate::requests::http::video::image::http_veo_2_image_to_video::{veo_2_image_to_video, Veo2ImageToVideoInput};
|
||||
use crate::requests::api::webhook_response::WebhookResponse;
|
||||
use crate::requests::http::video::image::http_veo_2_image_to_video::{veo_2_image_to_video, Veo2ImageToVideoInput};
|
||||
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
/// Args for Veo 2 image-to-video. Note: image-to-video does NOT support
|
||||
/// aspect_ratio — the output inherits the source image's aspect ratio.
|
||||
pub struct Veo2Args<'a, U: IntoUrl, V: IntoUrl> {
|
||||
pub image_url: U,
|
||||
pub webhook_url: V,
|
||||
pub prompt: &'a str,
|
||||
pub api_key: &'a FalApiKey,
|
||||
pub duration: Veo2Duration,
|
||||
pub aspect_ratio: Veo2AspectRatio,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Veo2Duration {
|
||||
Default, // NB: Default is 5 seconds
|
||||
FiveSeconds,
|
||||
@@ -24,6 +25,9 @@ pub enum Veo2Duration {
|
||||
EightSeconds,
|
||||
}
|
||||
|
||||
/// Aspect ratio enum — used by the text-to-video endpoint only.
|
||||
/// Kept here because it's the canonical location that the text-to-video
|
||||
/// webhook and the router plan both re-export from.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Veo2AspectRatio {
|
||||
Auto,
|
||||
@@ -59,22 +63,13 @@ pub async fn enqueue_veo_2_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
|
||||
Veo2Duration::SevenSeconds => Some("7s".to_string()),
|
||||
Veo2Duration::EightSeconds => Some("8s".to_string()),
|
||||
};
|
||||
|
||||
let aspect_ratio = match args.aspect_ratio {
|
||||
Veo2AspectRatio::Auto => Some("auto".to_string()),
|
||||
Veo2AspectRatio::AutoPreferPortrait => Some("auto_prefer_portrait".to_string()),
|
||||
Veo2AspectRatio::WideSixteenNine => Some("16:9".to_string()),
|
||||
Veo2AspectRatio::TallNineSixteen => Some("9:16".to_string()),
|
||||
};
|
||||
|
||||
let image_url = args.image_url.as_str().to_string();
|
||||
|
||||
let request = Veo2ImageToVideoInput {
|
||||
image_url,
|
||||
prompt: args.prompt.to_string(),
|
||||
aspect_ratio,
|
||||
duration,
|
||||
// Maybe expose these later
|
||||
};
|
||||
|
||||
let result = veo_2_image_to_video(request)
|
||||
@@ -89,26 +84,24 @@ pub async fn enqueue_veo_2_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::requests::webhook::video::image::enqueue_veo_2_image_to_video_webhook::{enqueue_veo_2_image_to_video_webhook, Veo2Args, Veo2AspectRatio, Veo2Duration};
|
||||
use crate::requests::webhook::video::image::enqueue_veo_2_image_to_video_webhook::{enqueue_veo_2_image_to_video_webhook, Veo2Args, Veo2Duration};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use test_data::web::image_urls::MOUNTAIN_TREE_IMAGE_URL;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_kling21_pro_video() -> AnyhowResult<()> {
|
||||
let image_url = "https://cdn-2.fakeyou.com/media/3/4/h/f/s/34hfsmt8e38rvne6mwa4pwbxr6292sgy/image_34hfsmt8e38rvne6mwa4pwbxr6292sgy.png";
|
||||
|
||||
async fn test_veo_2_image_to_video() -> AnyhowResult<()> {
|
||||
// XXX: Don't commit secrets!
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = Veo2Args {
|
||||
image_url: image_url,
|
||||
image_url: MOUNTAIN_TREE_IMAGE_URL,
|
||||
prompt: "a shot of the mountains as the sun sets and reveals the moon and stars",
|
||||
api_key: &api_key,
|
||||
duration: Veo2Duration::Default,
|
||||
aspect_ratio: Veo2AspectRatio::WideSixteenNine,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
|
||||
+18
-2
@@ -9,6 +9,7 @@ use reqwest::IntoUrl;
|
||||
pub struct Veo3FastArgs<'a, U: IntoUrl, V: IntoUrl> {
|
||||
pub prompt: &'a str,
|
||||
pub image_url: U,
|
||||
pub aspect_ratio: Veo3FastAspectRatio,
|
||||
pub duration: Veo3FastDuration,
|
||||
pub api_key: &'a FalApiKey,
|
||||
pub resolution: Veo3FastResolution,
|
||||
@@ -24,6 +25,13 @@ pub enum Veo3FastDuration {
|
||||
EightSeconds,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Veo3FastAspectRatio {
|
||||
Auto,
|
||||
WideSixteenNine, // 16:9
|
||||
TallNineSixteen, // 9:16
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Veo3FastResolution {
|
||||
Default,
|
||||
@@ -62,7 +70,13 @@ pub async fn enqueue_veo_3_fast_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
|
||||
Veo3FastDuration::EightSeconds => Some("8s".to_string()),
|
||||
};
|
||||
|
||||
let resolution= match args.resolution {
|
||||
let aspect_ratio = match args.aspect_ratio {
|
||||
Veo3FastAspectRatio::Auto => Some("auto".to_string()),
|
||||
Veo3FastAspectRatio::WideSixteenNine => Some("16:9".to_string()),
|
||||
Veo3FastAspectRatio::TallNineSixteen => Some("9:16".to_string()),
|
||||
};
|
||||
|
||||
let resolution = match args.resolution {
|
||||
Veo3FastResolution::Default => None,
|
||||
Veo3FastResolution::SevenTwentyP => Some("720p".to_string()),
|
||||
Veo3FastResolution::TenEightyP => Some("1080p".to_string()),
|
||||
@@ -73,6 +87,7 @@ pub async fn enqueue_veo_3_fast_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
|
||||
let request = Veo3FastImageToVideoInput {
|
||||
image_url,
|
||||
prompt: args.prompt.to_string(),
|
||||
aspect_ratio,
|
||||
resolution,
|
||||
duration,
|
||||
generate_audio: Some(args.generate_audio),
|
||||
@@ -90,7 +105,7 @@ pub async fn enqueue_veo_3_fast_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::requests::webhook::video::image::enqueue_veo_3_fast_image_to_video_webhook::{enqueue_veo_3_fast_image_to_video_webhook, Veo3FastArgs, Veo3FastDuration, Veo3FastResolution};
|
||||
use crate::requests::webhook::video::image::enqueue_veo_3_fast_image_to_video_webhook::{enqueue_veo_3_fast_image_to_video_webhook, Veo3FastArgs, Veo3FastAspectRatio, Veo3FastDuration, Veo3FastResolution};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use test_data::web::image_urls::ERNEST_GHOST_TREX_IMAGE_URL;
|
||||
@@ -109,6 +124,7 @@ mod tests {
|
||||
image_url: image_url,
|
||||
prompt: "man is standing next to a ghost and t-rex, they begin to chase him as the camera pulls back to show the wider scene",
|
||||
api_key: &api_key,
|
||||
aspect_ratio: Veo3FastAspectRatio::WideSixteenNine,
|
||||
duration: Veo3FastDuration::EightSeconds,
|
||||
generate_audio: true,
|
||||
resolution: Veo3FastResolution::TenEightyP,
|
||||
|
||||
+41
-39
@@ -9,32 +9,35 @@ use reqwest::IntoUrl;
|
||||
pub struct Veo3Args<'a, U: IntoUrl, V: IntoUrl> {
|
||||
pub image_url: U,
|
||||
pub prompt: &'a str,
|
||||
pub duration: Veo3Duration,
|
||||
pub aspect_ratio: Veo3AspectRatio,
|
||||
pub resolution: Veo3Resolution,
|
||||
pub duration: Veo3I2vDuration,
|
||||
pub aspect_ratio: Veo3I2vAspectRatio,
|
||||
pub resolution: Veo3I2vResolution,
|
||||
pub generate_audio: bool,
|
||||
pub api_key: &'a FalApiKey,
|
||||
pub webhook_url: V,
|
||||
}
|
||||
|
||||
/// Duration for Veo 3 image-to-video. Default is 8 seconds.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Veo3Duration {
|
||||
pub enum Veo3I2vDuration {
|
||||
Default, // Default is 8 seconds
|
||||
FourSeconds,
|
||||
SixSeconds,
|
||||
EightSeconds,
|
||||
}
|
||||
|
||||
/// Aspect ratio for Veo 3 image-to-video.
|
||||
/// Supports Auto (inherit source), 16:9, and 9:16. No Square.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Veo3AspectRatio {
|
||||
Default,
|
||||
WideSixteenNine,
|
||||
TallNineSixteen,
|
||||
Square,
|
||||
pub enum Veo3I2vAspectRatio {
|
||||
Auto,
|
||||
WideSixteenNine, // 16:9
|
||||
TallNineSixteen, // 9:16
|
||||
}
|
||||
|
||||
/// Resolution for Veo 3 image-to-video.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Veo3Resolution {
|
||||
pub enum Veo3I2vResolution {
|
||||
Default,
|
||||
SevenTwentyP,
|
||||
TenEightyP,
|
||||
@@ -48,14 +51,14 @@ impl <U: IntoUrl, V: IntoUrl> FalRequestCostCalculator for Veo3Args<'_, U, V> {
|
||||
// For example, a 5s video with audio on will cost $2."
|
||||
|
||||
match (self.duration, self.generate_audio) {
|
||||
(Veo3Duration::FourSeconds, false) => 80,
|
||||
(Veo3Duration::SixSeconds, false) => 120,
|
||||
(Veo3Duration::EightSeconds, false) => 160,
|
||||
(Veo3Duration::Default, false) => 160, // NB: Default is 8 seconds
|
||||
(Veo3Duration::FourSeconds, true) => 160,
|
||||
(Veo3Duration::SixSeconds, true) => 240,
|
||||
(Veo3Duration::EightSeconds, true) => 320,
|
||||
(Veo3Duration::Default, true) => 320, // NB: Default is 8 seconds320
|
||||
(Veo3I2vDuration::FourSeconds, false) => 80,
|
||||
(Veo3I2vDuration::SixSeconds, false) => 120,
|
||||
(Veo3I2vDuration::EightSeconds, false) => 160,
|
||||
(Veo3I2vDuration::Default, false) => 160,
|
||||
(Veo3I2vDuration::FourSeconds, true) => 160,
|
||||
(Veo3I2vDuration::SixSeconds, true) => 240,
|
||||
(Veo3I2vDuration::EightSeconds, true) => 320,
|
||||
(Veo3I2vDuration::Default, true) => 320,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,23 +70,22 @@ pub async fn enqueue_veo_3_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
|
||||
args: Veo3Args<'_, U, V>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
let duration = match args.duration {
|
||||
Veo3Duration::Default => None,
|
||||
Veo3Duration::FourSeconds => Some("4s".to_string()),
|
||||
Veo3Duration::SixSeconds => Some("6s".to_string()),
|
||||
Veo3Duration::EightSeconds => Some("8s".to_string()),
|
||||
Veo3I2vDuration::Default => None,
|
||||
Veo3I2vDuration::FourSeconds => Some("4s".to_string()),
|
||||
Veo3I2vDuration::SixSeconds => Some("6s".to_string()),
|
||||
Veo3I2vDuration::EightSeconds => Some("8s".to_string()),
|
||||
};
|
||||
|
||||
let aspect_ratio = match args.aspect_ratio {
|
||||
Veo3AspectRatio::Default => None,
|
||||
Veo3AspectRatio::WideSixteenNine => Some("16:9".to_string()),
|
||||
Veo3AspectRatio::TallNineSixteen => Some("9:16".to_string()),
|
||||
Veo3AspectRatio::Square => Some("1:1".to_string()),
|
||||
Veo3I2vAspectRatio::Auto => Some("auto".to_string()),
|
||||
Veo3I2vAspectRatio::WideSixteenNine => Some("16:9".to_string()),
|
||||
Veo3I2vAspectRatio::TallNineSixteen => Some("9:16".to_string()),
|
||||
};
|
||||
|
||||
let resolution= match args.resolution {
|
||||
Veo3Resolution::Default => None,
|
||||
Veo3Resolution::SevenTwentyP => Some("720p".to_string()),
|
||||
Veo3Resolution::TenEightyP => Some("1080p".to_string()),
|
||||
|
||||
let resolution = match args.resolution {
|
||||
Veo3I2vResolution::Default => None,
|
||||
Veo3I2vResolution::SevenTwentyP => Some("720p".to_string()),
|
||||
Veo3I2vResolution::TenEightyP => Some("1080p".to_string()),
|
||||
};
|
||||
|
||||
let image_url = args.image_url.as_str().to_string();
|
||||
@@ -109,7 +111,10 @@ pub async fn enqueue_veo_3_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::requests::webhook::video::image::enqueue_veo_3_image_to_video_webhook::{enqueue_veo_3_image_to_video_webhook, Veo3Args, Veo3AspectRatio, Veo3Duration, Veo3Resolution};
|
||||
use crate::requests::webhook::video::image::enqueue_veo_3_image_to_video_webhook::{
|
||||
enqueue_veo_3_image_to_video_webhook, Veo3Args, Veo3I2vAspectRatio, Veo3I2vDuration,
|
||||
Veo3I2vResolution,
|
||||
};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use test_data::web::image_urls::TREX_SKELETON_IMAGE_URL;
|
||||
@@ -117,24 +122,21 @@ mod tests {
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test() -> AnyhowResult<()> {
|
||||
// XXX: Don't commit secrets!
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = Veo3Args {
|
||||
image_url: TREX_SKELETON_IMAGE_URL,
|
||||
prompt: "the t-rex skeleton starts walking towards the camera and roars",
|
||||
api_key: &api_key,
|
||||
duration: Veo3Duration::EightSeconds,
|
||||
aspect_ratio: Veo3AspectRatio::WideSixteenNine,
|
||||
resolution: Veo3Resolution::TenEightyP,
|
||||
duration: Veo3I2vDuration::EightSeconds,
|
||||
aspect_ratio: Veo3I2vAspectRatio::WideSixteenNine,
|
||||
resolution: Veo3I2vResolution::TenEightyP,
|
||||
generate_audio: true,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let result = enqueue_veo_3_image_to_video_webhook(args).await?;
|
||||
|
||||
let _result = enqueue_veo_3_image_to_video_webhook(args).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use crate::requests::api::webhook_response::WebhookResponse;
|
||||
use crate::requests::http::video::text::http_veo_2_text_to_video::{
|
||||
veo_2_text_to_video, Veo2TextToVideoInput,
|
||||
};
|
||||
use crate::requests::traits::fal_request_cost_calculator_trait::{
|
||||
FalRequestCostCalculator, UsdCents,
|
||||
};
|
||||
// Re-use the same duration/aspect-ratio enums as image-to-video.
|
||||
pub use crate::requests::webhook::video::image::enqueue_veo_2_image_to_video_webhook::{
|
||||
Veo2AspectRatio, Veo2Duration,
|
||||
};
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct Veo2TextToVideoArgs<'a, V: IntoUrl> {
|
||||
pub prompt: &'a str,
|
||||
pub negative_prompt: Option<&'a str>,
|
||||
pub duration: Veo2Duration,
|
||||
pub aspect_ratio: Veo2AspectRatio,
|
||||
pub webhook_url: V,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
impl<V: IntoUrl> FalRequestCostCalculator for Veo2TextToVideoArgs<'_, V> {
|
||||
fn calculate_cost_in_cents(&self) -> UsdCents {
|
||||
// Same pricing as image-to-video:
|
||||
// "For 5s video your request will cost $2.50.
|
||||
// For every additional second you will be charged $0.50."
|
||||
match self.duration {
|
||||
Veo2Duration::Default => 250,
|
||||
Veo2Duration::FiveSeconds => 250,
|
||||
Veo2Duration::SixSeconds => 300,
|
||||
Veo2Duration::SevenSeconds => 350,
|
||||
Veo2Duration::EightSeconds => 400,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Veo 2 Text-to-Video
|
||||
/// https://fal.ai/models/fal-ai/veo2
|
||||
pub async fn enqueue_veo_2_text_to_video_webhook<V: IntoUrl>(
|
||||
args: Veo2TextToVideoArgs<'_, V>,
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
let duration = match args.duration {
|
||||
Veo2Duration::Default => None,
|
||||
Veo2Duration::FiveSeconds => Some("5s".to_string()),
|
||||
Veo2Duration::SixSeconds => Some("6s".to_string()),
|
||||
Veo2Duration::SevenSeconds => Some("7s".to_string()),
|
||||
Veo2Duration::EightSeconds => Some("8s".to_string()),
|
||||
};
|
||||
|
||||
let aspect_ratio = match args.aspect_ratio {
|
||||
Veo2AspectRatio::Auto => None, // Let the API default (16:9)
|
||||
Veo2AspectRatio::AutoPreferPortrait => Some("9:16".to_string()),
|
||||
Veo2AspectRatio::WideSixteenNine => Some("16:9".to_string()),
|
||||
Veo2AspectRatio::TallNineSixteen => Some("9:16".to_string()),
|
||||
};
|
||||
|
||||
let request = Veo2TextToVideoInput {
|
||||
prompt: args.prompt.to_string(),
|
||||
aspect_ratio,
|
||||
duration,
|
||||
negative_prompt: args.negative_prompt.map(|s| s.to_string()),
|
||||
};
|
||||
|
||||
let result = veo_2_text_to_video(request)
|
||||
.with_api_key(&args.api_key.0)
|
||||
.queue_webhook(args.webhook_url)
|
||||
.await;
|
||||
|
||||
result.map_err(|err| classify_fal_error(err))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::requests::webhook::video::text::enqueue_veo_2_text_to_video_webhook::{
|
||||
enqueue_veo_2_text_to_video_webhook, Veo2TextToVideoArgs, Veo2AspectRatio, Veo2Duration,
|
||||
};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_veo_2_text_to_video() -> AnyhowResult<()> {
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = Veo2TextToVideoArgs {
|
||||
prompt: "a drone shot of a coastal sunset with waves crashing on rocks",
|
||||
negative_prompt: None,
|
||||
api_key: &api_key,
|
||||
duration: Veo2Duration::FiveSeconds,
|
||||
aspect_ratio: Veo2AspectRatio::WideSixteenNine,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let _result = enqueue_veo_2_text_to_video_webhook(args).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use crate::requests::api::webhook_response::WebhookResponse;
|
||||
use crate::requests::http::video::text::http_veo_3_text_to_video::{
|
||||
veo_3_text_to_video, Veo3TextToVideoInput,
|
||||
};
|
||||
use crate::requests::traits::fal_request_cost_calculator_trait::{
|
||||
FalRequestCostCalculator, UsdCents,
|
||||
};
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct Veo3TextToVideoArgs<'a, V: IntoUrl> {
|
||||
pub prompt: &'a str,
|
||||
pub negative_prompt: Option<&'a str>,
|
||||
pub duration: Veo3T2vDuration,
|
||||
pub aspect_ratio: Veo3T2vAspectRatio,
|
||||
pub resolution: Veo3T2vResolution,
|
||||
pub generate_audio: bool,
|
||||
pub webhook_url: V,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
/// Duration for Veo 3 text-to-video. Default is 8 seconds.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Veo3T2vDuration {
|
||||
Default, // Default is 8 seconds
|
||||
FourSeconds,
|
||||
SixSeconds,
|
||||
EightSeconds,
|
||||
}
|
||||
|
||||
/// Aspect ratio for Veo 3 text-to-video.
|
||||
/// Only 16:9 and 9:16 are supported. No Auto, no Square.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Veo3T2vAspectRatio {
|
||||
Default, // Default is 16:9
|
||||
WideSixteenNine, // 16:9
|
||||
TallNineSixteen, // 9:16
|
||||
}
|
||||
|
||||
/// Resolution for Veo 3 text-to-video.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Veo3T2vResolution {
|
||||
Default,
|
||||
SevenTwentyP,
|
||||
TenEightyP,
|
||||
}
|
||||
|
||||
impl<V: IntoUrl> FalRequestCostCalculator for Veo3TextToVideoArgs<'_, V> {
|
||||
fn calculate_cost_in_cents(&self) -> UsdCents {
|
||||
// $0.20/sec audio off, $0.40/sec audio on.
|
||||
match (self.duration, self.generate_audio) {
|
||||
(Veo3T2vDuration::FourSeconds, false) => 80,
|
||||
(Veo3T2vDuration::SixSeconds, false) => 120,
|
||||
(Veo3T2vDuration::EightSeconds, false) => 160,
|
||||
(Veo3T2vDuration::Default, false) => 160,
|
||||
(Veo3T2vDuration::FourSeconds, true) => 160,
|
||||
(Veo3T2vDuration::SixSeconds, true) => 240,
|
||||
(Veo3T2vDuration::EightSeconds, true) => 320,
|
||||
(Veo3T2vDuration::Default, true) => 320,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Veo 3 Text-to-Video
|
||||
/// https://fal.ai/models/fal-ai/veo3
|
||||
pub async fn enqueue_veo_3_text_to_video_webhook<V: IntoUrl>(
|
||||
args: Veo3TextToVideoArgs<'_, V>,
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
let duration = match args.duration {
|
||||
Veo3T2vDuration::Default => None,
|
||||
Veo3T2vDuration::FourSeconds => Some("4s".to_string()),
|
||||
Veo3T2vDuration::SixSeconds => Some("6s".to_string()),
|
||||
Veo3T2vDuration::EightSeconds => Some("8s".to_string()),
|
||||
};
|
||||
|
||||
let aspect_ratio = match args.aspect_ratio {
|
||||
Veo3T2vAspectRatio::Default => None,
|
||||
Veo3T2vAspectRatio::WideSixteenNine => Some("16:9".to_string()),
|
||||
Veo3T2vAspectRatio::TallNineSixteen => Some("9:16".to_string()),
|
||||
};
|
||||
|
||||
let resolution = match args.resolution {
|
||||
Veo3T2vResolution::Default => None,
|
||||
Veo3T2vResolution::SevenTwentyP => Some("720p".to_string()),
|
||||
Veo3T2vResolution::TenEightyP => Some("1080p".to_string()),
|
||||
};
|
||||
|
||||
let request = Veo3TextToVideoInput {
|
||||
prompt: args.prompt.to_string(),
|
||||
aspect_ratio,
|
||||
resolution,
|
||||
duration,
|
||||
generate_audio: Some(args.generate_audio),
|
||||
negative_prompt: args.negative_prompt.map(|s| s.to_string()),
|
||||
};
|
||||
|
||||
let result = veo_3_text_to_video(request)
|
||||
.with_api_key(&args.api_key.0)
|
||||
.queue_webhook(args.webhook_url)
|
||||
.await;
|
||||
|
||||
result.map_err(|err| classify_fal_error(err))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::requests::webhook::video::text::enqueue_veo_3_text_to_video_webhook::{
|
||||
enqueue_veo_3_text_to_video_webhook, Veo3TextToVideoArgs, Veo3T2vAspectRatio,
|
||||
Veo3T2vDuration, Veo3T2vResolution,
|
||||
};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_veo_3_text_to_video() -> AnyhowResult<()> {
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = Veo3TextToVideoArgs {
|
||||
prompt: "a drone shot of a coastal sunset with waves crashing on rocks",
|
||||
negative_prompt: None,
|
||||
api_key: &api_key,
|
||||
duration: Veo3T2vDuration::EightSeconds,
|
||||
aspect_ratio: Veo3T2vAspectRatio::WideSixteenNine,
|
||||
resolution: Veo3T2vResolution::TenEightyP,
|
||||
generate_audio: true,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let _result = enqueue_veo_3_text_to_video_webhook(args).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -7,5 +7,7 @@ pub mod enqueue_pixverse_v5_text_to_video_webhook;
|
||||
pub mod enqueue_seedance_1p5_pro_text_to_video_webhook;
|
||||
pub mod enqueue_sora_2_pro_text_to_video_webhook;
|
||||
pub mod enqueue_sora_2_text_to_video_webhook;
|
||||
pub mod enqueue_veo_2_text_to_video_webhook;
|
||||
pub mod enqueue_veo_3_text_to_video_webhook;
|
||||
pub mod enqueue_veo_3p1_fast_text_to_video_webhook;
|
||||
pub mod enqueue_veo_3p1_text_to_video_webhook;
|
||||
+1
@@ -41,6 +41,7 @@ pub (super) async fn handle_artcraft_veo3_fast(
|
||||
generate_audio: request.generate_audio,
|
||||
duration: None, // TODO: Parameterize
|
||||
resolution: None, // TODO: Parameterize
|
||||
aspect_ratio: None, // TODO: Parameterize
|
||||
};
|
||||
|
||||
let result = generate_veo_3_fast_image_to_video(
|
||||
|
||||
@@ -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.27.0";
|
||||
pub const ARTCRAFT_VERSION: &str = "0.28.0";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2.0.0-rc",
|
||||
"productName": "ArtCraft",
|
||||
"version": "0.27.0",
|
||||
"version": "0.28.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.27.0",
|
||||
"version": "0.28.0",
|
||||
"identifier": "ai.artcraft.app",
|
||||
"build": {
|
||||
"frontendDist": "../../../frontend/apps/artcraft/dist",
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "opaque_cursors"
|
||||
edition = "2021"
|
||||
version = "0.0.1"
|
||||
authors = [
|
||||
"Brandon Thomas <bt@brand.io>",
|
||||
"Brandon Thomas <echelon@gmail.com>",
|
||||
]
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
name = "opaque_cursors"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
base64.workspace = true
|
||||
magic-crypt = "3.1.13"
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod v1;
|
||||
pub mod v2;
|
||||
@@ -0,0 +1 @@
|
||||
// Reserved for migrating the legacy SortKeyCrypto / SortId from storyteller-web.
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod opaque_cursor_encoder_v2;
|
||||
pub mod opaque_cursor_error_v2;
|
||||
pub mod opaque_cursor_v2;
|
||||
@@ -0,0 +1,243 @@
|
||||
use std::io::Cursor;
|
||||
|
||||
use base64::alphabet::{Alphabet, URL_SAFE};
|
||||
use base64::engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig};
|
||||
use base64::Engine;
|
||||
use magic_crypt::generic_array::typenum::U256;
|
||||
use magic_crypt::{MagicCrypt256, MagicCryptTrait, new_magic_crypt};
|
||||
|
||||
use crate::v2::opaque_cursor_error_v2::OpaqueCursorErrorV2;
|
||||
use crate::v2::opaque_cursor_v2::OpaqueCursorV2;
|
||||
|
||||
/// Encrypts and decrypts opaque cursor payloads so that internal database
|
||||
/// IDs are never leaked to the frontend.
|
||||
#[derive(Clone)]
|
||||
pub struct OpaqueCursorEncoderV2 {
|
||||
crypt: MagicCrypt256,
|
||||
alphabet: Alphabet,
|
||||
base64_config: GeneralPurposeConfig,
|
||||
}
|
||||
|
||||
impl OpaqueCursorEncoderV2 {
|
||||
pub fn new(secret: &str) -> Self {
|
||||
let base64_config = GeneralPurposeConfig::new()
|
||||
.with_encode_padding(false)
|
||||
.with_decode_allow_trailing_bits(true)
|
||||
.with_decode_padding_mode(DecodePaddingMode::Indifferent);
|
||||
|
||||
Self {
|
||||
crypt: new_magic_crypt!(secret, 256),
|
||||
alphabet: URL_SAFE,
|
||||
base64_config,
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode a last-id cursor into an opaque string.
|
||||
pub fn encode_last_id_cursor(&self, name: &str, id: u64) -> Result<String, OpaqueCursorErrorV2> {
|
||||
let cursor = OpaqueCursorV2 {
|
||||
name: Some(name.to_string()),
|
||||
last_id: Some(id),
|
||||
};
|
||||
self.encode_cursor(&cursor)
|
||||
}
|
||||
|
||||
/// Encode an arbitrary cursor payload into an opaque string.
|
||||
pub fn encode_cursor(&self, cursor: &OpaqueCursorV2) -> Result<String, OpaqueCursorErrorV2> {
|
||||
let json = serde_json::to_string(cursor)?;
|
||||
|
||||
let engine = GeneralPurpose::new(&self.alphabet, self.base64_config);
|
||||
|
||||
let mut reader = Cursor::new(json);
|
||||
let mut writer = Vec::new();
|
||||
|
||||
self.crypt.encrypt_reader_to_writer2::<U256>(&mut reader, &mut writer)?;
|
||||
|
||||
Ok(engine.encode(&writer))
|
||||
}
|
||||
|
||||
/// Decode an opaque cursor string and verify the name matches `expected_name`.
|
||||
pub fn decode_cursor_expecting_name(
|
||||
&self,
|
||||
expected_name: &str,
|
||||
cursor: &str,
|
||||
) -> Result<OpaqueCursorV2, OpaqueCursorErrorV2> {
|
||||
let decoded = self.decode_cursor(cursor)?;
|
||||
|
||||
match &decoded.name {
|
||||
None => Err(OpaqueCursorErrorV2::DecodedNameNotPresent),
|
||||
Some(actual) if actual != expected_name => {
|
||||
Err(OpaqueCursorErrorV2::DecodedNameMismatch {
|
||||
expected: expected_name.to_string(),
|
||||
actual: actual.clone(),
|
||||
})
|
||||
}
|
||||
_ => Ok(decoded),
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode an opaque cursor string back into a cursor payload.
|
||||
pub fn decode_cursor(&self, cursor: &str) -> Result<OpaqueCursorV2, OpaqueCursorErrorV2> {
|
||||
let engine = GeneralPurpose::new(&self.alphabet, self.base64_config);
|
||||
|
||||
let decoded_bytes = engine.decode(cursor)?;
|
||||
let decrypted_bytes = self.crypt.decrypt_bytes_to_bytes(&decoded_bytes)?;
|
||||
|
||||
let json = String::from_utf8(decrypted_bytes)?;
|
||||
let cursor = serde_json::from_str::<OpaqueCursorV2>(&json)?;
|
||||
|
||||
Ok(cursor)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const SECRET: &str = "test_secret_key_for_opaque_cursors";
|
||||
|
||||
#[test]
|
||||
fn roundtrip_last_id() {
|
||||
let encoder = OpaqueCursorEncoderV2::new(SECRET);
|
||||
let encoded = encoder.encode_last_id_cursor("test", 42).unwrap();
|
||||
let decoded = encoder.decode_cursor(&encoded).unwrap();
|
||||
assert_eq!(decoded.name, Some("test".to_string()));
|
||||
assert_eq!(decoded.last_id, Some(42));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_none_fields() {
|
||||
let encoder = OpaqueCursorEncoderV2::new(SECRET);
|
||||
let cursor = OpaqueCursorV2 { name: None, last_id: None };
|
||||
let encoded = encoder.encode_cursor(&cursor).unwrap();
|
||||
let decoded = encoder.decode_cursor(&encoded).unwrap();
|
||||
assert_eq!(decoded.name, None);
|
||||
assert_eq!(decoded.last_id, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_zero() {
|
||||
let encoder = OpaqueCursorEncoderV2::new(SECRET);
|
||||
let encoded = encoder.encode_last_id_cursor("zero", 0).unwrap();
|
||||
let decoded = encoder.decode_cursor(&encoded).unwrap();
|
||||
assert_eq!(decoded.name, Some("zero".to_string()));
|
||||
assert_eq!(decoded.last_id, Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_large_id() {
|
||||
let encoder = OpaqueCursorEncoderV2::new(SECRET);
|
||||
let encoded = encoder.encode_last_id_cursor("big", u64::MAX).unwrap();
|
||||
let decoded = encoder.decode_cursor(&encoded).unwrap();
|
||||
assert_eq!(decoded.last_id, Some(u64::MAX));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_many_ids() {
|
||||
let encoder = OpaqueCursorEncoderV2::new(SECRET);
|
||||
for id in [0, 1, 100, 999, 100_000, 1_000_000_000, u64::MAX - 1] {
|
||||
let encoded = encoder.encode_last_id_cursor("batch", id).unwrap();
|
||||
let decoded = encoder.decode_cursor(&encoded).unwrap();
|
||||
assert_eq!(decoded.name, Some("batch".to_string()));
|
||||
assert_eq!(decoded.last_id, Some(id), "roundtrip failed for id={}", id);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn name_preserved_in_roundtrip() {
|
||||
let encoder = OpaqueCursorEncoderV2::new(SECRET);
|
||||
let encoded = encoder.encode_last_id_cursor("media_files_list", 999).unwrap();
|
||||
let decoded = encoder.decode_cursor(&encoded).unwrap();
|
||||
assert_eq!(decoded.name, Some("media_files_list".to_string()));
|
||||
assert_eq!(decoded.last_id, Some(999));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stable_cursor() {
|
||||
let encoder = OpaqueCursorEncoderV2::new("stable_secret");
|
||||
let encoded = encoder.encode_last_id_cursor("media_files_list", 123456).unwrap();
|
||||
assert_eq!(encoded, "0-8bNhoOVA37eZAKwVQ0B8kWq-HLs0x0gQc51TJXjw2hWJ7PNfVqas7WvRnIK80O");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn different_secrets_produce_different_output() {
|
||||
let encoder_a = OpaqueCursorEncoderV2::new("secret_a");
|
||||
let encoder_b = OpaqueCursorEncoderV2::new("secret_b");
|
||||
|
||||
let encoded_a = encoder_a.encode_last_id_cursor("test", 42).unwrap();
|
||||
let encoded_b = encoder_b.encode_last_id_cursor("test", 42).unwrap();
|
||||
|
||||
assert_ne!(encoded_a, encoded_b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrong_secret_fails_to_decode() {
|
||||
let encoder_a = OpaqueCursorEncoderV2::new("secret_a");
|
||||
let encoder_b = OpaqueCursorEncoderV2::new("secret_b");
|
||||
|
||||
let encoded = encoder_a.encode_last_id_cursor("test", 42).unwrap();
|
||||
let result = encoder_b.decode_cursor(&encoded);
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_base64_fails() {
|
||||
let encoder = OpaqueCursorEncoderV2::new(SECRET);
|
||||
let result = encoder.decode_cursor("!!!not-valid-base64!!!");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_string_fails() {
|
||||
let encoder = OpaqueCursorEncoderV2::new(SECRET);
|
||||
let result = encoder.decode_cursor("");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encoded_cursor_is_url_safe() {
|
||||
let encoder = OpaqueCursorEncoderV2::new(SECRET);
|
||||
for id in [0, 1, 42, 999_999_999] {
|
||||
let encoded = encoder.encode_last_id_cursor("safe", id).unwrap();
|
||||
assert!(
|
||||
encoded.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_'),
|
||||
"non-URL-safe character in cursor: {:?}",
|
||||
encoded,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_expecting_name_matches() {
|
||||
let encoder = OpaqueCursorEncoderV2::new(SECRET);
|
||||
let encoded = encoder.encode_last_id_cursor("my_list", 100).unwrap();
|
||||
let decoded = encoder.decode_cursor_expecting_name("my_list", &encoded).unwrap();
|
||||
assert_eq!(decoded.name, Some("my_list".to_string()));
|
||||
assert_eq!(decoded.last_id, Some(100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_expecting_name_mismatch() {
|
||||
let encoder = OpaqueCursorEncoderV2::new(SECRET);
|
||||
let encoded = encoder.encode_last_id_cursor("wrong_name", 100).unwrap();
|
||||
let result = encoder.decode_cursor_expecting_name("expected_name", &encoded);
|
||||
assert!(matches!(result, Err(OpaqueCursorErrorV2::DecodedNameMismatch { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_expecting_name_not_present() {
|
||||
let encoder = OpaqueCursorEncoderV2::new(SECRET);
|
||||
let cursor = OpaqueCursorV2 { name: None, last_id: Some(42) };
|
||||
let encoded = encoder.encode_cursor(&cursor).unwrap();
|
||||
let result = encoder.decode_cursor_expecting_name("anything", &encoded);
|
||||
assert!(matches!(result, Err(OpaqueCursorErrorV2::DecodedNameNotPresent)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deterministic_output() {
|
||||
let encoder = OpaqueCursorEncoderV2::new(SECRET);
|
||||
let a = encoder.encode_last_id_cursor("det", 42).unwrap();
|
||||
let b = encoder.encode_last_id_cursor("det", 42).unwrap();
|
||||
assert_eq!(a, b);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
use std::fmt;
|
||||
use std::string::FromUtf8Error;
|
||||
|
||||
use magic_crypt::MagicCryptError;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum OpaqueCursorErrorV2 {
|
||||
SerdeError(serde_json::Error),
|
||||
MagicCryptError(MagicCryptError),
|
||||
Base64DecodeError(base64::DecodeError),
|
||||
Utf8Error(FromUtf8Error),
|
||||
/// The decoded cursor did not contain a name at all.
|
||||
DecodedNameNotPresent,
|
||||
/// The decoded cursor's name did not match the expected name.
|
||||
DecodedNameMismatch { expected: String, actual: String },
|
||||
}
|
||||
|
||||
impl fmt::Display for OpaqueCursorErrorV2 {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for OpaqueCursorErrorV2 {}
|
||||
|
||||
impl From<serde_json::Error> for OpaqueCursorErrorV2 {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
Self::SerdeError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MagicCryptError> for OpaqueCursorErrorV2 {
|
||||
fn from(err: MagicCryptError) -> Self {
|
||||
Self::MagicCryptError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<base64::DecodeError> for OpaqueCursorErrorV2 {
|
||||
fn from(err: base64::DecodeError) -> Self {
|
||||
Self::Base64DecodeError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FromUtf8Error> for OpaqueCursorErrorV2 {
|
||||
fn from(err: FromUtf8Error) -> Self {
|
||||
Self::Utf8Error(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
/// An opaque cursor payload that encodes pagination state.
|
||||
///
|
||||
/// This gets encrypted and sent to the frontend as an opaque handle.
|
||||
/// The frontend cannot read or forge these cursors.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct OpaqueCursorV2 {
|
||||
/// Name for the cursor type.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(rename = "n")]
|
||||
pub name: Option<String>,
|
||||
|
||||
/// The id of the last ordered element we fetched, so the next page
|
||||
/// will start after (or before) this id depending on sort direction.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(rename = "l")]
|
||||
pub last_id: Option<u64>,
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
# mysql_queries
|
||||
|
||||
SQLx-based MySQL queries for the storyteller-web backend and background jobs.
|
||||
|
||||
## Building
|
||||
|
||||
```
|
||||
SQLX_OFFLINE=true cargo check -p mysql_queries
|
||||
```
|
||||
|
||||
The `SQLX_OFFLINE=true` env var is **required** for compilation. This crate uses the sqlx
|
||||
offline cache (`.sqlx/` directory at the workspace root) to validate queries at compile time
|
||||
without a live database connection.
|
||||
|
||||
## Query Style
|
||||
|
||||
### Prefer `sqlx::query!` and `sqlx::query_as!` macros
|
||||
|
||||
Always use the compile-time checked `sqlx::query!` and `sqlx::query_as!` macros over the
|
||||
runtime `sqlx::query()` and `sqlx::query_as()` functions. The macros validate SQL syntax,
|
||||
column names, and parameter types against the offline cache at compile time.
|
||||
|
||||
```rust
|
||||
// PREFERRED: compile-time checked
|
||||
sqlx::query!(
|
||||
r#"INSERT INTO my_table SET token = ?, name = ?"#,
|
||||
token,
|
||||
name,
|
||||
)
|
||||
.execute(args.mysql_executor)
|
||||
.await?;
|
||||
|
||||
// AVOID: runtime only, no compile-time checking
|
||||
sqlx::query(r#"INSERT INTO my_table SET token = ?, name = ?"#)
|
||||
.bind(token)
|
||||
.bind(name)
|
||||
.execute(args.mysql_executor)
|
||||
.await?;
|
||||
```
|
||||
|
||||
Use runtime `sqlx::query()` only when:
|
||||
- The query involves a table not yet in the offline cache and you cannot run `cargo sqlx prepare`
|
||||
- The query is dynamically constructed (rare)
|
||||
- SELECT queries for new tables where column type metadata isn't cached yet
|
||||
|
||||
### Offline Cache
|
||||
|
||||
When adding new queries for **existing tables**, the `.sqlx/` cache should already have the
|
||||
schema metadata. For **new tables**, you may need to:
|
||||
|
||||
1. Run the migration against a real database
|
||||
2. Run `cargo sqlx prepare` to regenerate the cache
|
||||
3. Or manually create cache entries for simple INSERT/UPDATE queries (they only need parameter count)
|
||||
|
||||
### Table Conventions
|
||||
|
||||
- Primary key `id` columns must be `BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT`. Do not use signed IDs.
|
||||
- Always create tables with `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin`.
|
||||
|
||||
### Error Types
|
||||
|
||||
New queries should return `Result<T, sqlx::Error>` instead of the legacy `AnyhowResult<T>`.
|
||||
The `AnyhowResult` pattern is being phased out.
|
||||
|
||||
### Transaction Support
|
||||
|
||||
Use the executor-generic `Args` pattern for queries that need to work inside transactions:
|
||||
|
||||
```rust
|
||||
pub struct MyQueryArgs<'a, 'c: 'a, E>
|
||||
where E: 'a + Executor<'c, Database = MySql>
|
||||
{
|
||||
pub field: &'a str,
|
||||
pub mysql_executor: E,
|
||||
pub phantom: PhantomData<&'c E>,
|
||||
}
|
||||
```
|
||||
|
||||
This allows the same query function to accept both `&MySqlPool` and `&mut Transaction`.
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use tokens::tokens::staff_audit_logs::StaffAuditLogToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
pub struct StaffAuditLogListItem {
|
||||
pub id: u64,
|
||||
pub token: StaffAuditLogToken,
|
||||
pub audit_action: String,
|
||||
pub maybe_entity_type: Option<String>,
|
||||
pub maybe_entity_token: Option<String>,
|
||||
pub maybe_target_username: Option<String>,
|
||||
pub maybe_target_display_name: Option<String>,
|
||||
pub staff_user_token: Option<UserToken>,
|
||||
pub staff_username: Option<String>,
|
||||
pub staff_display_name: Option<String>,
|
||||
pub staff_ip_address: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub struct ListStaffAuditLogsArgs<'a> {
|
||||
pub maybe_cursor_id: Option<u64>,
|
||||
pub limit: u32,
|
||||
pub mysql_pool: &'a MySqlPool,
|
||||
}
|
||||
|
||||
pub async fn list_staff_audit_logs(
|
||||
args: ListStaffAuditLogsArgs<'_>,
|
||||
) -> Result<Vec<StaffAuditLogListItem>, sqlx::Error> {
|
||||
|
||||
let limit = args.limit as i64;
|
||||
|
||||
let items = match args.maybe_cursor_id {
|
||||
Some(cursor_id) => {
|
||||
sqlx::query_as!(
|
||||
StaffAuditLogListItem,
|
||||
r#"
|
||||
SELECT
|
||||
sal.id,
|
||||
sal.token as `token: StaffAuditLogToken`,
|
||||
sal.audit_action,
|
||||
sal.maybe_entity_type,
|
||||
sal.maybe_entity_token,
|
||||
target_user.username as maybe_target_username,
|
||||
target_user.display_name as maybe_target_display_name,
|
||||
sal.staff_user_token as `staff_user_token: UserToken`,
|
||||
staff_user.username as staff_username,
|
||||
staff_user.display_name as staff_display_name,
|
||||
sal.staff_ip_address,
|
||||
sal.created_at
|
||||
FROM staff_audit_logs sal
|
||||
LEFT JOIN users target_user ON target_user.token = sal.maybe_entity_token
|
||||
LEFT JOIN users staff_user ON staff_user.token = sal.staff_user_token
|
||||
WHERE sal.id < ?
|
||||
ORDER BY sal.id DESC
|
||||
LIMIT ?
|
||||
"#,
|
||||
cursor_id as u64,
|
||||
limit,
|
||||
)
|
||||
.fetch_all(args.mysql_pool)
|
||||
.await?
|
||||
}
|
||||
None => {
|
||||
sqlx::query_as!(
|
||||
StaffAuditLogListItem,
|
||||
r#"
|
||||
SELECT
|
||||
sal.id,
|
||||
sal.token as `token: StaffAuditLogToken`,
|
||||
sal.audit_action,
|
||||
sal.maybe_entity_type,
|
||||
sal.maybe_entity_token,
|
||||
target_user.username as maybe_target_username,
|
||||
target_user.display_name as maybe_target_display_name,
|
||||
sal.staff_user_token as `staff_user_token: UserToken`,
|
||||
staff_user.username as staff_username,
|
||||
staff_user.display_name as staff_display_name,
|
||||
sal.staff_ip_address,
|
||||
sal.created_at
|
||||
FROM staff_audit_logs sal
|
||||
LEFT JOIN users target_user ON target_user.token = sal.maybe_entity_token
|
||||
LEFT JOIN users staff_user ON staff_user.token = sal.staff_user_token
|
||||
ORDER BY sal.id DESC
|
||||
LIMIT ?
|
||||
"#,
|
||||
limit,
|
||||
)
|
||||
.fetch_all(args.mysql_pool)
|
||||
.await?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(items)
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
pub mod insert_staff_audit_log;
|
||||
pub mod list_staff_audit_logs;
|
||||
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
pub struct UserImpersonationRequestListItem {
|
||||
pub id: u64,
|
||||
pub impersonator_user_token: UserToken,
|
||||
pub impersonator_username: String,
|
||||
pub impersonator_display_name: String,
|
||||
pub impersonated_user_token: UserToken,
|
||||
pub impersonated_username: String,
|
||||
pub impersonated_display_name: String,
|
||||
pub is_redeemed: bool,
|
||||
pub is_expired: bool,
|
||||
pub expires_at: DateTime<Utc>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub struct ListUserImpersonationRequestsArgs<'a> {
|
||||
pub maybe_cursor_id: Option<u64>,
|
||||
pub limit: u32,
|
||||
pub mysql_pool: &'a MySqlPool,
|
||||
}
|
||||
|
||||
pub async fn list_user_impersonation_requests(
|
||||
args: ListUserImpersonationRequestsArgs<'_>,
|
||||
) -> Result<Vec<UserImpersonationRequestListItem>, sqlx::Error> {
|
||||
|
||||
let limit = args.limit as i64;
|
||||
|
||||
let items = match args.maybe_cursor_id {
|
||||
Some(cursor_id) => {
|
||||
sqlx::query_as!(
|
||||
UserImpersonationRequestListItem,
|
||||
r#"
|
||||
SELECT
|
||||
uir.id,
|
||||
uir.impersonator_user_token as `impersonator_user_token: UserToken`,
|
||||
impersonator.username as impersonator_username,
|
||||
impersonator.display_name as impersonator_display_name,
|
||||
uir.impersonated_user_token as `impersonated_user_token: UserToken`,
|
||||
impersonated.username as impersonated_username,
|
||||
impersonated.display_name as impersonated_display_name,
|
||||
uir.is_redeemed as `is_redeemed: bool`,
|
||||
(uir.expires_at < NOW()) as `is_expired: bool`,
|
||||
uir.expires_at,
|
||||
uir.created_at,
|
||||
uir.updated_at
|
||||
FROM user_impersonation_requests uir
|
||||
JOIN users impersonator ON impersonator.token = uir.impersonator_user_token
|
||||
JOIN users impersonated ON impersonated.token = uir.impersonated_user_token
|
||||
WHERE uir.id < ?
|
||||
ORDER BY uir.id DESC
|
||||
LIMIT ?
|
||||
"#,
|
||||
cursor_id as u64,
|
||||
limit,
|
||||
)
|
||||
.fetch_all(args.mysql_pool)
|
||||
.await?
|
||||
}
|
||||
None => {
|
||||
sqlx::query_as!(
|
||||
UserImpersonationRequestListItem,
|
||||
r#"
|
||||
SELECT
|
||||
uir.id,
|
||||
uir.impersonator_user_token as `impersonator_user_token: UserToken`,
|
||||
impersonator.username as impersonator_username,
|
||||
impersonator.display_name as impersonator_display_name,
|
||||
uir.impersonated_user_token as `impersonated_user_token: UserToken`,
|
||||
impersonated.username as impersonated_username,
|
||||
impersonated.display_name as impersonated_display_name,
|
||||
uir.is_redeemed as `is_redeemed: bool`,
|
||||
(uir.expires_at < NOW()) as `is_expired: bool`,
|
||||
uir.expires_at,
|
||||
uir.created_at,
|
||||
uir.updated_at
|
||||
FROM user_impersonation_requests uir
|
||||
JOIN users impersonator ON impersonator.token = uir.impersonator_user_token
|
||||
JOIN users impersonated ON impersonated.token = uir.impersonated_user_token
|
||||
ORDER BY uir.id DESC
|
||||
LIMIT ?
|
||||
"#,
|
||||
limit,
|
||||
)
|
||||
.fetch_all(args.mysql_pool)
|
||||
.await?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(items)
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
pub struct UserImpersonationRequestListItem {
|
||||
pub id: u64,
|
||||
pub impersonator_user_token: UserToken,
|
||||
pub impersonator_username: String,
|
||||
pub impersonator_display_name: String,
|
||||
pub impersonated_user_token: UserToken,
|
||||
pub impersonated_username: String,
|
||||
pub impersonated_display_name: String,
|
||||
pub is_redeemed: bool,
|
||||
pub is_expired: bool,
|
||||
pub expires_at: DateTime<Utc>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub struct ListUserImpersonationRequestsArgs<'a> {
|
||||
pub user_token: &'a str,
|
||||
pub maybe_cursor_id: Option<u64>,
|
||||
pub limit: u32,
|
||||
pub mysql_pool: &'a MySqlPool,
|
||||
}
|
||||
|
||||
pub async fn list_user_impersonation_requests_for_user(
|
||||
args: ListUserImpersonationRequestsArgs<'_>,
|
||||
) -> Result<Vec<UserImpersonationRequestListItem>, sqlx::Error> {
|
||||
|
||||
let limit = args.limit as i64;
|
||||
|
||||
let items = match args.maybe_cursor_id {
|
||||
Some(cursor_id) => {
|
||||
sqlx::query_as!(
|
||||
UserImpersonationRequestListItem,
|
||||
r#"
|
||||
SELECT
|
||||
uir.id,
|
||||
uir.impersonator_user_token as `impersonator_user_token: UserToken`,
|
||||
impersonator.username as impersonator_username,
|
||||
impersonator.display_name as impersonator_display_name,
|
||||
uir.impersonated_user_token as `impersonated_user_token: UserToken`,
|
||||
impersonated.username as impersonated_username,
|
||||
impersonated.display_name as impersonated_display_name,
|
||||
uir.is_redeemed as `is_redeemed: bool`,
|
||||
(uir.expires_at < NOW()) as `is_expired: bool`,
|
||||
uir.expires_at,
|
||||
uir.created_at,
|
||||
uir.updated_at
|
||||
FROM user_impersonation_requests uir
|
||||
JOIN users impersonator ON impersonator.token = uir.impersonator_user_token
|
||||
JOIN users impersonated ON impersonated.token = uir.impersonated_user_token
|
||||
WHERE uir.impersonated_user_token = ?
|
||||
AND uir.id < ?
|
||||
ORDER BY uir.id DESC
|
||||
LIMIT ?
|
||||
"#,
|
||||
args.user_token,
|
||||
cursor_id as u64,
|
||||
limit,
|
||||
)
|
||||
.fetch_all(args.mysql_pool)
|
||||
.await?
|
||||
}
|
||||
None => {
|
||||
sqlx::query_as!(
|
||||
UserImpersonationRequestListItem,
|
||||
r#"
|
||||
SELECT
|
||||
uir.id,
|
||||
uir.impersonator_user_token as `impersonator_user_token: UserToken`,
|
||||
impersonator.username as impersonator_username,
|
||||
impersonator.display_name as impersonator_display_name,
|
||||
uir.impersonated_user_token as `impersonated_user_token: UserToken`,
|
||||
impersonated.username as impersonated_username,
|
||||
impersonated.display_name as impersonated_display_name,
|
||||
uir.is_redeemed as `is_redeemed: bool`,
|
||||
(uir.expires_at < NOW()) as `is_expired: bool`,
|
||||
uir.expires_at,
|
||||
uir.created_at,
|
||||
uir.updated_at
|
||||
FROM user_impersonation_requests uir
|
||||
JOIN users impersonator ON impersonator.token = uir.impersonator_user_token
|
||||
JOIN users impersonated ON impersonated.token = uir.impersonated_user_token
|
||||
WHERE uir.impersonated_user_token = ?
|
||||
ORDER BY uir.id DESC
|
||||
LIMIT ?
|
||||
"#,
|
||||
args.user_token,
|
||||
limit,
|
||||
)
|
||||
.fetch_all(args.mysql_pool)
|
||||
.await?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(items)
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod insert_user_impersonation_request;
|
||||
pub mod list_user_impersonation_requests;
|
||||
pub mod list_user_impersonation_requests_for_user;
|
||||
pub mod lookup_user_impersonation_request;
|
||||
pub mod mark_impersonation_token_as_redeemed;
|
||||
|
||||
+24
-20
@@ -1,20 +1,27 @@
|
||||
use anyhow::anyhow;
|
||||
use sqlx::MySqlPool;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use sqlx::{Executor, MySql};
|
||||
|
||||
use errors::AnyhowResult;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
pub struct SetUserBanStatsArgs<'a> {
|
||||
pub struct SetUserBanStatusArgs<'a, 'c: 'a, E>
|
||||
where E: 'a + Executor<'c, Database = MySql>
|
||||
{
|
||||
pub subject_user_token: &'a UserToken,
|
||||
pub is_banned: bool,
|
||||
pub mod_user_token: &'a UserToken,
|
||||
pub maybe_mod_comments: Option<&'a str>,
|
||||
pub mysql_pool: &'a MySqlPool,
|
||||
pub mysql_executor: E,
|
||||
pub phantom: PhantomData<&'c E>,
|
||||
}
|
||||
|
||||
pub async fn set_user_ban_status(args: SetUserBanStatsArgs<'_>) -> AnyhowResult<()> {
|
||||
let query_result = sqlx::query!(
|
||||
r#"
|
||||
pub async fn set_user_ban_status<'a, 'c, E>(
|
||||
args: SetUserBanStatusArgs<'a, 'c, E>,
|
||||
) -> Result<(), sqlx::Error>
|
||||
where E: 'a + Executor<'c, Database = MySql>
|
||||
{
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE users
|
||||
SET
|
||||
is_banned = ?,
|
||||
@@ -24,17 +31,14 @@ SET
|
||||
|
||||
WHERE users.token = ?
|
||||
LIMIT 1
|
||||
"#,
|
||||
args.is_banned,
|
||||
args.maybe_mod_comments,
|
||||
args.mod_user_token,
|
||||
args.subject_user_token,
|
||||
)
|
||||
.execute(args.mysql_pool)
|
||||
.await;
|
||||
"#,
|
||||
args.is_banned,
|
||||
args.maybe_mod_comments,
|
||||
args.mod_user_token,
|
||||
args.subject_user_token,
|
||||
)
|
||||
.execute(args.mysql_executor)
|
||||
.await?;
|
||||
|
||||
match query_result {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(anyhow!("error with query: {:?}", err)),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -20,6 +20,22 @@ pub enum StaffAuditAction {
|
||||
/// Staff redeemed an impersonation request, creating a session.
|
||||
#[serde(rename = "impersonate_user_redeem")]
|
||||
ImpersonateUserRedeem,
|
||||
|
||||
/// Staff banned a user.
|
||||
#[serde(rename = "ban_user")]
|
||||
BanUser,
|
||||
|
||||
/// Staff unbanned a user.
|
||||
#[serde(rename = "unban_user")]
|
||||
UnbanUser,
|
||||
|
||||
/// Staff added banked balance to a wallet.
|
||||
#[serde(rename = "add_wallet_banked_balance")]
|
||||
AddWalletBankedBalance,
|
||||
|
||||
/// Staff sent a manual alert via the pager system.
|
||||
#[serde(rename = "send_alert")]
|
||||
SendAlert,
|
||||
}
|
||||
|
||||
impl_enum_display_and_debug_using_to_str!(StaffAuditAction);
|
||||
@@ -31,6 +47,10 @@ impl StaffAuditAction {
|
||||
match self {
|
||||
Self::ImpersonateUserRequest => "impersonate_user_request",
|
||||
Self::ImpersonateUserRedeem => "impersonate_user_redeem",
|
||||
Self::BanUser => "ban_user",
|
||||
Self::UnbanUser => "unban_user",
|
||||
Self::AddWalletBankedBalance => "add_wallet_banked_balance",
|
||||
Self::SendAlert => "send_alert",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +58,10 @@ impl StaffAuditAction {
|
||||
match value {
|
||||
"impersonate_user_request" => Ok(Self::ImpersonateUserRequest),
|
||||
"impersonate_user_redeem" => Ok(Self::ImpersonateUserRedeem),
|
||||
"ban_user" => Ok(Self::BanUser),
|
||||
"unban_user" => Ok(Self::UnbanUser),
|
||||
"add_wallet_banked_balance" => Ok(Self::AddWalletBankedBalance),
|
||||
"send_alert" => Ok(Self::SendAlert),
|
||||
_ => Err(format!("invalid StaffAuditAction value: {:?}", value)),
|
||||
}
|
||||
}
|
||||
@@ -46,6 +70,10 @@ impl StaffAuditAction {
|
||||
BTreeSet::from([
|
||||
Self::ImpersonateUserRequest,
|
||||
Self::ImpersonateUserRedeem,
|
||||
Self::BanUser,
|
||||
Self::UnbanUser,
|
||||
Self::AddWalletBankedBalance,
|
||||
Self::SendAlert,
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -62,24 +90,36 @@ mod tests {
|
||||
fn test_serialization() {
|
||||
assert_serialization(StaffAuditAction::ImpersonateUserRequest, "impersonate_user_request");
|
||||
assert_serialization(StaffAuditAction::ImpersonateUserRedeem, "impersonate_user_redeem");
|
||||
assert_serialization(StaffAuditAction::BanUser, "ban_user");
|
||||
assert_serialization(StaffAuditAction::UnbanUser, "unban_user");
|
||||
assert_serialization(StaffAuditAction::AddWalletBankedBalance, "add_wallet_banked_balance");
|
||||
assert_serialization(StaffAuditAction::SendAlert, "send_alert");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_str() {
|
||||
assert_eq!(StaffAuditAction::ImpersonateUserRequest.to_str(), "impersonate_user_request");
|
||||
assert_eq!(StaffAuditAction::ImpersonateUserRedeem.to_str(), "impersonate_user_redeem");
|
||||
assert_eq!(StaffAuditAction::BanUser.to_str(), "ban_user");
|
||||
assert_eq!(StaffAuditAction::UnbanUser.to_str(), "unban_user");
|
||||
assert_eq!(StaffAuditAction::AddWalletBankedBalance.to_str(), "add_wallet_banked_balance");
|
||||
assert_eq!(StaffAuditAction::SendAlert.to_str(), "send_alert");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str() {
|
||||
assert_eq!(StaffAuditAction::from_str("impersonate_user_request").unwrap(), StaffAuditAction::ImpersonateUserRequest);
|
||||
assert_eq!(StaffAuditAction::from_str("impersonate_user_redeem").unwrap(), StaffAuditAction::ImpersonateUserRedeem);
|
||||
assert_eq!(StaffAuditAction::from_str("ban_user").unwrap(), StaffAuditAction::BanUser);
|
||||
assert_eq!(StaffAuditAction::from_str("unban_user").unwrap(), StaffAuditAction::UnbanUser);
|
||||
assert_eq!(StaffAuditAction::from_str("add_wallet_banked_balance").unwrap(), StaffAuditAction::AddWalletBankedBalance);
|
||||
assert_eq!(StaffAuditAction::from_str("send_alert").unwrap(), StaffAuditAction::SendAlert);
|
||||
assert!(StaffAuditAction::from_str("invalid").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
const EXPECTED_COUNT: usize = 2;
|
||||
const EXPECTED_COUNT: usize = 6;
|
||||
assert_eq!(StaffAuditAction::all_variants().len(), EXPECTED_COUNT);
|
||||
}
|
||||
}
|
||||
|
||||
+11
-1
@@ -16,6 +16,10 @@ pub enum StaffAuditEntityType {
|
||||
/// A user account.
|
||||
#[serde(rename = "user")]
|
||||
User,
|
||||
|
||||
/// A wallet.
|
||||
#[serde(rename = "wallet")]
|
||||
Wallet,
|
||||
}
|
||||
|
||||
impl_enum_display_and_debug_using_to_str!(StaffAuditEntityType);
|
||||
@@ -26,12 +30,14 @@ impl StaffAuditEntityType {
|
||||
pub fn to_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::User => "user",
|
||||
Self::Wallet => "wallet",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(value: &str) -> Result<Self, String> {
|
||||
match value {
|
||||
"user" => Ok(Self::User),
|
||||
"wallet" => Ok(Self::Wallet),
|
||||
_ => Err(format!("invalid StaffAuditEntityType value: {:?}", value)),
|
||||
}
|
||||
}
|
||||
@@ -39,6 +45,7 @@ impl StaffAuditEntityType {
|
||||
pub fn all_variants() -> BTreeSet<Self> {
|
||||
BTreeSet::from([
|
||||
Self::User,
|
||||
Self::Wallet,
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -54,22 +61,25 @@ mod tests {
|
||||
#[test]
|
||||
fn test_serialization() {
|
||||
assert_serialization(StaffAuditEntityType::User, "user");
|
||||
assert_serialization(StaffAuditEntityType::Wallet, "wallet");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_str() {
|
||||
assert_eq!(StaffAuditEntityType::User.to_str(), "user");
|
||||
assert_eq!(StaffAuditEntityType::Wallet.to_str(), "wallet");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str() {
|
||||
assert_eq!(StaffAuditEntityType::from_str("user").unwrap(), StaffAuditEntityType::User);
|
||||
assert_eq!(StaffAuditEntityType::from_str("wallet").unwrap(), StaffAuditEntityType::Wallet);
|
||||
assert!(StaffAuditEntityType::from_str("invalid").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
const EXPECTED_COUNT: usize = 1;
|
||||
const EXPECTED_COUNT: usize = 2;
|
||||
assert_eq!(StaffAuditEntityType::all_variants().len(), EXPECTED_COUNT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ memory_caching = { path = "../../../lib/caching/memory_caching" }
|
||||
migration = { path = "../../../schema/database/migration" }
|
||||
mimetypes.workspace = true
|
||||
mysql_queries.workspace = true
|
||||
opaque_cursors.workspace = true
|
||||
pager.workspace = true
|
||||
password = { path = "../../../lib/password" }
|
||||
rootly_client.workspace = true
|
||||
|
||||
@@ -48,6 +48,7 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Seedance2p0,
|
||||
full_name: None,
|
||||
text_prompt_supported: Some(true),
|
||||
@@ -79,11 +80,14 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// TODO(bt,2026-04-10): Veo 2 image-to-video doesn't support aspect ratio
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Veo2,
|
||||
full_name: Some("Google Veo 2".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
// TODO(bt,2026-04-10): Veo 2 image-to-video doesn't support aspect ratio
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::Auto,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
@@ -97,6 +101,7 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Veo3,
|
||||
full_name: Some("Google Veo 3".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
@@ -105,8 +110,9 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::Auto, // TODO: Only for image-to-video
|
||||
]),
|
||||
// TODO: image-to-video aspect ratio options
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::SevenTwentyP,
|
||||
@@ -137,6 +143,7 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Veo3p1,
|
||||
full_name: Some("Google Veo 3.1".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
@@ -161,6 +168,7 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Veo3p1Fast,
|
||||
full_name: Some("Google Veo 3.1 Fast".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
@@ -185,6 +193,7 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Kling16Pro,
|
||||
full_name: Some("Kling 1.6 Pro".to_string()),
|
||||
starting_keyframe_supported: Some(true),
|
||||
@@ -201,6 +210,7 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Kling21Pro,
|
||||
full_name: Some("Kling 2.1 Pro".to_string()),
|
||||
starting_keyframe_supported: Some(true),
|
||||
@@ -217,6 +227,7 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Kling21Master,
|
||||
full_name: Some("Kling 2.1 Master".to_string()),
|
||||
starting_keyframe_supported: Some(true),
|
||||
@@ -232,6 +243,7 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Kling2p5TurboPro,
|
||||
full_name: Some("Kling 2.5 Turbo Pro".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
@@ -249,6 +261,7 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Kling2p6Pro,
|
||||
full_name: Some("Kling 2.6 Pro".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
@@ -266,6 +279,7 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Kling3p0Pro,
|
||||
full_name: Some("Kling 3.0 Pro".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
@@ -285,6 +299,7 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Kling3p0Standard,
|
||||
full_name: Some("Kling 3.0 Standard".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
@@ -304,6 +319,7 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Seedance10Lite,
|
||||
full_name: Some("Seedance 1.0 Lite".to_string()),
|
||||
starting_keyframe_supported: Some(true),
|
||||
@@ -330,6 +346,7 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Sora2,
|
||||
full_name: Some("Sora 2".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
@@ -347,6 +364,7 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Sora2Pro,
|
||||
full_name: Some("Sora 2 Pro".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
|
||||
@@ -86,6 +86,10 @@ use crate::http_server::endpoints::media_files::upsert_upload::write_error::Medi
|
||||
use crate::http_server::endpoints::media_files::upsert_upload::write_scene_file::write_scene_file_media_file_handler::*;
|
||||
use crate::http_server::endpoints::model_download::enqueue_gptsovits_model_download_handler::*;
|
||||
use crate::http_server::endpoints::moderation::user_feature_flags::edit_user_feature_flags_handler::*;
|
||||
use crate::http_server::endpoints::moderation::staff_audit_logs::moderator_list_staff_audit_logs_handler::*;
|
||||
use crate::http_server::endpoints::moderation::user_bans::moderation_ban_user_handler::*;
|
||||
use crate::http_server::endpoints::moderation::user_sessions::moderator_list_user_session_impersonation_requests_for_user_handler::*;
|
||||
use crate::http_server::endpoints::moderation::user_sessions::moderator_list_user_session_impersonation_requests_handler::*;
|
||||
use crate::http_server::endpoints::moderation::user_sessions::moderator_user_session_impersonation_request_handler::*;
|
||||
use crate::http_server::endpoints::prompts::get_prompt_handler::*;
|
||||
use artcraft_api_defs::common::responses::job_details::JobDetailsLipsyncRequest;
|
||||
@@ -508,6 +512,10 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
crate::http_server::endpoints::moderation::wallets::moderator_add_banked_balance_to_wallet_handler::moderator_add_banked_balance_to_wallet_handler,
|
||||
crate::http_server::endpoints::moderation::wallets::moderator_create_wallet_for_user_handler::moderator_create_wallet_for_user_handler,
|
||||
crate::http_server::endpoints::moderation::wallets::moderator_get_wallet_handler::moderator_get_wallet_handler,
|
||||
crate::http_server::endpoints::moderation::staff_audit_logs::moderator_list_staff_audit_logs_handler::moderator_list_staff_audit_logs_handler,
|
||||
crate::http_server::endpoints::moderation::user_bans::moderation_ban_user_handler::moderation_ban_user_handler,
|
||||
crate::http_server::endpoints::moderation::user_sessions::moderator_list_user_session_impersonation_requests_for_user_handler::moderator_list_user_session_impersonation_requests_for_user_handler,
|
||||
crate::http_server::endpoints::moderation::user_sessions::moderator_list_user_session_impersonation_requests_handler::moderator_list_user_session_impersonation_requests_handler,
|
||||
crate::http_server::endpoints::moderation::user_sessions::moderator_user_session_impersonation_request_handler::moderator_user_session_impersonation_request_handler,
|
||||
// Credits
|
||||
crate::http_server::endpoints::credits::get_session_credits_handler::get_session_credits_handler,
|
||||
@@ -594,6 +602,7 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
WeightsDefaultCoverInfo,
|
||||
|
||||
// Endpoint API types
|
||||
AllImpersonationRequestResponse,
|
||||
AppStateError,
|
||||
AppStateLegacyPermissionFlags,
|
||||
AppStatePermissions,
|
||||
@@ -887,6 +896,13 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
ListFeaturedWeightsError,
|
||||
ListFeaturedWeightsQueryParams,
|
||||
ListFeaturedWeightsSuccessResponse,
|
||||
ListAllImpersonationRequestsQueryParams,
|
||||
ListAllImpersonationRequestsSuccessResponse,
|
||||
ListImpersonationRequestsPathInfo,
|
||||
ListStaffAuditLogsQueryParams,
|
||||
ListStaffAuditLogsSuccessResponse,
|
||||
ListImpersonationRequestsQueryParams,
|
||||
ListUserImpersonationRequestsSuccessResponse,
|
||||
ListMediaFilesByBatchPathInfo,
|
||||
ListMediaFilesByBatchSuccessResponse,
|
||||
ListMediaFilesForUserPathInfo,
|
||||
@@ -933,6 +949,8 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
MediaFilesByBatchListItem,
|
||||
ModelWeightForList,
|
||||
ModelWeightSearchResult,
|
||||
ModerationBanUserRequest,
|
||||
ModerationBanUserSuccessResponse,
|
||||
ModerationImpersonateRequest,
|
||||
ModerationImpersonateSuccessResponse,
|
||||
ModernInferenceQueueStats,
|
||||
@@ -950,6 +968,7 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
RenameMediaFileRequest,
|
||||
RequestDetailsResponse,
|
||||
ResultDetailsResponse,
|
||||
StaffAuditLogResponse,
|
||||
SearchFeaturedMediaFileListItem,
|
||||
SearchFeaturedMediaFilesError,
|
||||
SearchFeaturedMediaFilesQueryParams,
|
||||
@@ -995,6 +1014,7 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
UploadEngineAssetMediaSuccessResponse,
|
||||
UploadImageMediaFileForm,
|
||||
UploadImageMediaFileSuccessResponse,
|
||||
UserImpersonationRequestResponse,
|
||||
UploadMediaSuccessResponse,
|
||||
UploadNewEngineAssetFileForm,
|
||||
UploadNewEngineAssetSuccessResponse,
|
||||
|
||||
+4
-14
@@ -148,13 +148,9 @@ pub async fn generate_veo_2_image_to_video_handler(
|
||||
.map(|prompt| prompt.trim())
|
||||
.unwrap_or_else(|| "");
|
||||
|
||||
let aspect_ratio = match &request.aspect_ratio {
|
||||
Some(GenerateVeo2AspectRatio::Auto) => Veo2AspectRatio::Auto,
|
||||
Some(GenerateVeo2AspectRatio::AutoPreferPortrait) => Veo2AspectRatio::AutoPreferPortrait,
|
||||
Some(GenerateVeo2AspectRatio::WideSixteenNine) => Veo2AspectRatio::WideSixteenNine,
|
||||
Some(GenerateVeo2AspectRatio::TallNineSixteen) => Veo2AspectRatio::TallNineSixteen,
|
||||
None => Veo2AspectRatio::WideSixteenNine, // Default to 16:9
|
||||
};
|
||||
if let Some(aspect_ratio) = &request.aspect_ratio {
|
||||
warn!("Aspect ratio is not read in Veo 2 image-to-video");
|
||||
}
|
||||
|
||||
let duration = match &request.duration {
|
||||
Some(GenerateVeo2Duration::FiveSeconds) => Veo2Duration::FiveSeconds,
|
||||
@@ -169,7 +165,6 @@ pub async fn generate_veo_2_image_to_video_handler(
|
||||
webhook_url: &server_state.fal.webhook_url,
|
||||
duration,
|
||||
prompt,
|
||||
aspect_ratio,
|
||||
api_key: &server_state.fal.api_key,
|
||||
};
|
||||
|
||||
@@ -222,12 +217,7 @@ pub async fn generate_veo_2_image_to_video_handler(
|
||||
maybe_negative_prompt: None,
|
||||
maybe_other_args: None,
|
||||
maybe_generation_mode: Some(CommonGenerationMode::Keyframe), // TODO: This endpoint only supports keyframes for now
|
||||
maybe_aspect_ratio: request.aspect_ratio.as_ref().map(|ar| match ar {
|
||||
GenerateVeo2AspectRatio::Auto => CommonAspectRatio::Auto,
|
||||
GenerateVeo2AspectRatio::AutoPreferPortrait => CommonAspectRatio::Auto,
|
||||
GenerateVeo2AspectRatio::WideSixteenNine => CommonAspectRatio::WideSixteenByNine,
|
||||
GenerateVeo2AspectRatio::TallNineSixteen => CommonAspectRatio::TallNineBySixteen,
|
||||
}),
|
||||
maybe_aspect_ratio: None, // NB: The aspect ratio is not read for Veo 2 image-to-video
|
||||
maybe_resolution: None,
|
||||
maybe_batch_count: None,
|
||||
maybe_generate_audio: None,
|
||||
|
||||
+15
-3
@@ -11,7 +11,7 @@ use crate::state::server_state::ServerState;
|
||||
use actix_web::web::Json;
|
||||
use actix_web::{web, HttpRequest};
|
||||
use anyhow::anyhow;
|
||||
use artcraft_api_defs::generate::video::generate_veo_3_fast_image_to_video::{GenerateVeo3FastDuration, GenerateVeo3FastImageToVideoRequest, GenerateVeo3FastImageToVideoResponse, GenerateVeo3FastResolution};
|
||||
use artcraft_api_defs::generate::video::generate_veo_3_fast_image_to_video::{GenerateVeo3FastAspectRatio, GenerateVeo3FastDuration, GenerateVeo3FastImageToVideoRequest, GenerateVeo3FastImageToVideoResponse, GenerateVeo3FastResolution};
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use enums::by_table::prompts::prompt_type::PromptType;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
@@ -19,7 +19,7 @@ use enums::common::generation::common_model_type::CommonModelType;
|
||||
use enums::common::visibility::Visibility;
|
||||
use enums::common::generation::common_generation_mode::CommonGenerationMode;
|
||||
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
|
||||
use fal_client::requests::webhook::video::image::enqueue_veo_3_fast_image_to_video_webhook::{enqueue_veo_3_fast_image_to_video_webhook, Veo3FastArgs, Veo3FastDuration, Veo3FastResolution};
|
||||
use fal_client::requests::webhook::video::image::enqueue_veo_3_fast_image_to_video_webhook::{enqueue_veo_3_fast_image_to_video_webhook, Veo3FastArgs, Veo3FastAspectRatio, Veo3FastDuration, Veo3FastResolution};
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use log::{error, info, warn};
|
||||
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::insert_generic_inference_job_for_fal_queue;
|
||||
@@ -141,6 +141,13 @@ pub async fn generate_veo_3_fast_image_to_video_handler(
|
||||
.map(|prompt| prompt.trim())
|
||||
.unwrap_or_else(|| "");
|
||||
|
||||
let aspect_ratio = match &request.aspect_ratio {
|
||||
Some(GenerateVeo3FastAspectRatio::Auto) => Veo3FastAspectRatio::Auto,
|
||||
Some(GenerateVeo3FastAspectRatio::SixteenByNine) => Veo3FastAspectRatio::WideSixteenNine,
|
||||
Some(GenerateVeo3FastAspectRatio::NineBySixteen) => Veo3FastAspectRatio::TallNineSixteen,
|
||||
None => Veo3FastAspectRatio::Auto,
|
||||
};
|
||||
|
||||
let resolution = match &request.resolution {
|
||||
Some(GenerateVeo3FastResolution::SevenTwentyP) => Veo3FastResolution::SevenTwentyP,
|
||||
Some(GenerateVeo3FastResolution::TenEightyP) => Veo3FastResolution::TenEightyP,
|
||||
@@ -158,6 +165,7 @@ pub async fn generate_veo_3_fast_image_to_video_handler(
|
||||
let args = Veo3FastArgs {
|
||||
image_url: media_links.cdn_url,
|
||||
prompt,
|
||||
aspect_ratio,
|
||||
duration,
|
||||
resolution,
|
||||
generate_audio,
|
||||
@@ -214,7 +222,11 @@ pub async fn generate_veo_3_fast_image_to_video_handler(
|
||||
maybe_negative_prompt: None,
|
||||
maybe_other_args: None,
|
||||
maybe_generation_mode: Some(CommonGenerationMode::Keyframe), // TODO: This endpoint only supports keyframes for now
|
||||
maybe_aspect_ratio: None,
|
||||
maybe_aspect_ratio: request.aspect_ratio.as_ref().map(|ar| match ar {
|
||||
GenerateVeo3FastAspectRatio::Auto => enums::common::generation::common_aspect_ratio::CommonAspectRatio::Auto,
|
||||
GenerateVeo3FastAspectRatio::SixteenByNine => enums::common::generation::common_aspect_ratio::CommonAspectRatio::WideSixteenByNine,
|
||||
GenerateVeo3FastAspectRatio::NineBySixteen => enums::common::generation::common_aspect_ratio::CommonAspectRatio::TallNineBySixteen,
|
||||
}),
|
||||
maybe_resolution: None,
|
||||
maybe_batch_count: None,
|
||||
maybe_generate_audio: Some(generate_audio),
|
||||
|
||||
+14
-13
@@ -14,13 +14,13 @@ use anyhow::anyhow;
|
||||
use artcraft_api_defs::generate::video::generate_veo_3_image_to_video::{GenerateVeo3AspectRatio, GenerateVeo3Duration, GenerateVeo3ImageToVideoRequest, GenerateVeo3ImageToVideoResponse, GenerateVeo3Resolution};
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use enums::by_table::prompts::prompt_type::PromptType;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::common::generation::common_model_type::CommonModelType;
|
||||
use enums::common::visibility::Visibility;
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_generation_mode::CommonGenerationMode;
|
||||
use enums::common::generation::common_model_type::CommonModelType;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::common::visibility::Visibility;
|
||||
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
|
||||
use fal_client::requests::webhook::video::image::enqueue_veo_3_image_to_video_webhook::{enqueue_veo_3_image_to_video_webhook, Veo3Args, Veo3AspectRatio, Veo3Duration, Veo3Resolution};
|
||||
use fal_client::requests::webhook::video::image::enqueue_veo_3_image_to_video_webhook::{enqueue_veo_3_image_to_video_webhook, Veo3Args, Veo3I2vAspectRatio, Veo3I2vDuration, Veo3I2vResolution};
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use log::{error, info, warn};
|
||||
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::insert_generic_inference_job_for_fal_queue;
|
||||
@@ -143,21 +143,22 @@ pub async fn generate_veo_3_image_to_video_handler(
|
||||
.unwrap_or_else(|| "");
|
||||
|
||||
let aspect_ratio = match &request.aspect_ratio {
|
||||
Some(GenerateVeo3AspectRatio::WideSixteenNine) => Veo3AspectRatio::WideSixteenNine,
|
||||
Some(GenerateVeo3AspectRatio::TallNineSixteen) => Veo3AspectRatio::TallNineSixteen,
|
||||
Some(GenerateVeo3AspectRatio::Square) => Veo3AspectRatio::Square,
|
||||
None => Veo3AspectRatio::WideSixteenNine,
|
||||
Some(GenerateVeo3AspectRatio::WideSixteenNine) => Veo3I2vAspectRatio::WideSixteenNine,
|
||||
Some(GenerateVeo3AspectRatio::TallNineSixteen) => Veo3I2vAspectRatio::TallNineSixteen,
|
||||
// Square is not supported by Veo 3 i2v — fall back to Auto.
|
||||
Some(GenerateVeo3AspectRatio::Square) => Veo3I2vAspectRatio::Auto,
|
||||
None => Veo3I2vAspectRatio::WideSixteenNine,
|
||||
};
|
||||
|
||||
let resolution = match &request.resolution {
|
||||
Some(GenerateVeo3Resolution::SevenTwentyP) => Veo3Resolution::SevenTwentyP,
|
||||
Some(GenerateVeo3Resolution::TenEightyP) => Veo3Resolution::TenEightyP,
|
||||
None => Veo3Resolution::Default,
|
||||
Some(GenerateVeo3Resolution::SevenTwentyP) => Veo3I2vResolution::SevenTwentyP,
|
||||
Some(GenerateVeo3Resolution::TenEightyP) => Veo3I2vResolution::TenEightyP,
|
||||
None => Veo3I2vResolution::Default,
|
||||
};
|
||||
|
||||
let duration = match &request.duration {
|
||||
Some(GenerateVeo3Duration::EightSeconds) => Veo3Duration::EightSeconds,
|
||||
None => Veo3Duration::EightSeconds,
|
||||
Some(GenerateVeo3Duration::EightSeconds) => Veo3I2vDuration::EightSeconds,
|
||||
None => Veo3I2vDuration::EightSeconds,
|
||||
};
|
||||
|
||||
let generate_audio = request.generate_audio
|
||||
|
||||
+29
-6
@@ -1,3 +1,4 @@
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::Json;
|
||||
@@ -5,11 +6,14 @@ use actix_web::{web, HttpRequest};
|
||||
use log::warn;
|
||||
|
||||
use artcraft_api_defs::moderation::alerts::moderation_send_alert::{ModerationSendAlertRequest, ModerationSendAlertResponse, ModerationSendAlertUrgency};
|
||||
use pager::client::pager::Pager;
|
||||
use pager::notification::notification_details::NotificationDetails;
|
||||
use enums::by_table::staff_audit_logs::staff_audit_action::StaffAuditAction;
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use mysql_queries::queries::staff_audit_logs::insert_staff_audit_log::{
|
||||
insert_staff_audit_log, InsertStaffAuditLogArgs,
|
||||
};
|
||||
use pager::notification::notification_details_builder::NotificationDetailsBuilder;
|
||||
use pager::notification::notification_urgency::NotificationUrgency;
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, UseDatabase};
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
@@ -29,11 +33,16 @@ pub async fn moderation_send_alert_handler(
|
||||
http_request: HttpRequest,
|
||||
request: Json<ModerationSendAlertRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<ModerationSendAlertResponse>, CommonWebError> {
|
||||
) -> Result<Json<ModerationSendAlertResponse>, AdvancedCommonWebError> {
|
||||
|
||||
let user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
|
||||
.await
|
||||
.map_err(|_| CommonWebError::NotAuthorized)?;
|
||||
.map_err(|err| {
|
||||
warn!("Moderator check failed: {:?}", err);
|
||||
AdvancedCommonWebError::NotAuthorized
|
||||
})?;
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
let title = request.title.clone()
|
||||
.unwrap_or_else(|| "Moderation Alert".to_string());
|
||||
@@ -68,9 +77,23 @@ pub async fn moderation_send_alert_handler(
|
||||
.enqueue_page(notification)
|
||||
.map_err(|err| {
|
||||
warn!("moderation_send_alert error: {:?}", err);
|
||||
CommonWebError::ServerError
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
// Insert staff audit log.
|
||||
let _audit_token = insert_staff_audit_log(InsertStaffAuditLogArgs {
|
||||
audit_action: StaffAuditAction::SendAlert,
|
||||
maybe_entity_type: None,
|
||||
maybe_entity_token: None,
|
||||
staff_user_token: &user_session.user_token,
|
||||
actor_ip_address: &ip_address,
|
||||
mysql_executor: &server_state.mysql_pool,
|
||||
phantom: PhantomData,
|
||||
}).await.map_err(|err| {
|
||||
warn!("Failed to insert staff audit log: {:?}", err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
Ok(Json(ModerationSendAlertResponse {
|
||||
success: true,
|
||||
}))
|
||||
|
||||
@@ -2,6 +2,7 @@ pub mod alerts;
|
||||
pub mod info;
|
||||
pub mod ip_bans;
|
||||
pub mod jobs;
|
||||
pub mod staff_audit_logs;
|
||||
pub mod user;
|
||||
pub mod user_bans;
|
||||
pub mod user_feature_flags;
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
pub mod moderator_list_staff_audit_logs_handler;
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::{Json, Query};
|
||||
use actix_web::{web, HttpRequest};
|
||||
use chrono::{DateTime, Utc};
|
||||
use log::warn;
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
use mysql_queries::queries::staff_audit_logs::list_staff_audit_logs::{
|
||||
list_staff_audit_logs, ListStaffAuditLogsArgs,
|
||||
};
|
||||
use tokens::tokens::staff_audit_logs::StaffAuditLogToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_moderator::{
|
||||
require_moderator, UseDatabase,
|
||||
};
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
const CURSOR_NAME: &str = "modstfaud";
|
||||
const DEFAULT_LIMIT: u32 = 50;
|
||||
const MAX_LIMIT: u32 = 1000;
|
||||
|
||||
// --- Request ---
|
||||
|
||||
#[derive(Deserialize, ToSchema, IntoParams)]
|
||||
pub struct ListStaffAuditLogsQueryParams {
|
||||
pub cursor: Option<String>,
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
// --- Response ---
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ListStaffAuditLogsSuccessResponse {
|
||||
pub success: bool,
|
||||
pub audit_logs: Vec<StaffAuditLogResponse>,
|
||||
pub maybe_cursor: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct StaffAuditLogResponse {
|
||||
pub token: StaffAuditLogToken,
|
||||
pub audit_action: String,
|
||||
pub maybe_entity_type: Option<String>,
|
||||
pub maybe_entity_token: Option<String>,
|
||||
pub maybe_target_username: Option<String>,
|
||||
pub maybe_target_display_name: Option<String>,
|
||||
pub staff_user_token: Option<UserToken>,
|
||||
pub staff_username: Option<String>,
|
||||
pub staff_display_name: Option<String>,
|
||||
pub staff_ip_address: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
// --- Handler ---
|
||||
|
||||
/// List all staff audit logs. Moderators only.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
tag = "Moderation",
|
||||
path = "/v1/moderation/staff_audit_logs/list",
|
||||
params(
|
||||
ListStaffAuditLogsQueryParams,
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Success", body = ListStaffAuditLogsSuccessResponse),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 500, description = "Server error"),
|
||||
),
|
||||
)]
|
||||
pub async fn moderator_list_staff_audit_logs_handler(
|
||||
http_request: HttpRequest,
|
||||
query: Query<ListStaffAuditLogsQueryParams>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<ListStaffAuditLogsSuccessResponse>, AdvancedCommonWebError> {
|
||||
|
||||
let _user_session = require_moderator(
|
||||
&http_request,
|
||||
&server_state,
|
||||
UseDatabase::GrabNewConnection,
|
||||
).await.map_err(|err| {
|
||||
warn!("Moderator check failed: {:?}", err);
|
||||
AdvancedCommonWebError::NotAuthorized
|
||||
})?;
|
||||
|
||||
let limit = query.limit
|
||||
.unwrap_or(DEFAULT_LIMIT)
|
||||
.min(MAX_LIMIT);
|
||||
|
||||
let maybe_cursor_id = match &query.cursor {
|
||||
None => None,
|
||||
Some(cursor_str) => {
|
||||
let decoded = server_state.opaque_cursors
|
||||
.decode_cursor_expecting_name(CURSOR_NAME, cursor_str)
|
||||
.map_err(|err| {
|
||||
warn!("Failed to decode cursor: {:?}", err);
|
||||
AdvancedCommonWebError::BadInputWithSimpleMessage(
|
||||
"Invalid cursor".to_string())
|
||||
})?;
|
||||
decoded.last_id
|
||||
}
|
||||
};
|
||||
|
||||
let records = list_staff_audit_logs(
|
||||
ListStaffAuditLogsArgs {
|
||||
maybe_cursor_id,
|
||||
limit,
|
||||
mysql_pool: &server_state.mysql_pool,
|
||||
},
|
||||
).await.map_err(|err| {
|
||||
warn!("Failed to list staff audit logs: {:?}", err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let maybe_cursor = records.last().map(|last| {
|
||||
server_state.opaque_cursors
|
||||
.encode_last_id_cursor(CURSOR_NAME, last.id)
|
||||
}).transpose().map_err(|err| {
|
||||
warn!("Failed to encode cursor: {:?}", err);
|
||||
AdvancedCommonWebError::server_error_with_message("Failed to encode cursor")
|
||||
})?;
|
||||
|
||||
let audit_logs = records.into_iter().map(|r| {
|
||||
StaffAuditLogResponse {
|
||||
token: r.token,
|
||||
audit_action: r.audit_action,
|
||||
maybe_entity_type: r.maybe_entity_type,
|
||||
maybe_entity_token: r.maybe_entity_token,
|
||||
maybe_target_username: r.maybe_target_username,
|
||||
maybe_target_display_name: r.maybe_target_display_name,
|
||||
staff_user_token: r.staff_user_token,
|
||||
staff_username: r.staff_username,
|
||||
staff_display_name: r.staff_display_name,
|
||||
staff_ip_address: r.staff_ip_address,
|
||||
created_at: r.created_at,
|
||||
}
|
||||
}).collect();
|
||||
|
||||
Ok(Json(ListStaffAuditLogsSuccessResponse {
|
||||
success: true,
|
||||
audit_logs,
|
||||
maybe_cursor,
|
||||
}))
|
||||
}
|
||||
-142
@@ -1,142 +0,0 @@
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use log::warn;
|
||||
|
||||
use mysql_queries::queries::users::user::update::set_user_ban_status::{set_user_ban_status, SetUserBanStatsArgs};
|
||||
use mysql_queries::queries::users::user_profiles::get_user_profile_by_username::get_user_profile_by_username;
|
||||
|
||||
use crate::http_server::web_utils::response_success_helpers::simple_json_success;
|
||||
use crate::http_server::web_utils::serialize_as_json_error::serialize_as_json_error;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct BanUserRequest {
|
||||
pub username: String,
|
||||
pub mod_notes: String,
|
||||
pub is_banned: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct BanUserSuccessResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct BanUserErrorResponse {
|
||||
pub success: bool,
|
||||
pub error_type: BanUserErrorType,
|
||||
pub error_message: String,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize)]
|
||||
pub enum BanUserErrorType {
|
||||
ServerError,
|
||||
Unauthorized,
|
||||
UserNotFound,
|
||||
}
|
||||
|
||||
impl BanUserErrorResponse {
|
||||
fn unauthorized() -> Self {
|
||||
Self {
|
||||
success: false,
|
||||
error_type: BanUserErrorType::Unauthorized,
|
||||
error_message: "unauthorized".to_string()
|
||||
}
|
||||
}
|
||||
fn server_error() -> Self {
|
||||
Self {
|
||||
success: false,
|
||||
error_type: BanUserErrorType::ServerError,
|
||||
error_message: "server error".to_string()
|
||||
}
|
||||
}
|
||||
fn not_found() -> Self {
|
||||
Self {
|
||||
success: false,
|
||||
error_type: BanUserErrorType::UserNotFound,
|
||||
error_message: "user not found".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponseError for BanUserErrorResponse {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match self.error_type {
|
||||
BanUserErrorType::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
BanUserErrorType::Unauthorized => StatusCode::UNAUTHORIZED,
|
||||
BanUserErrorType::UserNotFound => StatusCode::NOT_FOUND,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
serialize_as_json_error(self)
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using DeriveMore since Clion doesn't understand it.
|
||||
impl fmt::Display for BanUserErrorResponse {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self.error_type)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn ban_user_handler(
|
||||
http_request: HttpRequest,
|
||||
request: web::Json<BanUserRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<HttpResponse, BanUserErrorResponse> {
|
||||
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
BanUserErrorResponse::server_error()
|
||||
})?;
|
||||
|
||||
let user_session = match maybe_user_session {
|
||||
Some(session) => session,
|
||||
None => {
|
||||
warn!("not logged in");
|
||||
return Err(BanUserErrorResponse::unauthorized());
|
||||
}
|
||||
};
|
||||
|
||||
if !user_session.can_ban_users {
|
||||
warn!("user is not allowed to add bans: {:?}", user_session.user_token);
|
||||
return Err(BanUserErrorResponse::unauthorized());
|
||||
}
|
||||
|
||||
let username_lower = request.username.to_lowercase();
|
||||
|
||||
let user_lookup_result = get_user_profile_by_username(
|
||||
&username_lower, &server_state.mysql_pool).await;
|
||||
|
||||
let user_profile = match user_lookup_result {
|
||||
Ok(Some(result)) => result,
|
||||
Ok(None) => return Err(BanUserErrorResponse::not_found()),
|
||||
Err(err) => {
|
||||
warn!("lookup error: {:?}", err);
|
||||
return Err(BanUserErrorResponse::server_error());
|
||||
}
|
||||
};
|
||||
|
||||
set_user_ban_status(SetUserBanStatsArgs {
|
||||
subject_user_token: &user_profile.user_token,
|
||||
is_banned: request.is_banned,
|
||||
mod_user_token: &user_session.user_token,
|
||||
maybe_mod_comments: Some(&request.mod_notes),
|
||||
mysql_pool: &server_state.mysql_pool,
|
||||
}).await
|
||||
.map_err(|err| {
|
||||
warn!("Add IP ban DB error: {:?}", err);
|
||||
BanUserErrorResponse::server_error()
|
||||
})?;
|
||||
|
||||
Ok(simple_json_success())
|
||||
}
|
||||
+1
-1
@@ -1 +1 @@
|
||||
pub mod ban_user;
|
||||
pub mod moderation_ban_user_handler;
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::Json;
|
||||
use actix_web::{web, HttpRequest};
|
||||
use log::{info, warn};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use enums::by_table::staff_audit_logs::staff_audit_action::StaffAuditAction;
|
||||
use enums::by_table::staff_audit_logs::staff_audit_entity_type::StaffAuditEntityType;
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use mysql_queries::queries::staff_audit_logs::insert_staff_audit_log::{
|
||||
insert_staff_audit_log, InsertStaffAuditLogArgs,
|
||||
};
|
||||
use mysql_queries::queries::users::user::update::set_user_ban_status::{
|
||||
set_user_ban_status, SetUserBanStatusArgs,
|
||||
};
|
||||
use mysql_queries::queries::users::user_profiles::get_user_profile_by_username::get_user_profile_by_username;
|
||||
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_moderator::{
|
||||
require_moderator, UseDatabase,
|
||||
};
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
// --- Request ---
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct ModerationBanUserRequest {
|
||||
pub username: String,
|
||||
pub mod_notes: String,
|
||||
pub is_banned: bool,
|
||||
}
|
||||
|
||||
// --- Response ---
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ModerationBanUserSuccessResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
// --- Handler ---
|
||||
|
||||
/// Ban or unban a user. Moderators only.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
tag = "Moderation",
|
||||
path = "/moderation/user_bans/manage_ban",
|
||||
request_body = ModerationBanUserRequest,
|
||||
responses(
|
||||
(status = 200, description = "User ban status updated", body = ModerationBanUserSuccessResponse),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "User not found"),
|
||||
(status = 500, description = "Server error"),
|
||||
),
|
||||
)]
|
||||
pub async fn moderation_ban_user_handler(
|
||||
http_request: HttpRequest,
|
||||
request: Json<ModerationBanUserRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<ModerationBanUserSuccessResponse>, AdvancedCommonWebError> {
|
||||
|
||||
// 1. Require moderator with ban permissions.
|
||||
let user_session = require_moderator(
|
||||
&http_request,
|
||||
&server_state,
|
||||
UseDatabase::GrabNewConnection,
|
||||
).await.map_err(|err| {
|
||||
warn!("Moderator check failed: {:?}", err);
|
||||
AdvancedCommonWebError::NotAuthorized
|
||||
})?;
|
||||
|
||||
if !user_session.can_ban_users {
|
||||
warn!("User {} is not allowed to ban users", user_session.user_token.as_str());
|
||||
return Err(AdvancedCommonWebError::NotAuthorized);
|
||||
}
|
||||
|
||||
// 2. Look up the target user by username.
|
||||
let username_lower = request.username.to_lowercase();
|
||||
|
||||
let user_profile = get_user_profile_by_username(
|
||||
&username_lower,
|
||||
&server_state.mysql_pool,
|
||||
).await.map_err(|err| {
|
||||
warn!("User lookup error: {:?}", err);
|
||||
AdvancedCommonWebError::from_anyhow_error(err)
|
||||
})?.ok_or_else(|| {
|
||||
warn!("User not found for ban: {}", username_lower);
|
||||
AdvancedCommonWebError::NotFound
|
||||
})?;
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
let audit_action = if request.is_banned {
|
||||
StaffAuditAction::BanUser
|
||||
} else {
|
||||
StaffAuditAction::UnbanUser
|
||||
};
|
||||
|
||||
info!(
|
||||
"Moderator {} {} user {} ({})",
|
||||
user_session.user_token.as_str(),
|
||||
audit_action.to_str(),
|
||||
user_profile.user_token.as_str(),
|
||||
username_lower,
|
||||
);
|
||||
|
||||
// 3. Begin transaction: ban + audit log.
|
||||
let mut transaction = server_state.mysql_pool.begin().await
|
||||
.map_err(|err| {
|
||||
warn!("Failed to begin transaction: {:?}", err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
// 4. Set ban status.
|
||||
set_user_ban_status(SetUserBanStatusArgs {
|
||||
subject_user_token: &user_profile.user_token,
|
||||
is_banned: request.is_banned,
|
||||
mod_user_token: &user_session.user_token,
|
||||
maybe_mod_comments: Some(&request.mod_notes),
|
||||
mysql_executor: &mut *transaction,
|
||||
phantom: PhantomData,
|
||||
}).await.map_err(|err| {
|
||||
warn!("Failed to set user ban status: {:?}", err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
// 5. Insert staff audit log.
|
||||
let _audit_token = insert_staff_audit_log(InsertStaffAuditLogArgs {
|
||||
audit_action,
|
||||
maybe_entity_type: Some(StaffAuditEntityType::User),
|
||||
maybe_entity_token: Some(user_profile.user_token.as_str()),
|
||||
staff_user_token: &user_session.user_token,
|
||||
actor_ip_address: &ip_address,
|
||||
mysql_executor: &mut *transaction,
|
||||
phantom: PhantomData,
|
||||
}).await.map_err(|err| {
|
||||
warn!("Failed to insert staff audit log: {:?}", err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
// 6. Commit transaction.
|
||||
transaction.commit().await.map_err(|err| {
|
||||
warn!("Failed to commit transaction: {:?}", err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
info!(
|
||||
"User {} ban status set to {} by moderator {}",
|
||||
user_profile.user_token.as_str(),
|
||||
request.is_banned,
|
||||
user_session.user_token.as_str(),
|
||||
);
|
||||
|
||||
Ok(Json(ModerationBanUserSuccessResponse {
|
||||
success: true,
|
||||
}))
|
||||
}
|
||||
+2
@@ -1 +1,3 @@
|
||||
pub mod moderator_list_user_session_impersonation_requests_for_user_handler;
|
||||
pub mod moderator_list_user_session_impersonation_requests_handler;
|
||||
pub mod moderator_user_session_impersonation_request_handler;
|
||||
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::{Json, Path, Query};
|
||||
use actix_web::{web, HttpRequest};
|
||||
use chrono::{DateTime, Utc};
|
||||
use log::warn;
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
use mysql_queries::queries::user_impersonation_requests::list_user_impersonation_requests_for_user::{
|
||||
list_user_impersonation_requests_for_user, ListUserImpersonationRequestsArgs,
|
||||
};
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_moderator::{
|
||||
require_moderator, UseDatabase,
|
||||
};
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
const CURSOR_NAME: &str = "moduserimp";
|
||||
const DEFAULT_LIMIT: u32 = 50;
|
||||
const MAX_LIMIT: u32 = 1000;
|
||||
|
||||
// --- Request ---
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct ListImpersonationRequestsPathInfo {
|
||||
user_token: UserToken,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, ToSchema, IntoParams)]
|
||||
pub struct ListImpersonationRequestsQueryParams {
|
||||
pub cursor: Option<String>,
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
// --- Response ---
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ListUserImpersonationRequestsSuccessResponse {
|
||||
pub success: bool,
|
||||
pub impersonation_requests: Vec<UserImpersonationRequestResponse>,
|
||||
pub maybe_cursor: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct UserImpersonationRequestResponse {
|
||||
pub impersonator_user_token: UserToken,
|
||||
pub impersonator_username: String,
|
||||
pub impersonator_display_name: String,
|
||||
pub impersonated_user_token: UserToken,
|
||||
pub impersonated_username: String,
|
||||
pub impersonated_display_name: String,
|
||||
pub is_redeemed: bool,
|
||||
pub is_expired: bool,
|
||||
pub expires_at: DateTime<Utc>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
// --- Handler ---
|
||||
|
||||
/// List impersonation requests for a given user. Moderators only.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
tag = "Moderation",
|
||||
path = "/v1/moderation/user_sessions/impersonation_requests/user/{user_token}",
|
||||
params(
|
||||
ListImpersonationRequestsQueryParams,
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Success", body = ListUserImpersonationRequestsSuccessResponse),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 500, description = "Server error"),
|
||||
),
|
||||
)]
|
||||
pub async fn moderator_list_user_session_impersonation_requests_for_user_handler(
|
||||
http_request: HttpRequest,
|
||||
path: Path<ListImpersonationRequestsPathInfo>,
|
||||
query: Query<ListImpersonationRequestsQueryParams>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<ListUserImpersonationRequestsSuccessResponse>, AdvancedCommonWebError> {
|
||||
|
||||
let _user_session = require_moderator(
|
||||
&http_request,
|
||||
&server_state,
|
||||
UseDatabase::GrabNewConnection,
|
||||
).await.map_err(|err| {
|
||||
warn!("Moderator check failed: {:?}", err);
|
||||
AdvancedCommonWebError::NotAuthorized
|
||||
})?;
|
||||
|
||||
let limit = query.limit
|
||||
.unwrap_or(DEFAULT_LIMIT)
|
||||
.min(MAX_LIMIT);
|
||||
|
||||
let maybe_cursor_id = match &query.cursor {
|
||||
None => None,
|
||||
Some(cursor_str) => {
|
||||
let decoded = server_state.opaque_cursors
|
||||
.decode_cursor_expecting_name(CURSOR_NAME, cursor_str)
|
||||
.map_err(|err| {
|
||||
warn!("Failed to decode cursor: {:?}", err);
|
||||
AdvancedCommonWebError::BadInputWithSimpleMessage(
|
||||
"Invalid cursor".to_string())
|
||||
})?;
|
||||
decoded.last_id
|
||||
}
|
||||
};
|
||||
|
||||
let records = list_user_impersonation_requests_for_user(
|
||||
ListUserImpersonationRequestsArgs {
|
||||
user_token: path.user_token.as_str(),
|
||||
maybe_cursor_id,
|
||||
limit,
|
||||
mysql_pool: &server_state.mysql_pool,
|
||||
},
|
||||
).await.map_err(|err| {
|
||||
warn!("Failed to list impersonation requests: {:?}", err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let maybe_cursor = records.last().map(|last| {
|
||||
server_state.opaque_cursors
|
||||
.encode_last_id_cursor(CURSOR_NAME, last.id)
|
||||
}).transpose().map_err(|err| {
|
||||
warn!("Failed to encode cursor: {:?}", err);
|
||||
AdvancedCommonWebError::server_error_with_message("Failed to encode cursor")
|
||||
})?;
|
||||
|
||||
let impersonation_requests = records.into_iter().map(|r| {
|
||||
UserImpersonationRequestResponse {
|
||||
impersonator_user_token: r.impersonator_user_token,
|
||||
impersonator_username: r.impersonator_username,
|
||||
impersonator_display_name: r.impersonator_display_name,
|
||||
impersonated_user_token: r.impersonated_user_token,
|
||||
impersonated_username: r.impersonated_username,
|
||||
impersonated_display_name: r.impersonated_display_name,
|
||||
is_redeemed: r.is_redeemed,
|
||||
is_expired: r.is_expired,
|
||||
expires_at: r.expires_at,
|
||||
created_at: r.created_at,
|
||||
updated_at: r.updated_at,
|
||||
}
|
||||
}).collect();
|
||||
|
||||
Ok(Json(ListUserImpersonationRequestsSuccessResponse {
|
||||
success: true,
|
||||
impersonation_requests,
|
||||
maybe_cursor,
|
||||
}))
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::{Json, Query};
|
||||
use actix_web::{web, HttpRequest};
|
||||
use chrono::{DateTime, Utc};
|
||||
use log::warn;
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
use mysql_queries::queries::user_impersonation_requests::list_user_impersonation_requests::{
|
||||
list_user_impersonation_requests, ListUserImpersonationRequestsArgs,
|
||||
};
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_moderator::{
|
||||
require_moderator, UseDatabase,
|
||||
};
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
const CURSOR_NAME: &str = "modallimp";
|
||||
const DEFAULT_LIMIT: u32 = 50;
|
||||
const MAX_LIMIT: u32 = 1000;
|
||||
|
||||
// --- Request ---
|
||||
|
||||
#[derive(Deserialize, ToSchema, IntoParams)]
|
||||
pub struct ListAllImpersonationRequestsQueryParams {
|
||||
pub cursor: Option<String>,
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
// --- Response ---
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ListAllImpersonationRequestsSuccessResponse {
|
||||
pub success: bool,
|
||||
pub impersonation_requests: Vec<AllImpersonationRequestResponse>,
|
||||
pub maybe_cursor: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct AllImpersonationRequestResponse {
|
||||
pub impersonator_user_token: UserToken,
|
||||
pub impersonator_username: String,
|
||||
pub impersonator_display_name: String,
|
||||
pub impersonated_user_token: UserToken,
|
||||
pub impersonated_username: String,
|
||||
pub impersonated_display_name: String,
|
||||
pub is_redeemed: bool,
|
||||
pub is_expired: bool,
|
||||
pub expires_at: DateTime<Utc>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
// --- Handler ---
|
||||
|
||||
/// List all impersonation requests (not scoped to a user). Moderators only.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
tag = "Moderation",
|
||||
path = "/v1/moderation/user_sessions/impersonation_requests/list",
|
||||
params(
|
||||
ListAllImpersonationRequestsQueryParams,
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Success", body = ListAllImpersonationRequestsSuccessResponse),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 500, description = "Server error"),
|
||||
),
|
||||
)]
|
||||
pub async fn moderator_list_user_session_impersonation_requests_handler(
|
||||
http_request: HttpRequest,
|
||||
query: Query<ListAllImpersonationRequestsQueryParams>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<ListAllImpersonationRequestsSuccessResponse>, AdvancedCommonWebError> {
|
||||
|
||||
let _user_session = require_moderator(
|
||||
&http_request,
|
||||
&server_state,
|
||||
UseDatabase::GrabNewConnection,
|
||||
).await.map_err(|err| {
|
||||
warn!("Moderator check failed: {:?}", err);
|
||||
AdvancedCommonWebError::NotAuthorized
|
||||
})?;
|
||||
|
||||
let limit = query.limit
|
||||
.unwrap_or(DEFAULT_LIMIT)
|
||||
.min(MAX_LIMIT);
|
||||
|
||||
let maybe_cursor_id = match &query.cursor {
|
||||
None => None,
|
||||
Some(cursor_str) => {
|
||||
let decoded = server_state.opaque_cursors
|
||||
.decode_cursor_expecting_name(CURSOR_NAME, cursor_str)
|
||||
.map_err(|err| {
|
||||
warn!("Failed to decode cursor: {:?}", err);
|
||||
AdvancedCommonWebError::BadInputWithSimpleMessage(
|
||||
"Invalid cursor".to_string())
|
||||
})?;
|
||||
decoded.last_id
|
||||
}
|
||||
};
|
||||
|
||||
let records = list_user_impersonation_requests(
|
||||
ListUserImpersonationRequestsArgs {
|
||||
maybe_cursor_id,
|
||||
limit,
|
||||
mysql_pool: &server_state.mysql_pool,
|
||||
},
|
||||
).await.map_err(|err| {
|
||||
warn!("Failed to list impersonation requests: {:?}", err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let maybe_cursor = records.last().map(|last| {
|
||||
server_state.opaque_cursors
|
||||
.encode_last_id_cursor(CURSOR_NAME, last.id)
|
||||
}).transpose().map_err(|err| {
|
||||
warn!("Failed to encode cursor: {:?}", err);
|
||||
AdvancedCommonWebError::server_error_with_message("Failed to encode cursor")
|
||||
})?;
|
||||
|
||||
let impersonation_requests = records.into_iter().map(|r| {
|
||||
AllImpersonationRequestResponse {
|
||||
impersonator_user_token: r.impersonator_user_token,
|
||||
impersonator_username: r.impersonator_username,
|
||||
impersonator_display_name: r.impersonator_display_name,
|
||||
impersonated_user_token: r.impersonated_user_token,
|
||||
impersonated_username: r.impersonated_username,
|
||||
impersonated_display_name: r.impersonated_display_name,
|
||||
is_redeemed: r.is_redeemed,
|
||||
is_expired: r.is_expired,
|
||||
expires_at: r.expires_at,
|
||||
created_at: r.created_at,
|
||||
updated_at: r.updated_at,
|
||||
}
|
||||
}).collect();
|
||||
|
||||
Ok(Json(ListAllImpersonationRequestsSuccessResponse {
|
||||
success: true,
|
||||
impersonation_requests,
|
||||
maybe_cursor,
|
||||
}))
|
||||
}
|
||||
+41
-8
@@ -1,19 +1,26 @@
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::{Json, Path};
|
||||
use actix_web::{web, HttpRequest};
|
||||
use log::warn;
|
||||
use log::{info, warn};
|
||||
|
||||
use artcraft_api_defs::moderation::wallets::moderator_add_banked_balance_to_wallet::{
|
||||
ModeratorAddBankedBalanceToWalletPathInfo,
|
||||
ModeratorAddBankedBalanceToWalletRequest,
|
||||
ModeratorAddBankedBalanceToWalletResponse,
|
||||
};
|
||||
use enums::by_table::staff_audit_logs::staff_audit_action::StaffAuditAction;
|
||||
use enums::by_table::staff_audit_logs::staff_audit_entity_type::StaffAuditEntityType;
|
||||
use enums::by_table::wallet_ledger_entries::wallet_ledger_entry_type::WalletLedgerEntryType;
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use mysql_queries::queries::staff_audit_logs::insert_staff_audit_log::{
|
||||
insert_staff_audit_log, InsertStaffAuditLogArgs,
|
||||
};
|
||||
use mysql_queries::queries::wallets::add_durable_banked_balance_to_wallet::add_durable_banked_balance_to_wallet;
|
||||
|
||||
use tokens::tokens::wallets::WalletToken;
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, UseDatabase};
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
@@ -37,17 +44,29 @@ pub async fn moderator_add_banked_balance_to_wallet_handler(
|
||||
path: Path<ModeratorAddBankedBalanceToWalletPathInfo>,
|
||||
request: Json<ModeratorAddBankedBalanceToWalletRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<ModeratorAddBankedBalanceToWalletResponse>, CommonWebError> {
|
||||
) -> Result<Json<ModeratorAddBankedBalanceToWalletResponse>, AdvancedCommonWebError> {
|
||||
|
||||
let _user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
|
||||
let user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
|
||||
.await
|
||||
.map_err(|_| CommonWebError::NotAuthorized)?;
|
||||
.map_err(|err| {
|
||||
warn!("Moderator check failed: {:?}", err);
|
||||
AdvancedCommonWebError::NotAuthorized
|
||||
})?;
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
info!(
|
||||
"Moderator {} adding {} banked credits to wallet {}",
|
||||
user_session.user_token.as_str(),
|
||||
request.credits,
|
||||
path.wallet_token.as_str(),
|
||||
);
|
||||
|
||||
let mut transaction = server_state.mysql_pool.begin()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("moderator_add_banked_balance_to_wallet transaction begin error: {:?}", err);
|
||||
CommonWebError::ServerError
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let _result = add_durable_banked_balance_to_wallet(
|
||||
@@ -60,14 +79,28 @@ pub async fn moderator_add_banked_balance_to_wallet_handler(
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("moderator_add_banked_balance_to_wallet error: {:?}", err);
|
||||
CommonWebError::ServerError
|
||||
AdvancedCommonWebError::from_anyhow_error(err)
|
||||
})?;
|
||||
|
||||
// Insert staff audit log.
|
||||
let _audit_token = insert_staff_audit_log(InsertStaffAuditLogArgs {
|
||||
audit_action: StaffAuditAction::AddWalletBankedBalance,
|
||||
maybe_entity_type: Some(StaffAuditEntityType::Wallet),
|
||||
maybe_entity_token: Some(path.wallet_token.as_str()),
|
||||
staff_user_token: &user_session.user_token,
|
||||
actor_ip_address: &ip_address,
|
||||
mysql_executor: &mut *transaction,
|
||||
phantom: PhantomData,
|
||||
}).await.map_err(|err| {
|
||||
warn!("Failed to insert staff audit log: {:?}", err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
transaction.commit()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("moderator_add_banked_balance_to_wallet commit error: {:?}", err);
|
||||
CommonWebError::ServerError
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
Ok(Json(ModeratorAddBankedBalanceToWalletResponse {
|
||||
|
||||
+2
@@ -1 +1,3 @@
|
||||
mod test_seedance_1p5_pro;
|
||||
mod test_veo_2;
|
||||
mod test_veo_3;
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
//! Tests for omni-gen video distillation against the Seedance 1.5 Pro model.
|
||||
//! Tests for omni-gen video against the Seedance 1.5 Pro model.
|
||||
//!
|
||||
//! Seedance 1.5 Pro supports both text-to-video and image-to-video (via
|
||||
//! start_frame / end_frame). Pricing is based on resolution × duration ×
|
||||
|
||||
+442
@@ -0,0 +1,442 @@
|
||||
//! Tests for omni-gen video against the Veo 2 model.
|
||||
//!
|
||||
//! Veo 2 supports both text-to-video and image-to-video (via start_frame).
|
||||
//!
|
||||
//! Pricing: $2.50 for the first 5 seconds, +$0.50 per additional second.
|
||||
//! 5s → 250¢, 6s → 300¢, 7s → 350¢, 8s → 400¢
|
||||
//!
|
||||
//! Supported aspect ratios: Auto, WideSixteenNine, TallNineSixteen.
|
||||
//! Unsupported ratios fall back to Auto under PayMoreUpgrade.
|
||||
//!
|
||||
//! Supported durations: 5–8 seconds. Over-max clamps to 8s (PayMoreUpgrade).
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use url::Url;
|
||||
|
||||
use crate::http_server::endpoints::omni_gen::generate::video::distill_video_request::{
|
||||
distill_video_request, DistilledVideoRequest,
|
||||
};
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
fn make_request(
|
||||
prompt: Option<&str>,
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
duration_seconds: Option<u16>,
|
||||
start_frame_token: Option<MediaFileToken>,
|
||||
) -> OmniGenVideoCostAndGenerateRequest {
|
||||
OmniGenVideoCostAndGenerateRequest {
|
||||
idempotency_token: Some("11111111-1111-1111-1111-111111111111".to_string()),
|
||||
model: Some(CommonVideoModel::Veo2),
|
||||
prompt: prompt.map(|s| s.to_string()),
|
||||
negative_prompt: None,
|
||||
start_frame_image_media_token: start_frame_token,
|
||||
end_frame_image_media_token: None,
|
||||
reference_image_media_tokens: None,
|
||||
reference_video_media_tokens: None,
|
||||
reference_audio_media_tokens: None,
|
||||
reference_character_tokens: None,
|
||||
resolution: None,
|
||||
aspect_ratio,
|
||||
quality: None,
|
||||
duration_seconds,
|
||||
video_batch_count: None,
|
||||
generate_audio: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn fake_token(name: &str) -> (MediaFileToken, HashMap<MediaFileToken, Url>) {
|
||||
let token = MediaFileToken::new_from_str(name);
|
||||
let url = Url::parse(&format!("https://fake.example.com/{}.png", name)).unwrap();
|
||||
let mut map = HashMap::new();
|
||||
map.insert(token.clone(), url);
|
||||
(token, map)
|
||||
}
|
||||
|
||||
fn distill_text(request: &OmniGenVideoCostAndGenerateRequest) -> DistilledVideoRequest {
|
||||
let empty: HashMap<MediaFileToken, Url> = HashMap::new();
|
||||
distill_video_request(request, Some(&empty))
|
||||
.expect("distill_video_request should succeed for Veo 2 (text)")
|
||||
}
|
||||
|
||||
fn distill_image(
|
||||
request: &OmniGenVideoCostAndGenerateRequest,
|
||||
hydration: &HashMap<MediaFileToken, Url>,
|
||||
) -> DistilledVideoRequest {
|
||||
distill_video_request(request, Some(hydration))
|
||||
.expect("distill_video_request should succeed for Veo 2 (image)")
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// COST
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
mod cost {
|
||||
use super::*;
|
||||
|
||||
mod text {
|
||||
use super::*;
|
||||
|
||||
fn cost(duration: Option<u16>) -> u64 {
|
||||
let request = make_request(Some("p"), None, duration, None);
|
||||
distill_text(&request).cost.cost_in_usd_cents.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_duration_costs_250() { assert_eq!(cost(None), 250); }
|
||||
|
||||
#[test]
|
||||
fn five_seconds_costs_250() { assert_eq!(cost(Some(5)), 250); }
|
||||
|
||||
#[test]
|
||||
fn six_seconds_costs_300() { assert_eq!(cost(Some(6)), 300); }
|
||||
|
||||
#[test]
|
||||
fn seven_seconds_costs_350() { assert_eq!(cost(Some(7)), 350); }
|
||||
|
||||
#[test]
|
||||
fn eight_seconds_costs_400() { assert_eq!(cost(Some(8)), 400); }
|
||||
|
||||
#[test]
|
||||
fn duration_above_8_clamps_to_400() { assert_eq!(cost(Some(15)), 400); }
|
||||
|
||||
#[test]
|
||||
fn cost_independent_of_aspect_ratio() {
|
||||
let ars = [
|
||||
None,
|
||||
Some(CommonAspectRatio::Auto),
|
||||
Some(CommonAspectRatio::WideSixteenByNine),
|
||||
Some(CommonAspectRatio::TallNineBySixteen),
|
||||
Some(CommonAspectRatio::Square),
|
||||
];
|
||||
for ar in ars {
|
||||
let request = make_request(Some("p"), ar, None, None);
|
||||
assert_eq!(distill_text(&request).cost.cost_in_usd_cents.unwrap(), 250);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_metadata_flags_are_default() {
|
||||
let d = distill_text(&make_request(Some("p"), None, None, None));
|
||||
assert!(!d.cost.is_free);
|
||||
assert!(!d.cost.is_unlimited);
|
||||
assert!(!d.cost.is_rate_limited);
|
||||
assert!(!d.cost.has_watermark);
|
||||
}
|
||||
}
|
||||
|
||||
mod image {
|
||||
use super::*;
|
||||
|
||||
fn cost(duration: Option<u16>) -> u64 {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), None, duration, Some(start));
|
||||
distill_image(&request, &map).cost.cost_in_usd_cents.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_duration_costs_250() { assert_eq!(cost(None), 250); }
|
||||
|
||||
#[test]
|
||||
fn five_seconds_costs_250() { assert_eq!(cost(Some(5)), 250); }
|
||||
|
||||
#[test]
|
||||
fn six_seconds_costs_300() { assert_eq!(cost(Some(6)), 300); }
|
||||
|
||||
#[test]
|
||||
fn seven_seconds_costs_350() { assert_eq!(cost(Some(7)), 350); }
|
||||
|
||||
#[test]
|
||||
fn eight_seconds_costs_400() { assert_eq!(cost(Some(8)), 400); }
|
||||
|
||||
#[test]
|
||||
fn duration_above_8_clamps_to_400() { assert_eq!(cost(Some(15)), 400); }
|
||||
|
||||
#[test]
|
||||
fn image_and_text_cost_match() {
|
||||
for dur in [5u16, 6, 7, 8] {
|
||||
let text = distill_text(&make_request(Some("p"), None, Some(dur), None))
|
||||
.cost.cost_in_usd_cents.unwrap();
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let img = distill_image(
|
||||
&make_request(Some("p"), None, Some(dur), Some(start)),
|
||||
&map,
|
||||
).cost.cost_in_usd_cents.unwrap();
|
||||
assert_eq!(text, img, "text/image cost diverged at dur={}", dur);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// PLAN
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
mod plan {
|
||||
use super::*;
|
||||
|
||||
use artcraft_router::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use artcraft_router::generate::generate_video::plan::fal::plan_generate_video_fal_veo_2::{
|
||||
FalVeo2Mode, PlanFalVeo2,
|
||||
};
|
||||
use fal_client::requests::webhook::video::image::enqueue_veo_2_image_to_video_webhook::{
|
||||
Veo2AspectRatio, Veo2Duration,
|
||||
};
|
||||
|
||||
fn with_text_plan<F: FnOnce(&PlanFalVeo2)>(
|
||||
request: &OmniGenVideoCostAndGenerateRequest,
|
||||
assertion: F,
|
||||
) {
|
||||
let distilled = distill_text(request);
|
||||
match distilled.plan() {
|
||||
VideoGenerationPlan::FalVeo2(plan) => assertion(plan),
|
||||
other => panic!("expected FalVeo2, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
fn with_image_plan<F: FnOnce(&PlanFalVeo2)>(
|
||||
request: &OmniGenVideoCostAndGenerateRequest,
|
||||
hydration: &HashMap<MediaFileToken, Url>,
|
||||
assertion: F,
|
||||
) {
|
||||
let distilled = distill_image(request, hydration);
|
||||
match distilled.plan() {
|
||||
VideoGenerationPlan::FalVeo2(plan) => assertion(plan),
|
||||
other => panic!("expected FalVeo2, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
mod text {
|
||||
use super::*;
|
||||
|
||||
// ── Mode detection ────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn text_mode_is_text_to_video() {
|
||||
with_text_plan(&make_request(Some("p"), None, None, None), |plan| {
|
||||
assert!(matches!(plan.mode, FalVeo2Mode::TextToVideo));
|
||||
});
|
||||
}
|
||||
|
||||
// ── Prompt passthrough ────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn prompt_is_passed_through() {
|
||||
with_text_plan(&make_request(Some("a horse on mars"), None, None, None), |plan| {
|
||||
assert_eq!(plan.prompt, "a horse on mars");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_prompt_is_empty_string() {
|
||||
with_text_plan(&make_request(None, None, None, None), |plan| {
|
||||
assert_eq!(plan.prompt, "");
|
||||
});
|
||||
}
|
||||
|
||||
// ── Aspect ratio mappings ─────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn default_aspect_ratio_is_auto() {
|
||||
with_text_plan(&make_request(Some("p"), None, None, None), |plan| {
|
||||
assert!(matches!(plan.aspect_ratio, Some(Veo2AspectRatio::Auto)));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_yields_auto() {
|
||||
for ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
|
||||
with_text_plan(&make_request(Some("p"), Some(ar), None, None), |plan| {
|
||||
assert!(matches!(plan.aspect_ratio, Some(Veo2AspectRatio::Auto)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wide_16x9_yields_wide() {
|
||||
for ar in [CommonAspectRatio::WideSixteenByNine, CommonAspectRatio::Wide] {
|
||||
with_text_plan(&make_request(Some("p"), Some(ar), None, None), |plan| {
|
||||
assert!(matches!(plan.aspect_ratio, Some(Veo2AspectRatio::WideSixteenNine)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tall_9x16_yields_tall() {
|
||||
for ar in [CommonAspectRatio::TallNineBySixteen, CommonAspectRatio::Tall] {
|
||||
with_text_plan(&make_request(Some("p"), Some(ar), None, None), |plan| {
|
||||
assert!(matches!(plan.aspect_ratio, Some(Veo2AspectRatio::TallNineSixteen)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_aspect_ratio_falls_back_to_auto() {
|
||||
for ar in [
|
||||
CommonAspectRatio::Square, CommonAspectRatio::SquareHd,
|
||||
CommonAspectRatio::WideFourByThree, CommonAspectRatio::TallThreeByFour,
|
||||
] {
|
||||
with_text_plan(&make_request(Some("p"), Some(ar), None, None), |plan| {
|
||||
assert!(matches!(plan.aspect_ratio, Some(Veo2AspectRatio::Auto)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── Duration mappings ─────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn default_duration_is_default() {
|
||||
with_text_plan(&make_request(Some("p"), None, None, None), |plan| {
|
||||
assert!(matches!(plan.duration, Veo2Duration::Default));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_5_yields_five() {
|
||||
with_text_plan(&make_request(Some("p"), None, Some(5), None), |plan| {
|
||||
assert!(matches!(plan.duration, Veo2Duration::FiveSeconds));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_6_yields_six() {
|
||||
with_text_plan(&make_request(Some("p"), None, Some(6), None), |plan| {
|
||||
assert!(matches!(plan.duration, Veo2Duration::SixSeconds));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_7_yields_seven() {
|
||||
with_text_plan(&make_request(Some("p"), None, Some(7), None), |plan| {
|
||||
assert!(matches!(plan.duration, Veo2Duration::SevenSeconds));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_8_yields_eight() {
|
||||
with_text_plan(&make_request(Some("p"), None, Some(8), None), |plan| {
|
||||
assert!(matches!(plan.duration, Veo2Duration::EightSeconds));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_above_8_clamps_to_eight() {
|
||||
with_text_plan(&make_request(Some("p"), None, Some(20), None), |plan| {
|
||||
assert!(matches!(plan.duration, Veo2Duration::EightSeconds));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
mod image {
|
||||
use super::*;
|
||||
|
||||
// ── Mode detection / start frame hydration ────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn start_frame_triggers_image_to_video() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), None, None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
match &plan.mode {
|
||||
FalVeo2Mode::ImageToVideo { image_url } => {
|
||||
assert!(image_url.starts_with("https://fake.example.com/"));
|
||||
}
|
||||
other => panic!("expected ImageToVideo, got {:?}", other),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_frame_url_matches_hydration_map_exactly() {
|
||||
let token = MediaFileToken::new_from_str("mf_myframe0000000000000000000000");
|
||||
let expected_url = "https://cdn.example.com/frames/my_frame.png";
|
||||
let mut map = HashMap::new();
|
||||
map.insert(token.clone(), Url::parse(expected_url).unwrap());
|
||||
|
||||
let request = make_request(Some("p"), None, None, Some(token));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
match &plan.mode {
|
||||
FalVeo2Mode::ImageToVideo { image_url } => {
|
||||
assert_eq!(image_url, expected_url);
|
||||
}
|
||||
other => panic!("expected ImageToVideo, got {:?}", other),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Prompt passthrough in image mode ──────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn prompt_is_passed_through_in_image_mode() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("a horse on mars"), None, None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert_eq!(plan.prompt, "a horse on mars");
|
||||
});
|
||||
}
|
||||
|
||||
// ── Aspect ratio is None in image mode (i2v inherits source AR) ────────
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_is_none_in_image_mode() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), Some(CommonAspectRatio::WideSixteenByNine), None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(plan.aspect_ratio.is_none(), "image-to-video should not have aspect_ratio");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_is_none_in_image_mode_even_with_tall() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), Some(CommonAspectRatio::TallNineBySixteen), None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(plan.aspect_ratio.is_none());
|
||||
});
|
||||
}
|
||||
|
||||
// ── Duration pass-throughs in image mode ──────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn duration_5_in_image_mode() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), None, Some(5), Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(matches!(plan.duration, Veo2Duration::FiveSeconds));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_8_in_image_mode() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), None, Some(8), Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(matches!(plan.duration, Veo2Duration::EightSeconds));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_above_8_clamps_in_image_mode() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), None, Some(20), Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(matches!(plan.duration, Veo2Duration::EightSeconds));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_duration_in_image_mode() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), None, None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(matches!(plan.duration, Veo2Duration::Default));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+589
@@ -0,0 +1,589 @@
|
||||
//! Tests for omni-gen video against the Veo 3 model.
|
||||
//!
|
||||
//! Veo 3 supports both text-to-video and image-to-video (via start_frame).
|
||||
//!
|
||||
//! Pricing: $0.20/sec (audio off) or $0.40/sec (audio on).
|
||||
//! Default: 8s, audio off (artcraft legacy) → 160¢.
|
||||
//! 4s no-audio → 80¢, 6s no-audio → 120¢, 8s no-audio → 160¢
|
||||
//! 4s audio → 160¢, 6s audio → 240¢, 8s audio → 320¢
|
||||
//!
|
||||
//! Supported aspect ratios (text mode only): Default, 16:9, 9:16, Square.
|
||||
//! Image-to-video inherits the source frame's aspect ratio.
|
||||
//!
|
||||
//! Supported resolutions: 720p, 1080p.
|
||||
//! Supported durations: 4s, 6s, 8s. Over-max clamps to 8s (PayMoreUpgrade).
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use url::Url;
|
||||
|
||||
use crate::http_server::endpoints::omni_gen::generate::video::distill_video_request::{
|
||||
distill_video_request, DistilledVideoRequest,
|
||||
};
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
fn make_request(
|
||||
prompt: Option<&str>,
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
resolution: Option<CommonResolution>,
|
||||
duration_seconds: Option<u16>,
|
||||
generate_audio: Option<bool>,
|
||||
start_frame_token: Option<MediaFileToken>,
|
||||
) -> OmniGenVideoCostAndGenerateRequest {
|
||||
OmniGenVideoCostAndGenerateRequest {
|
||||
idempotency_token: Some("11111111-1111-1111-1111-111111111111".to_string()),
|
||||
model: Some(CommonVideoModel::Veo3),
|
||||
prompt: prompt.map(|s| s.to_string()),
|
||||
negative_prompt: None,
|
||||
start_frame_image_media_token: start_frame_token,
|
||||
end_frame_image_media_token: None,
|
||||
reference_image_media_tokens: None,
|
||||
reference_video_media_tokens: None,
|
||||
reference_audio_media_tokens: None,
|
||||
reference_character_tokens: None,
|
||||
resolution,
|
||||
aspect_ratio,
|
||||
quality: None,
|
||||
duration_seconds,
|
||||
video_batch_count: None,
|
||||
generate_audio,
|
||||
}
|
||||
}
|
||||
|
||||
fn fake_token(name: &str) -> (MediaFileToken, HashMap<MediaFileToken, Url>) {
|
||||
let token = MediaFileToken::new_from_str(name);
|
||||
let url = Url::parse(&format!("https://fake.example.com/{}.png", name)).unwrap();
|
||||
let mut map = HashMap::new();
|
||||
map.insert(token.clone(), url);
|
||||
(token, map)
|
||||
}
|
||||
|
||||
fn distill_text(request: &OmniGenVideoCostAndGenerateRequest) -> DistilledVideoRequest {
|
||||
let empty: HashMap<MediaFileToken, Url> = HashMap::new();
|
||||
distill_video_request(request, Some(&empty))
|
||||
.expect("distill should succeed for Veo 3 (text)")
|
||||
}
|
||||
|
||||
fn distill_image(
|
||||
request: &OmniGenVideoCostAndGenerateRequest,
|
||||
hydration: &HashMap<MediaFileToken, Url>,
|
||||
) -> DistilledVideoRequest {
|
||||
distill_video_request(request, Some(hydration))
|
||||
.expect("distill should succeed for Veo 3 (image)")
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// COST
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Artcraft cost path: always 8s (legacy). $0.20/sec no-audio, $0.40/sec audio.
|
||||
// Default audio = false (artcraft legacy).
|
||||
mod cost {
|
||||
use super::*;
|
||||
|
||||
mod text {
|
||||
use super::*;
|
||||
|
||||
fn cost(duration: Option<u16>, audio: Option<bool>) -> u64 {
|
||||
let request = make_request(Some("p"), None, None, duration, audio, None);
|
||||
distill_text(&request).cost.cost_in_usd_cents.unwrap()
|
||||
}
|
||||
|
||||
// ── No audio: $0.20/sec ──────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn default_no_audio_costs_160() { assert_eq!(cost(None, None), 160); } // 8s × 20¢
|
||||
|
||||
#[test]
|
||||
fn four_seconds_no_audio_costs_80() { assert_eq!(cost(Some(4), Some(false)), 80); }
|
||||
|
||||
#[test]
|
||||
fn six_seconds_no_audio_costs_120() { assert_eq!(cost(Some(6), Some(false)), 120); }
|
||||
|
||||
#[test]
|
||||
fn eight_seconds_no_audio_costs_160() { assert_eq!(cost(Some(8), Some(false)), 160); }
|
||||
|
||||
// ── Audio on: $0.40/sec ───────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn four_seconds_audio_on_costs_160() { assert_eq!(cost(Some(4), Some(true)), 160); }
|
||||
|
||||
#[test]
|
||||
fn six_seconds_audio_on_costs_240() { assert_eq!(cost(Some(6), Some(true)), 240); }
|
||||
|
||||
#[test]
|
||||
fn eight_seconds_audio_on_costs_320() { assert_eq!(cost(Some(8), Some(true)), 320); }
|
||||
|
||||
#[test]
|
||||
fn default_duration_audio_on_costs_320() { assert_eq!(cost(None, Some(true)), 320); }
|
||||
|
||||
// ── Over-max clamps to 8s ─────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn duration_above_8_clamps_to_160_no_audio() { assert_eq!(cost(Some(20), Some(false)), 160); }
|
||||
|
||||
#[test]
|
||||
fn duration_above_8_clamps_to_320_audio() { assert_eq!(cost(Some(20), Some(true)), 320); }
|
||||
|
||||
// ── Cost is independent of aspect ratio ───────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn cost_independent_of_aspect_ratio() {
|
||||
for ar in [None, Some(CommonAspectRatio::Auto), Some(CommonAspectRatio::WideSixteenByNine), Some(CommonAspectRatio::TallNineBySixteen)] {
|
||||
let request = make_request(Some("p"), ar, None, None, None, None);
|
||||
assert_eq!(distill_text(&request).cost.cost_in_usd_cents.unwrap(), 160);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Cost is independent of resolution ─────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn cost_independent_of_resolution() {
|
||||
for res in [None, Some(CommonResolution::SevenTwentyP), Some(CommonResolution::TenEightyP)] {
|
||||
let request = make_request(Some("p"), None, res, None, None, None);
|
||||
assert_eq!(distill_text(&request).cost.cost_in_usd_cents.unwrap(), 160);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Metadata flags ────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn cost_metadata_flags_are_default() {
|
||||
let d = distill_text(&make_request(Some("p"), None, None, None, None, None));
|
||||
assert!(!d.cost.is_free);
|
||||
assert!(!d.cost.is_unlimited);
|
||||
assert!(!d.cost.is_rate_limited);
|
||||
assert!(!d.cost.has_watermark);
|
||||
}
|
||||
}
|
||||
|
||||
mod image {
|
||||
use super::*;
|
||||
|
||||
fn cost_img(duration: Option<u16>, audio: Option<bool>) -> u64 {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), None, None, duration, audio, Some(start));
|
||||
distill_image(&request, &map).cost.cost_in_usd_cents.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_costs_160() { assert_eq!(cost_img(None, None), 160); }
|
||||
|
||||
#[test]
|
||||
fn four_seconds_no_audio_costs_80() { assert_eq!(cost_img(Some(4), Some(false)), 80); }
|
||||
|
||||
#[test]
|
||||
fn six_seconds_audio_on_costs_240() { assert_eq!(cost_img(Some(6), Some(true)), 240); }
|
||||
|
||||
#[test]
|
||||
fn eight_seconds_audio_on_costs_320() { assert_eq!(cost_img(Some(8), Some(true)), 320); }
|
||||
|
||||
#[test]
|
||||
fn image_and_text_cost_match() {
|
||||
let text = distill_text(&make_request(Some("p"), None, None, None, None, None))
|
||||
.cost.cost_in_usd_cents.unwrap();
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let img = distill_image(
|
||||
&make_request(Some("p"), None, None, None, None, Some(start)),
|
||||
&map,
|
||||
).cost.cost_in_usd_cents.unwrap();
|
||||
assert_eq!(text, img);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// PLAN
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
mod plan {
|
||||
use super::*;
|
||||
|
||||
use artcraft_router::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use artcraft_router::generate::generate_video::plan::fal::plan_generate_video_fal_veo_3::{
|
||||
FalVeo3Duration, FalVeo3I2vAspectRatio, FalVeo3Mode, FalVeo3Resolution,
|
||||
FalVeo3T2vAspectRatio, PlanFalVeo3,
|
||||
};
|
||||
|
||||
fn with_text_plan<F: FnOnce(&PlanFalVeo3)>(
|
||||
request: &OmniGenVideoCostAndGenerateRequest,
|
||||
assertion: F,
|
||||
) {
|
||||
let distilled = distill_text(request);
|
||||
match distilled.plan() {
|
||||
VideoGenerationPlan::FalVeo3(plan) => assertion(plan),
|
||||
other => panic!("expected FalVeo3, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
fn with_image_plan<F: FnOnce(&PlanFalVeo3)>(
|
||||
request: &OmniGenVideoCostAndGenerateRequest,
|
||||
hydration: &HashMap<MediaFileToken, Url>,
|
||||
assertion: F,
|
||||
) {
|
||||
let distilled = distill_image(request, hydration);
|
||||
match distilled.plan() {
|
||||
VideoGenerationPlan::FalVeo3(plan) => assertion(plan),
|
||||
other => panic!("expected FalVeo3, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
mod text {
|
||||
use super::*;
|
||||
|
||||
// ── Mode detection ────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn text_mode_is_text_to_video() {
|
||||
with_text_plan(&make_request(Some("p"), None, None, None, None, None), |plan| {
|
||||
assert!(matches!(plan.mode, FalVeo3Mode::TextToVideo));
|
||||
});
|
||||
}
|
||||
|
||||
// ── Prompt passthrough ────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn prompt_is_passed_through() {
|
||||
with_text_plan(&make_request(Some("a horse on mars"), None, None, None, None, None), |plan| {
|
||||
assert_eq!(plan.prompt, "a horse on mars");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_prompt_is_empty_string() {
|
||||
with_text_plan(&make_request(None, None, None, None, None, None), |plan| {
|
||||
assert_eq!(plan.prompt, "");
|
||||
});
|
||||
}
|
||||
|
||||
// ── Aspect ratio mappings (text mode only) ────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn default_aspect_ratio_is_default() {
|
||||
with_text_plan(&make_request(Some("p"), None, None, None, None, None), |plan| {
|
||||
assert!(matches!(plan.t2v_aspect_ratio, Some(FalVeo3T2vAspectRatio::Default)));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wide_16x9_yields_wide() {
|
||||
for ar in [CommonAspectRatio::WideSixteenByNine, CommonAspectRatio::Wide] {
|
||||
with_text_plan(&make_request(Some("p"), Some(ar), None, None, None, None), |plan| {
|
||||
assert!(matches!(plan.t2v_aspect_ratio, Some(FalVeo3T2vAspectRatio::WideSixteenNine)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tall_9x16_yields_tall() {
|
||||
for ar in [CommonAspectRatio::TallNineBySixteen, CommonAspectRatio::Tall] {
|
||||
with_text_plan(&make_request(Some("p"), Some(ar), None, None, None, None), |plan| {
|
||||
assert!(matches!(plan.t2v_aspect_ratio, Some(FalVeo3T2vAspectRatio::TallNineSixteen)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn square_falls_back_to_default() {
|
||||
// Veo 3 t2v has no Square — falls back to Default (16:9).
|
||||
for ar in [CommonAspectRatio::Square, CommonAspectRatio::SquareHd] {
|
||||
with_text_plan(&make_request(Some("p"), Some(ar), None, None, None, None), |plan| {
|
||||
assert!(matches!(plan.t2v_aspect_ratio, Some(FalVeo3T2vAspectRatio::Default)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_falls_back_to_default() {
|
||||
// Veo 3 t2v has no Auto — falls back to Default (16:9).
|
||||
for ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
|
||||
with_text_plan(&make_request(Some("p"), Some(ar), None, None, None, None), |plan| {
|
||||
assert!(matches!(plan.t2v_aspect_ratio, Some(FalVeo3T2vAspectRatio::Default)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_aspect_ratio_falls_back_to_default() {
|
||||
for ar in [CommonAspectRatio::WideFourByThree, CommonAspectRatio::TallThreeByFour, CommonAspectRatio::WideFiveByFour] {
|
||||
with_text_plan(&make_request(Some("p"), Some(ar), None, None, None, None), |plan| {
|
||||
assert!(matches!(plan.t2v_aspect_ratio, Some(FalVeo3T2vAspectRatio::Default)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── Duration mappings ─────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn default_duration_is_default() {
|
||||
with_text_plan(&make_request(Some("p"), None, None, None, None, None), |plan| {
|
||||
assert!(matches!(plan.duration, FalVeo3Duration::Default));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_4_yields_four() {
|
||||
with_text_plan(&make_request(Some("p"), None, None, Some(4), None, None), |plan| {
|
||||
assert!(matches!(plan.duration, FalVeo3Duration::FourSeconds));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_6_yields_six() {
|
||||
with_text_plan(&make_request(Some("p"), None, None, Some(6), None, None), |plan| {
|
||||
assert!(matches!(plan.duration, FalVeo3Duration::SixSeconds));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_8_yields_eight() {
|
||||
with_text_plan(&make_request(Some("p"), None, None, Some(8), None, None), |plan| {
|
||||
assert!(matches!(plan.duration, FalVeo3Duration::EightSeconds));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_above_8_clamps_to_eight() {
|
||||
with_text_plan(&make_request(Some("p"), None, None, Some(20), None, None), |plan| {
|
||||
assert!(matches!(plan.duration, FalVeo3Duration::EightSeconds));
|
||||
});
|
||||
}
|
||||
|
||||
// ── Resolution mappings ───────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn default_resolution_is_default() {
|
||||
with_text_plan(&make_request(Some("p"), None, None, None, None, None), |plan| {
|
||||
assert!(matches!(plan.resolution, FalVeo3Resolution::Default));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seven_twenty_p_yields_seven_twenty_p() {
|
||||
with_text_plan(&make_request(Some("p"), None, Some(CommonResolution::SevenTwentyP), None, None, None), |plan| {
|
||||
assert!(matches!(plan.resolution, FalVeo3Resolution::SevenTwentyP));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ten_eighty_p_yields_ten_eighty_p() {
|
||||
with_text_plan(&make_request(Some("p"), None, Some(CommonResolution::TenEightyP), None, None, None), |plan| {
|
||||
assert!(matches!(plan.resolution, FalVeo3Resolution::TenEightyP));
|
||||
});
|
||||
}
|
||||
|
||||
// ── Generate audio passthrough ────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn default_audio_is_true() {
|
||||
// Fal plan defaults generate_audio to true.
|
||||
with_text_plan(&make_request(Some("p"), None, None, None, None, None), |plan| {
|
||||
assert!(plan.generate_audio);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_true_passes_through() {
|
||||
with_text_plan(&make_request(Some("p"), None, None, None, Some(true), None), |plan| {
|
||||
assert!(plan.generate_audio);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_false_passes_through() {
|
||||
with_text_plan(&make_request(Some("p"), None, None, None, Some(false), None), |plan| {
|
||||
assert!(!plan.generate_audio);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
mod image {
|
||||
use super::*;
|
||||
|
||||
// ── Mode detection / start frame hydration ────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn start_frame_triggers_image_to_video() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), None, None, None, None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
match &plan.mode {
|
||||
FalVeo3Mode::ImageToVideo { image_url } => {
|
||||
assert!(image_url.starts_with("https://fake.example.com/"));
|
||||
}
|
||||
other => panic!("expected ImageToVideo, got {:?}", other),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_frame_url_matches_hydration_map_exactly() {
|
||||
let token = MediaFileToken::new_from_str("mf_myframe0000000000000000000000");
|
||||
let expected_url = "https://cdn.example.com/frames/my_frame.png";
|
||||
let mut map = HashMap::new();
|
||||
map.insert(token.clone(), Url::parse(expected_url).unwrap());
|
||||
|
||||
let request = make_request(Some("p"), None, None, None, None, Some(token));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
match &plan.mode {
|
||||
FalVeo3Mode::ImageToVideo { image_url } => {
|
||||
assert_eq!(image_url, expected_url);
|
||||
}
|
||||
other => panic!("expected ImageToVideo, got {:?}", other),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Aspect ratio is None in image mode ────────────────────────────────
|
||||
|
||||
// ── Aspect ratio in image mode: uses i2v (has Auto, no Square) ─────────
|
||||
|
||||
#[test]
|
||||
fn t2v_aspect_ratio_is_none_in_image_mode() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), Some(CommonAspectRatio::WideSixteenByNine), None, None, None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(plan.t2v_aspect_ratio.is_none());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn i2v_default_aspect_ratio_is_auto() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), None, None, None, None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(matches!(plan.i2v_aspect_ratio, Some(FalVeo3I2vAspectRatio::Auto)));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn i2v_wide_16x9_yields_wide() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), Some(CommonAspectRatio::WideSixteenByNine), None, None, None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(matches!(plan.i2v_aspect_ratio, Some(FalVeo3I2vAspectRatio::WideSixteenNine)));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn i2v_tall_9x16_yields_tall() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), Some(CommonAspectRatio::TallNineBySixteen), None, None, None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(matches!(plan.i2v_aspect_ratio, Some(FalVeo3I2vAspectRatio::TallNineSixteen)));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn i2v_auto_yields_auto() {
|
||||
for ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), Some(ar), None, None, None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(matches!(plan.i2v_aspect_ratio, Some(FalVeo3I2vAspectRatio::Auto)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn i2v_square_falls_back_to_auto() {
|
||||
for ar in [CommonAspectRatio::Square, CommonAspectRatio::SquareHd] {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), Some(ar), None, None, None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(matches!(plan.i2v_aspect_ratio, Some(FalVeo3I2vAspectRatio::Auto)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn i2v_unsupported_falls_back_to_auto() {
|
||||
for ar in [CommonAspectRatio::WideFourByThree, CommonAspectRatio::TallThreeByFour] {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), Some(ar), None, None, None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(matches!(plan.i2v_aspect_ratio, Some(FalVeo3I2vAspectRatio::Auto)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── Prompt passthrough in image mode ──────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn prompt_is_passed_through_in_image_mode() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("a horse on mars"), None, None, None, None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert_eq!(plan.prompt, "a horse on mars");
|
||||
});
|
||||
}
|
||||
|
||||
// ── Duration pass-throughs in image mode ──────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn duration_4_in_image_mode() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), None, None, Some(4), None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(matches!(plan.duration, FalVeo3Duration::FourSeconds));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_8_in_image_mode() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), None, None, Some(8), None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(matches!(plan.duration, FalVeo3Duration::EightSeconds));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_duration_in_image_mode() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), None, None, None, None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(matches!(plan.duration, FalVeo3Duration::Default));
|
||||
});
|
||||
}
|
||||
|
||||
// ── Resolution in image mode ──────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn resolution_passes_through_in_image_mode() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), None, Some(CommonResolution::TenEightyP), None, None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(matches!(plan.resolution, FalVeo3Resolution::TenEightyP));
|
||||
});
|
||||
}
|
||||
|
||||
// ── Audio in image mode ───────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn audio_passes_through_in_image_mode() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), None, None, None, Some(false), Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(!plan.generate_audio);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_audio_is_true_in_image_mode() {
|
||||
let (start, map) = fake_token("mf_start0000000000000000000000");
|
||||
let request = make_request(Some("p"), None, None, None, None, Some(start));
|
||||
with_image_plan(&request, &map, |plan| {
|
||||
assert!(plan.generate_audio);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
-2
@@ -29,13 +29,16 @@ use crate::http_server::endpoints::moderation::wallets::moderator_get_wallet_han
|
||||
use crate::http_server::endpoints::moderation::jobs::get_tts_inference_queue_count::get_tts_inference_queue_count_handler;
|
||||
use crate::http_server::endpoints::moderation::jobs::get_w2l_inference_queue_count::get_w2l_inference_queue_count_handler;
|
||||
use crate::http_server::endpoints::moderation::jobs::kill_tts_inference_jobs::kill_tts_inference_jobs_handler;
|
||||
use crate::http_server::endpoints::moderation::user_bans::ban_user::ban_user_handler;
|
||||
use crate::http_server::endpoints::moderation::user_bans::moderation_ban_user_handler::moderation_ban_user_handler;
|
||||
use crate::http_server::endpoints::moderation::user::moderator_list_subscribing_users_by_signup_date::moderator_list_subscribing_users_by_signup_date_handler;
|
||||
use crate::http_server::endpoints::moderation::user::moderator_list_users_by_signup_date::moderator_list_users_by_signup_date_handler;
|
||||
use crate::http_server::endpoints::moderation::user::moderator_user_lookup_by_stripe_customer_id_handler::moderator_user_lookup_by_stripe_customer_id_handler;
|
||||
use crate::http_server::endpoints::moderation::user::moderator_user_lookup_handler::moderator_user_lookup_handler;
|
||||
use crate::http_server::endpoints::moderation::alerts::moderation_send_alert_handler::moderation_send_alert_handler;
|
||||
use crate::http_server::endpoints::moderation::staff_audit_logs::moderator_list_staff_audit_logs_handler::moderator_list_staff_audit_logs_handler;
|
||||
use crate::http_server::endpoints::moderation::user_feature_flags::edit_user_feature_flags_handler::edit_user_feature_flags_handler;
|
||||
use crate::http_server::endpoints::moderation::user_sessions::moderator_list_user_session_impersonation_requests_for_user_handler::moderator_list_user_session_impersonation_requests_for_user_handler;
|
||||
use crate::http_server::endpoints::moderation::user_sessions::moderator_list_user_session_impersonation_requests_handler::moderator_list_user_session_impersonation_requests_handler;
|
||||
use crate::http_server::endpoints::moderation::user_sessions::moderator_user_session_impersonation_request_handler::moderator_user_session_impersonation_request_handler;
|
||||
|
||||
pub fn add_moderator_routes<T, B> (app: App<T>) -> App<T>
|
||||
@@ -104,11 +107,25 @@ pub fn add_moderator_routes<T, B> (app: App<T>) -> App<T>
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
)
|
||||
.service(web::scope("/staff_audit_logs")
|
||||
.service(web::resource("/list")
|
||||
.route(web::get().to(moderator_list_staff_audit_logs_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
)
|
||||
.service(web::scope("/user_sessions")
|
||||
.service(web::resource("/impersonate")
|
||||
.route(web::post().to(moderator_user_session_impersonation_request_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
.service(web::resource("/impersonation_requests/list")
|
||||
.route(web::get().to(moderator_list_user_session_impersonation_requests_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
.service(web::resource("/impersonation_requests/user/{user_token}")
|
||||
.route(web::get().to(moderator_list_user_session_impersonation_requests_for_user_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
)
|
||||
.service(web::resource("/wallet_ledger_entry/{wallet_ledger_entry_token}")
|
||||
.route(web::get().to(moderator_get_wallet_ledger_entry_handler))
|
||||
@@ -166,7 +183,7 @@ pub fn add_moderator_routes<T, B> (app: App<T>) -> App<T>
|
||||
web::scope("/user_bans")
|
||||
.service(
|
||||
web::resource("/manage_ban")
|
||||
.route(web::post().to(ban_user_handler))
|
||||
.route(web::post().to(moderation_ban_user_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
)
|
||||
|
||||
@@ -46,8 +46,6 @@ use bootstrap::bootstrap::{bootstrap, BootstrapArgs};
|
||||
use chrono::Utc;
|
||||
use cloud_storage::bucket_client::BucketClient;
|
||||
use component_traits::traits::internal_user_lookup::InternalUserLookup;
|
||||
use shared_env_var_config::logging::DEFAULT_RUST_LOG;
|
||||
use shared_env_var_config::redis::env_get_redis_0_connection_string_or_default;
|
||||
use elasticsearch::http::transport::Transport;
|
||||
use elasticsearch::Elasticsearch;
|
||||
use errors::AnyhowResult;
|
||||
@@ -60,6 +58,8 @@ use mysql_queries::mediators::firehose_publisher::FirehosePublisher;
|
||||
use redis::Client;
|
||||
use redis_caching::redis_ttl_cache::RedisTtlCache;
|
||||
use reusable_types::server_environment::ServerEnvironment;
|
||||
use shared_env_var_config::logging::DEFAULT_RUST_LOG;
|
||||
use shared_env_var_config::redis::env_get_redis_0_connection_string_or_default;
|
||||
use tokio::runtime::Runtime;
|
||||
use url_config::third_party_url_redirector::ThirdPartyUrlRedirector;
|
||||
use user_traits_component::traits::internal_session_cache_purge::InternalSessionCachePurge;
|
||||
@@ -71,16 +71,15 @@ use crate::billing::stripe_internal_user_lookup_impl::StripeInternalUserLookupIm
|
||||
use crate::configs::app_startup::redis_rate_limiters::configure_redis_rate_limiters;
|
||||
use crate::configs::connect_to_database::connect_to_database;
|
||||
use crate::configs::static_api_tokens::StaticApiTokenSet;
|
||||
use actix_artcraft::sessions::anonymous_visitor_tracking::avt_cookie_manager::AvtCookieManager;
|
||||
use crate::http_server::middleware::error_alerting_middleware::error_alerting_middleware::ErrorAlertingMiddleware;
|
||||
use crate::http_server::middleware::pushback_filter_middleware::PushbackFilter;
|
||||
use crate::http_server::routes::add_routes::add_routes;
|
||||
use crate::http_server::session::session_checker::SessionChecker;
|
||||
use crate::http_server::web_utils::handle_multipart_error::handle_multipart_error;
|
||||
use crate::http_server::web_utils::scoped_temp_dir_creator::ScopedTempDirCreator;
|
||||
use crate::startup::build_pager::build_pager;
|
||||
use crate::state::certs::google_sign_in_cert::GoogleSignInCert;
|
||||
use crate::state::memory_cache::model_token_to_info_cache::ModelTokenToInfoCache;
|
||||
use crate::startup::build_pager::build_pager;
|
||||
use crate::state::server_state::{DurableInMemoryCaches, EnvConfig, EphemeralInMemoryCaches, FalData, InMemoryCaches, OpenAiData, ResendData, Seedance2ProData, ServerInfo, ServerState, StaticFeatureFlags, StripeSettings, TrollBans, WorldLabsData};
|
||||
use crate::threads::db_health_checker_thread::db_health_check_status::HealthCheckStatus;
|
||||
use crate::threads::db_health_checker_thread::db_health_checker_thread::db_health_checker_thread;
|
||||
@@ -89,6 +88,8 @@ use crate::threads::poll_model_token_info_thread::poll_model_token_info_thread;
|
||||
use crate::util::encrypted_sort_id::SortKeyCrypto;
|
||||
use crate::util::troll_user_bans::load_troll_user_ban_list_from_directory::load_user_token_ban_list_from_directory;
|
||||
use crate::util::troll_user_bans::troll_user_ban_list::TrollUserBanList;
|
||||
use actix_artcraft::sessions::anonymous_visitor_tracking::avt_cookie_manager::AvtCookieManager;
|
||||
use opaque_cursors::v2::opaque_cursor_encoder_v2::OpaqueCursorEncoderV2;
|
||||
|
||||
pub mod billing;
|
||||
pub mod configs;
|
||||
@@ -280,7 +281,9 @@ async fn main() -> AnyhowResult<()> {
|
||||
// We can even rotate it without too much impact to users.
|
||||
let sort_key_crypto_secret =
|
||||
easyenv::get_env_string_or_default("SORT_KEY_SECRET", "webscale");
|
||||
|
||||
let sort_key_crypto = SortKeyCrypto::new(&sort_key_crypto_secret);
|
||||
let opaque_cursor_encoder = OpaqueCursorEncoderV2::new(&sort_key_crypto_secret);
|
||||
|
||||
let health_check_interval = easyenv::get_env_duration_seconds_or_default(
|
||||
"HEALTH_CHECK_INTERVAL_SECS", Duration::from_secs(3));
|
||||
@@ -470,6 +473,7 @@ async fn main() -> AnyhowResult<()> {
|
||||
auto_gc_bucket_client,
|
||||
audio_uploads_bucket_root,
|
||||
sort_key_crypto,
|
||||
opaque_cursors: opaque_cursor_encoder,
|
||||
static_api_token_set,
|
||||
fal: FalData {
|
||||
api_key: fal_api_key,
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
use crate::configs::app_startup::username_set::UsernameSet;
|
||||
use crate::configs::static_api_tokens::StaticApiTokenSet;
|
||||
use crate::http_server::deprecated_endpoints::categories::tts::list_fully_computed_assigned_tts_categories::list_fully_computed_assigned_tts_categories::ModelTokensByCategoryToken;
|
||||
use crate::http_server::deprecated_endpoints::leaderboard::get_leaderboard::LeaderboardInfo;
|
||||
use crate::http_server::endpoints::media_files::list::list_featured_media_files_handler::ListFeaturedMediaFilesQueryParams;
|
||||
use crate::http_server::endpoints::stats::result_transformer::CacheableQueueStats;
|
||||
use crate::http_server::endpoints::tts::list_tts_models::TtsModelRecordForResponse;
|
||||
use crate::http_server::endpoints::voice_conversion::list_voice_conversion_models_handler::VoiceConversionModel;
|
||||
use crate::http_server::session::session_checker::SessionChecker;
|
||||
use crate::http_server::web_utils::redis_rate_limiter::RedisRateLimiter;
|
||||
use crate::http_server::web_utils::scoped_temp_dir_creator::ScopedTempDirCreator;
|
||||
use crate::state::certs::google_sign_in_cert::GoogleSignInCert;
|
||||
use crate::state::flags::paging_flags::PagingFlags;
|
||||
use crate::state::memory_cache::model_token_to_info_cache::ModelTokenToInfoCache;
|
||||
use crate::threads::db_health_checker_thread::db_health_check_status::HealthCheckStatus;
|
||||
use crate::util::encrypted_sort_id::SortKeyCrypto;
|
||||
use crate::util::troll_user_bans::troll_user_ban_list::TrollUserBanList;
|
||||
|
||||
use actix_artcraft::sessions::anonymous_visitor_tracking::avt_cookie_manager::AvtCookieManager;
|
||||
use actix_artcraft::sessions::user_sessions::http_user_session_manager::HttpUserSessionManager;
|
||||
use actix_helpers::middleware::banned_cidr_filter::banned_cidr_set::BannedCidrSet;
|
||||
use actix_helpers::middleware::banned_ip_filter::ip_ban_list::ip_ban_list::IpBanList;
|
||||
@@ -9,7 +28,6 @@ use elasticsearch::Elasticsearch;
|
||||
use fal_client::creds::fal_api_key::FalApiKey;
|
||||
use memory_caching::arc_ttl_sieve::ArcTtlSieve;
|
||||
use memory_caching::single_item_ttl_cache::SingleItemTtlCache;
|
||||
use pager::client::pager::Pager;
|
||||
use mysql_queries::mediators::badge_granter::BadgeGranter;
|
||||
use mysql_queries::mediators::firehose_publisher::FirehosePublisher;
|
||||
use mysql_queries::queries::generic_inference::web::get_pending_inference_job_count::InferenceQueueLengthResult;
|
||||
@@ -17,31 +35,14 @@ use mysql_queries::queries::media_files::list::list_featured_media_files::Featur
|
||||
use mysql_queries::queries::model_categories::list_categories_query_builder::CategoryList;
|
||||
use mysql_queries::queries::tts::tts_inference_jobs::get_pending_tts_inference_job_count::TtsQueueLengthResult;
|
||||
use mysql_queries::queries::w2l::w2l_templates::list_w2l_templates::W2lTemplateRecordForList;
|
||||
use opaque_cursors::v2::opaque_cursor_encoder_v2::OpaqueCursorEncoderV2;
|
||||
use pager::client::pager::Pager;
|
||||
use redis::Client;
|
||||
use redis_caching::redis_ttl_cache::RedisTtlCache;
|
||||
use reusable_types::server_environment::ServerEnvironment;
|
||||
use sqlx::MySqlPool;
|
||||
use url_config::third_party_url_redirector::ThirdPartyUrlRedirector;
|
||||
|
||||
use crate::configs::app_startup::username_set::UsernameSet;
|
||||
use crate::configs::static_api_tokens::StaticApiTokenSet;
|
||||
use actix_artcraft::sessions::anonymous_visitor_tracking::avt_cookie_manager::AvtCookieManager;
|
||||
use crate::http_server::deprecated_endpoints::categories::tts::list_fully_computed_assigned_tts_categories::list_fully_computed_assigned_tts_categories::ModelTokensByCategoryToken;
|
||||
use crate::http_server::deprecated_endpoints::leaderboard::get_leaderboard::LeaderboardInfo;
|
||||
use crate::http_server::endpoints::media_files::list::list_featured_media_files_handler::ListFeaturedMediaFilesQueryParams;
|
||||
use crate::http_server::endpoints::stats::result_transformer::CacheableQueueStats;
|
||||
use crate::http_server::endpoints::tts::list_tts_models::TtsModelRecordForResponse;
|
||||
use crate::http_server::endpoints::voice_conversion::list_voice_conversion_models_handler::VoiceConversionModel;
|
||||
use crate::http_server::session::session_checker::SessionChecker;
|
||||
use crate::state::flags::paging_flags::PagingFlags;
|
||||
use crate::http_server::web_utils::redis_rate_limiter::RedisRateLimiter;
|
||||
use crate::http_server::web_utils::scoped_temp_dir_creator::ScopedTempDirCreator;
|
||||
use crate::state::certs::google_sign_in_cert::GoogleSignInCert;
|
||||
use crate::state::memory_cache::model_token_to_info_cache::ModelTokenToInfoCache;
|
||||
use crate::threads::db_health_checker_thread::db_health_check_status::HealthCheckStatus;
|
||||
use crate::util::encrypted_sort_id::SortKeyCrypto;
|
||||
use crate::util::troll_user_bans::troll_user_ban_list::TrollUserBanList;
|
||||
|
||||
/// State that is injected into every endpoint.
|
||||
pub struct ServerState {
|
||||
/// Configuration from ENV vars.
|
||||
@@ -108,6 +109,7 @@ pub struct ServerState {
|
||||
pub audio_uploads_bucket_root: String,
|
||||
|
||||
pub sort_key_crypto: SortKeyCrypto,
|
||||
pub opaque_cursors: OpaqueCursorEncoderV2,
|
||||
|
||||
pub ip_ban_list: IpBanList,
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user