mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-21 09:22:41 -04:00
Compare commits
75
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75c4af5bd7 | ||
|
|
3f596e351a | ||
|
|
afd3012405 | ||
|
|
7ee5a6aa9a | ||
|
|
f5cd65be9c | ||
|
|
fba1a65e95 | ||
|
|
a1d08460db | ||
|
|
ba538142e4 | ||
|
|
99fd390bc5 | ||
|
|
cd283bf8de | ||
|
|
0c6021e95c | ||
|
|
859d295014 | ||
|
|
e116df736f | ||
|
|
9bb4a78111 | ||
|
|
edb0330cec | ||
|
|
146f152732 | ||
|
|
32b939a67e | ||
|
|
866a409ff1 | ||
|
|
92731d87ef | ||
|
|
f574d66897 | ||
|
|
21a30bde2d | ||
|
|
6a1eeaffb4 | ||
|
|
00065f120e | ||
|
|
e30dc03466 | ||
|
|
029789ac78 | ||
|
|
7b5550c546 | ||
|
|
0f0bd4c907 | ||
|
|
63f77a4b11 | ||
|
|
5ba6a3e226 | ||
|
|
7683fe34b7 | ||
|
|
1d41fa8174 | ||
|
|
8fe15839b0 | ||
|
|
1eada337f9 | ||
|
|
016f5107e0 | ||
|
|
224ce3f80b | ||
|
|
73a6d0494b | ||
|
|
0a5f220a7d | ||
|
|
d852d24226 | ||
|
|
e7da946e2d | ||
|
|
ea80dab67b | ||
|
|
957bb7ff2f | ||
|
|
2f41f345a2 | ||
|
|
53225932fb | ||
|
|
0c353f2a8c | ||
|
|
6036be1e01 | ||
|
|
197e91d4c6 | ||
|
|
5ddd23a8ff | ||
|
|
31eb00cfa7 | ||
|
|
7d6f262ffe | ||
|
|
33162e4337 | ||
|
|
5afb1db285 | ||
|
|
a511e0e0a4 | ||
|
|
0598aadaa1 | ||
|
|
06c6ff4b36 | ||
|
|
5bc759e511 | ||
|
|
02eafa96cc | ||
|
|
706bdb8d83 | ||
|
|
57534c7a77 | ||
|
|
2228a01466 | ||
|
|
262d931c3d | ||
|
|
a7826f8be3 | ||
|
|
dc0e59b8ce | ||
|
|
e1f3458c54 | ||
|
|
e385b3c6c3 | ||
|
|
dbedb25930 | ||
|
|
952d1e936c | ||
|
|
88799a482d | ||
|
|
851aba7e06 | ||
|
|
8ff9323ae6 | ||
|
|
50b085d450 | ||
|
|
1b70ee7394 | ||
|
|
1c3a176705 | ||
|
|
70c305f78d | ||
|
|
1c3178d073 | ||
|
|
1243e755fe |
@@ -1,6 +1,7 @@
|
||||
SERVER_ENVIRONMENT=development
|
||||
|
||||
DATABASE_URL=mysql://storyteller:password@localhost/storyteller
|
||||
#DATABASE_URL=sqlite:/tmp/tasks.sqlite
|
||||
#DATABASE_URL=mysql://root:root@localhost/storyteller
|
||||
#DATABASE_URL=mysql://root@localhost/storyteller
|
||||
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n UPDATE tasks\n SET\n task_status = ?,\n on_complete_batch_token = ?,\n on_complete_primary_media_file_token = ?,\n on_complete_primary_media_file_class = ?,\n on_complete_primary_media_file_cdn_url = ?,\n on_complete_primary_media_file_thumbnail_url_template = ?\n WHERE\n provider = ?\n AND provider_job_id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 8
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1e3758851fc8451670f86a9432009b73bdb81de4a08dcaa96a86afb5f9be7a68"
|
||||
}
|
||||
+3
-21
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT\n id,\n task_status,\n task_type,\n model_type,\n provider,\n provider_job_id,\n frontend_caller,\n frontend_subscriber_id,\n frontend_subscriber_payload,\n created_at as \"created_at: DateTime<Utc>\",\n updated_at as \"updated_at: DateTime<Utc>\",\n completed_at as \"completed_at: DateTime<Utc>\"\n FROM tasks\n WHERE is_dismissed_by_user == 0\n ",
|
||||
"query": "\n SELECT\n id,\n task_status,\n task_type,\n model_type,\n provider,\n provider_job_id,\n frontend_caller,\n frontend_subscriber_id,\n frontend_subscriber_payload\n FROM tasks\n WHERE\n provider = ?\n AND provider_job_id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -47,25 +47,10 @@
|
||||
"name": "frontend_subscriber_payload",
|
||||
"ordinal": 8,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at: DateTime<Utc>",
|
||||
"ordinal": 9,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "updated_at: DateTime<Utc>",
|
||||
"ordinal": 10,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "completed_at: DateTime<Utc>",
|
||||
"ordinal": 11,
|
||||
"type_info": "Int64"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
@@ -76,11 +61,8 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "a142dc3fdf5b6e741c268d8f2e9996a28c214f5835681297d58a9431da8de939"
|
||||
"hash": "38ff2df237b16f077865711dd81261de1b5fa05a9787679ce9ccd4ac576b1069"
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT\n id,\n task_status,\n task_type,\n model_type,\n provider,\n provider_job_id,\n frontend_caller,\n frontend_subscriber_id,\n frontend_subscriber_payload,\n on_complete_primary_media_file_token,\n on_complete_primary_media_file_class,\n on_complete_batch_token,\n on_complete_primary_media_file_cdn_url,\n on_complete_primary_media_file_thumbnail_url_template,\n created_at as \"created_at: DateTime<Utc>\",\n updated_at as \"updated_at: DateTime<Utc>\",\n completed_at as \"completed_at: DateTime<Utc>\"\n FROM tasks\n WHERE is_dismissed_by_user == 0\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "task_status",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "task_type",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "model_type",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "provider",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "provider_job_id",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "frontend_caller",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "frontend_subscriber_id",
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "frontend_subscriber_payload",
|
||||
"ordinal": 8,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "on_complete_primary_media_file_token",
|
||||
"ordinal": 9,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "on_complete_primary_media_file_class",
|
||||
"ordinal": 10,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "on_complete_batch_token",
|
||||
"ordinal": 11,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "on_complete_primary_media_file_cdn_url",
|
||||
"ordinal": 12,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "on_complete_primary_media_file_thumbnail_url_template",
|
||||
"ordinal": 13,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at: DateTime<Utc>",
|
||||
"ordinal": 14,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "updated_at: DateTime<Utc>",
|
||||
"ordinal": 15,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "completed_at: DateTime<Utc>",
|
||||
"ordinal": 16,
|
||||
"type_info": "Int64"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "47f94c127abc20683b25c257aed3c2190b9bd29eb829d61392f08e10fd986767"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n UPDATE tasks\n SET\n task_status = ?,\n on_complete_batch_token = ?,\n on_complete_primary_media_file_token = ?,\n on_complete_primary_media_file_class = ?,\n on_complete_primary_media_file_cdn_url = ?,\n on_complete_primary_media_file_thumbnail_url_template = ?\n WHERE id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 7
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "50a2449b87abed7df3302a74ecd0e8ab87940077a247636680cb09b2fe488321"
|
||||
}
|
||||
Generated
+547
-184
File diff suppressed because it is too large
Load Diff
+15
-4
@@ -12,6 +12,7 @@ members = [
|
||||
"crates/api_clients/fal_client",
|
||||
"crates/api_clients/google_drive_common",
|
||||
"crates/api_clients/google_sign_in",
|
||||
"crates/api_clients/grok_client",
|
||||
"crates/api_clients/midjourney_client",
|
||||
"crates/api_clients/openai_sora_client",
|
||||
"crates/api_clients/openart_client",
|
||||
@@ -43,6 +44,7 @@ members = [
|
||||
"crates/lib/caching/memory_store",
|
||||
"crates/lib/caching/redis_caching",
|
||||
"crates/lib/cloudflare_errors",
|
||||
"crates/lib/cloudflare_mitigation",
|
||||
"crates/lib/concurrency",
|
||||
"crates/lib/cookie_store",
|
||||
"crates/lib/cookies",
|
||||
@@ -84,6 +86,7 @@ members = [
|
||||
"crates/lib/testing",
|
||||
"crates/lib/tts_common",
|
||||
"crates/lib/url_config",
|
||||
"crates/lib/url_utils",
|
||||
"crates/lib/user_input_common",
|
||||
"crates/lib/web_scrapers",
|
||||
#"crates/lib/api_clients/fakeyou_client",
|
||||
@@ -185,6 +188,7 @@ artcraft_api_defs = { path = "crates/api_clients/artcraft/artcraft_api_defs" }
|
||||
build_metadata = { path = "crates/lib/build_metadata" }
|
||||
cloud_storage = { path = "crates/api_clients/cloud_storage" }
|
||||
cloudflare_errors = { path = "crates/lib/cloudflare_errors" }
|
||||
cloudflare_mitigation = { path = "crates/lib/cloudflare_mitigation" }
|
||||
component_traits = { path = "crates/service/plugins/component_traits" }
|
||||
cookie_store = { path = "crates/lib/cookie_store" }
|
||||
crockford = { path = "crates/lib/data/crockford" }
|
||||
@@ -194,6 +198,7 @@ errors = { path = "crates/lib/errors" }
|
||||
fal_client = { path = "crates/api_clients/fal_client" }
|
||||
file_to_base64_url = { path = "crates/lib/files/file_to_base64_url" }
|
||||
filesys = { path = "crates/lib/files/filesys" }
|
||||
grok_client = { path = "crates/api_clients/grok_client" }
|
||||
idempotency = { path = "crates/lib/idempotency" }
|
||||
images = { path = "crates/lib/files/images" }
|
||||
jwt_light = { path = "crates/lib/jwt_light" }
|
||||
@@ -210,6 +215,7 @@ subprocess_common = { path = "crates/lib/subprocess_common" }
|
||||
test_data = { path = "crates/lib/test_data" }
|
||||
testing = { path = "crates/lib/testing" }
|
||||
tokens = { path = "crates/schema/public/tokens" }
|
||||
url_utils = { path = "crates/lib/url_utils" }
|
||||
web_base64 = { path = "crates/lib/data/web_base64" }
|
||||
|
||||
# External libs
|
||||
@@ -227,22 +233,26 @@ actix-web-actors = "4.3.1+deprecated"
|
||||
actix-web-lab = "0.20.2"
|
||||
anyhow = "1.0.98"
|
||||
base64 = "0.22.1"
|
||||
bstr = "1.12.1"
|
||||
bytes = "1.10.1"
|
||||
chrono = { version = "0.4.41", features = ["serde"] }
|
||||
cookie = "0.18.1"
|
||||
env = "1.0.1"
|
||||
env_logger = "0.11.8"
|
||||
futures = { version = "0.3.31" }
|
||||
hex-literal = "1.0.0"
|
||||
indexmap = "2.9.0"
|
||||
log = "0.4.27"
|
||||
once_cell = "1.21.3"
|
||||
rand = "0.9.1"
|
||||
regex = "1.11.1"
|
||||
reqwest = { version = "0.12.23", features = ["json", "cookies"] }
|
||||
regex = "1.12.2"
|
||||
reqwest = { version = "0.12.23", default-features = false, features = [ "charset", "http2", "system-proxy", "json", "cookies", "rustls-tls" ] }
|
||||
reqwest-websocket = { version = "0.5.0" }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_derive = "1.0.219"
|
||||
serde_json = "1.0.140"
|
||||
serde_yaml = "0.9.34+deprecated"
|
||||
sha2 = "0.11.0-rc.2"
|
||||
sha3 = "0.10.8"
|
||||
tauri = { version = "2.8.3", features = ["unstable", "devtools"] } # NB: "devtools" prevents App Store distribution; "ubstable" is needed for window access
|
||||
tauri-plugin-dialog = "2.3.3"
|
||||
@@ -255,6 +265,7 @@ thiserror = "2.0.12"
|
||||
tokio = { version = "1.45.1", features = ["macros"] }
|
||||
toml = "0.8.22"
|
||||
url = { version = "2.5.4", features = ["serde"] }
|
||||
uuid = "1.18.1"
|
||||
|
||||
# NB: wreq gets past fingerprinting, but we have to deal with platform-specific build issues.
|
||||
#
|
||||
@@ -266,5 +277,5 @@ url = { version = "2.5.4", features = ["serde"] }
|
||||
# choco install pkgconfiglite -y
|
||||
# choco install llvm -y
|
||||
# choco install nasm -y
|
||||
wreq = { version = "6.0.0-rc.20", features = ["json", "multipart", "cookies"] }
|
||||
wreq-util = "3.0.0-rc.5"
|
||||
wreq = { version = "6.0.0-rc.20", features = ["json", "multipart", "cookies", "ws", "stream"] }
|
||||
wreq-util = "3.0.0-rc.6"
|
||||
|
||||
@@ -10,6 +10,13 @@
|
||||
-- tasks_v2.sqlite - added model_type (nullable)
|
||||
-- tasks_v3.sqlite - added frontend_caller, comments
|
||||
-- tasks_v4.sqlite - added is_dismissed_by_user
|
||||
-- tasks_v5.sqlite - added five fields:
|
||||
-- on_complete_batch_token,
|
||||
-- on_complete_primary_media_file_token,
|
||||
-- on_complete_primary_media_file_class,
|
||||
-- on_complete_primary_media_file_cdn_url,
|
||||
-- on_complete_primary_media_file_thumbnail_url_template,
|
||||
--
|
||||
|
||||
CREATE TABLE tasks (
|
||||
-- Task auto-incrementing primary key.
|
||||
@@ -54,6 +61,29 @@ CREATE TABLE tasks (
|
||||
-- Whether the user has dismissed the task from view.
|
||||
is_dismissed_by_user INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- When the generation completes, the batch token (if any).
|
||||
-- This will point to a collection of multiple files.
|
||||
on_complete_batch_token TEXT,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- When the generation completes, the media token of the "primary" media file.
|
||||
-- We'll probably always generate homogenous types, but just in case this is the first or primary item in a batch.
|
||||
on_complete_primary_media_file_token TEXT,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- When the generation completes, the file type of the primary media file.
|
||||
-- We'll probably always generate homogenous types, but just in case this is the first or primary item in a batch.
|
||||
on_complete_primary_media_file_class TEXT,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- The URL to the media file.
|
||||
on_complete_primary_media_file_cdn_url TEXT,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- The URL template for the thumbnail of the primary media file.
|
||||
on_complete_primary_media_file_thumbnail_url_template TEXT,
|
||||
|
||||
created_at INTEGER NOT NULL DEFAULT (unixepoch('now')),
|
||||
updated_at INTEGER NOT NULL DEFAULT (unixepoch('now')),
|
||||
completed_at INTEGER DEFAULT NULL
|
||||
|
||||
@@ -152,6 +152,7 @@ CREATE TABLE media_files (
|
||||
-- * `vmd`
|
||||
-- * `pmx`
|
||||
-- * `csv`
|
||||
-- * `spz` Gaussian splats
|
||||
media_type VARCHAR(16) NOT NULL,
|
||||
|
||||
-- Broad class of media:
|
||||
|
||||
@@ -5,4 +5,5 @@ pub mod generate_flux_pro_11_text_to_image;
|
||||
pub mod generate_flux_pro_11_ultra_text_to_image;
|
||||
pub mod generate_gpt_image_1_text_to_image;
|
||||
pub mod inpaint;
|
||||
pub mod multi_function;
|
||||
pub mod remove_image_background;
|
||||
@@ -0,0 +1 @@
|
||||
pub mod nano_banana_pro_multi_function_image_gen;
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub const NANO_BANANA_PRO_MULTI_FUNCTION_IMAGE_GEN_PATH: &str = "/v1/generate/image/multi_function/nano_banana_pro";
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct NanoBananaProMultiFunctionImageGenRequest {
|
||||
/// Idempotency token to prevent duplicate requests.
|
||||
pub uuid_idempotency_token: String,
|
||||
|
||||
/// Text prompt to generate the image from.
|
||||
pub prompt: Option<String>,
|
||||
|
||||
/// Image media tokens to include in the editing context.
|
||||
/// If present, we're doing image editing (image-to-image / image-editing)
|
||||
/// If absent, we're doing image generation (text-to-image)
|
||||
pub image_media_tokens: Option<Vec<MediaFileToken>>,
|
||||
|
||||
/// Number of images to generate. Default is one.
|
||||
pub num_images: Option<NanoBananaProMultiFunctionImageGenNumImages>,
|
||||
|
||||
/// Resolution of the image to generate. Default is OneK (1K).
|
||||
pub resolution: Option<NanoBananaProMultiFunctionImageGenImageResolution>,
|
||||
|
||||
/// Aspect ratio of the images to generate. Default is "1:1"
|
||||
pub aspect_ratio: Option<NanoBananaProMultiFunctionImageGenAspectRatio>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum NanoBananaProMultiFunctionImageGenNumImages {
|
||||
One, // Default
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum NanoBananaProMultiFunctionImageGenImageResolution {
|
||||
OneK,
|
||||
TwoK,
|
||||
FourK,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum NanoBananaProMultiFunctionImageGenAspectRatio {
|
||||
// Square
|
||||
OneByOne, // Default
|
||||
// Wide
|
||||
FiveByFour,
|
||||
FourByThree,
|
||||
ThreeByTwo,
|
||||
SixteenByNine,
|
||||
TwentyOneByNine,
|
||||
// Tall
|
||||
FourByFive,
|
||||
ThreeByFour,
|
||||
TwoByThree,
|
||||
NineBySixteen, // NB: No NineByTwentyOne ?
|
||||
}
|
||||
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct NanoBananaProMultiFunctionImageGenResponse {
|
||||
pub success: bool,
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
}
|
||||
@@ -6,4 +6,5 @@ pub mod jobs;
|
||||
pub mod media_file;
|
||||
pub mod prompts;
|
||||
pub mod stripe_artcraft;
|
||||
pub mod subscriptions;
|
||||
pub mod subscriptions;
|
||||
pub mod utils;
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
use crate::common::responses::media_links::MediaLinks;
|
||||
|
||||
/// Get the thumbnail template for either images or videos.
|
||||
pub fn media_links_to_thumbnail_template(links: &MediaLinks) -> Option<&str> {
|
||||
// NB: We only populate video previews for video tasks.
|
||||
if let Some(video) = links.maybe_video_previews.as_ref() {
|
||||
return Some(&video.animated_thumbnail_template);
|
||||
}
|
||||
|
||||
// Image case - use the image thumbnail template if it exists.
|
||||
links.maybe_thumbnail_template.as_deref()
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod media_links_to_thumbnail_template;
|
||||
@@ -5,4 +5,5 @@ pub mod generate_flux_pro_11_text_to_image;
|
||||
pub mod generate_flux_pro_11_ultra_text_to_image;
|
||||
pub mod generate_gpt_image_1_text_to_image;
|
||||
pub mod inpaint;
|
||||
pub mod multi_function;
|
||||
pub mod remove_image_background;
|
||||
+1
@@ -0,0 +1 @@
|
||||
pub mod nano_banana_pro_multi_function_image_gen_image;
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
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::generate::image::multi_function::nano_banana_pro_multi_function_image_gen::{NanoBananaProMultiFunctionImageGenRequest, NanoBananaProMultiFunctionImageGenResponse, NANO_BANANA_PRO_MULTI_FUNCTION_IMAGE_GEN_PATH};
|
||||
|
||||
pub async fn nano_banana_pro_multi_function_image_gen(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: NanoBananaProMultiFunctionImageGenRequest,
|
||||
) -> Result<NanoBananaProMultiFunctionImageGenResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
NANO_BANANA_PRO_MULTI_FUNCTION_IMAGE_GEN_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
@@ -36,7 +36,7 @@ env_logger = "0.11.3"
|
||||
async-trait = "0.1.75"
|
||||
# The "never-encode-slash" feature seems to have been removed:
|
||||
# https://github.com/durch/rust-s3/pull/254
|
||||
rust-s3 = { version = "0.36.0-beta.2" }
|
||||
rust-s3 = { version = "0.36.0-beta.2", default-features = false, features = ["fail-on-err", "tags", "tokio-rustls-tls"] }
|
||||
|
||||
# Runtime
|
||||
tokio.workspace = true
|
||||
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use fal::endpoints::fal_ai::bytedance::seedream::bytedance_seedream_v4_edit_image::{bytedance_seedream_v4_edit_image, BytedanceSeedreamV4EditImageInput};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct EnqueueBytedanceSeedreamV4EditImageArgs<'a, R: IntoUrl> {
|
||||
// Request required
|
||||
pub prompt: &'a str,
|
||||
pub image_urls: Vec<String>,
|
||||
|
||||
// Optional args
|
||||
pub num_images: Option<EnqueueBytedanceSeedreamV4EditImageNumImages>,
|
||||
pub max_images: Option<EnqueueBytedanceSeedreamV4EditImageMaxImages>,
|
||||
pub image_size: Option<EnqueueBytedanceSeedreamV4EditImageSize>,
|
||||
|
||||
// Fulfillment
|
||||
pub webhook_url: R,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueBytedanceSeedreamV4EditImageNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueBytedanceSeedreamV4EditImageMaxImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueBytedanceSeedreamV4EditImageSize {
|
||||
// Square
|
||||
Square,
|
||||
SquareHd,
|
||||
// Tall
|
||||
PortraitFourThree,
|
||||
PortraitSixteenNine,
|
||||
// Wide
|
||||
LandscapeFourThree,
|
||||
LandscapeSixteenNine,
|
||||
// Auto
|
||||
Auto,
|
||||
Auto2k,
|
||||
Auto4k,
|
||||
}
|
||||
|
||||
pub async fn enqueue_bytedance_seedream_v4_edit_image_webhook<R: IntoUrl>(
|
||||
args: EnqueueBytedanceSeedreamV4EditImageArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
let num_images = args.num_images
|
||||
.map(|num_images| match num_images {
|
||||
EnqueueBytedanceSeedreamV4EditImageNumImages::One => 1,
|
||||
EnqueueBytedanceSeedreamV4EditImageNumImages::Two => 2,
|
||||
EnqueueBytedanceSeedreamV4EditImageNumImages::Three => 3,
|
||||
EnqueueBytedanceSeedreamV4EditImageNumImages::Four => 4,
|
||||
});
|
||||
|
||||
let max_images = args.max_images
|
||||
.map(|num_images| match num_images {
|
||||
EnqueueBytedanceSeedreamV4EditImageMaxImages::One => 1,
|
||||
EnqueueBytedanceSeedreamV4EditImageMaxImages::Two => 2,
|
||||
EnqueueBytedanceSeedreamV4EditImageMaxImages::Three => 3,
|
||||
EnqueueBytedanceSeedreamV4EditImageMaxImages::Four => 4,
|
||||
});
|
||||
|
||||
let image_size = args.image_size
|
||||
.map(|image_size| match image_size {
|
||||
EnqueueBytedanceSeedreamV4EditImageSize::Square => "square",
|
||||
EnqueueBytedanceSeedreamV4EditImageSize::SquareHd => "square_hd",
|
||||
EnqueueBytedanceSeedreamV4EditImageSize::PortraitFourThree => "portrait_4_3",
|
||||
EnqueueBytedanceSeedreamV4EditImageSize::PortraitSixteenNine => "portrait_16_9",
|
||||
EnqueueBytedanceSeedreamV4EditImageSize::LandscapeFourThree => "landscape_4_3",
|
||||
EnqueueBytedanceSeedreamV4EditImageSize::LandscapeSixteenNine => "landscape_16_9",
|
||||
EnqueueBytedanceSeedreamV4EditImageSize::Auto => "auto",
|
||||
EnqueueBytedanceSeedreamV4EditImageSize::Auto2k => "auto_2K",
|
||||
EnqueueBytedanceSeedreamV4EditImageSize::Auto4k => "auto_4K",
|
||||
})
|
||||
.map(|resolution| resolution.to_string());
|
||||
|
||||
let request = BytedanceSeedreamV4EditImageInput {
|
||||
prompt: args.prompt.to_string(),
|
||||
image_urls: args.image_urls,
|
||||
// Optionals
|
||||
num_images,
|
||||
max_images,
|
||||
image_size,
|
||||
// Constants
|
||||
enhance_prompt_mode: Some("standard".to_string()),
|
||||
enable_safety_checker: Some(false),
|
||||
};
|
||||
|
||||
let result = bytedance_seedream_v4_edit_image(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::image::edit::enqueue_bytedance_seedream_v4_edit_image_webhook::{enqueue_bytedance_seedream_v4_edit_image_webhook, EnqueueBytedanceSeedreamV4EditImageArgs, EnqueueBytedanceSeedreamV4EditImageMaxImages, EnqueueBytedanceSeedreamV4EditImageNumImages, EnqueueBytedanceSeedreamV4EditImageSize};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use test_data::web::image_urls::{GHOST_IMAGE_URL, TREX_SKELETON_IMAGE_URL};
|
||||
|
||||
#[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 = EnqueueBytedanceSeedreamV4EditImageArgs {
|
||||
image_urls: vec![
|
||||
GHOST_IMAGE_URL.to_string(),
|
||||
TREX_SKELETON_IMAGE_URL.to_string(),
|
||||
],
|
||||
prompt: "add the ghost to the image of the t-rex skeleton, make it look spooky but friendly",
|
||||
num_images: Some(EnqueueBytedanceSeedreamV4EditImageNumImages::Two),
|
||||
max_images: Some(EnqueueBytedanceSeedreamV4EditImageMaxImages::Two),
|
||||
image_size: Some(EnqueueBytedanceSeedreamV4EditImageSize::Auto2k),
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let result = enqueue_bytedance_seedream_v4_edit_image_webhook(args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use fal::endpoints::fal_ai::bytedance::seedream::bytedance_seedream_v4p5_edit_image::{bytedance_seedream_v4p5_edit_image, BytedanceSeedreamV4p5EditImageInput};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct EnqueueBytedanceSeedreamV4p5EditImageArgs<'a, R: IntoUrl> {
|
||||
// Request required
|
||||
pub prompt: &'a str,
|
||||
pub image_urls: Vec<String>,
|
||||
|
||||
// Optional args
|
||||
pub num_images: Option<EnqueueBytedanceSeedreamV4p5EditImageNumImages>,
|
||||
pub max_images: Option<EnqueueBytedanceSeedreamV4p5EditImageMaxImages>,
|
||||
pub image_size: Option<EnqueueBytedanceSeedreamV4p5EditImageSize>,
|
||||
|
||||
// Fulfillment
|
||||
pub webhook_url: R,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueBytedanceSeedreamV4p5EditImageNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueBytedanceSeedreamV4p5EditImageMaxImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueBytedanceSeedreamV4p5EditImageSize {
|
||||
// Square
|
||||
Square,
|
||||
SquareHd,
|
||||
// Tall
|
||||
PortraitFourThree,
|
||||
PortraitSixteenNine,
|
||||
// Wide
|
||||
LandscapeFourThree,
|
||||
LandscapeSixteenNine,
|
||||
// Auto
|
||||
Auto2k,
|
||||
Auto4k,
|
||||
}
|
||||
|
||||
pub async fn enqueue_bytedance_seedream_v4p5_edit_image_webhook<R: IntoUrl>(
|
||||
args: EnqueueBytedanceSeedreamV4p5EditImageArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
let num_images = args.num_images
|
||||
.map(|num_images| match num_images {
|
||||
EnqueueBytedanceSeedreamV4p5EditImageNumImages::One => 1,
|
||||
EnqueueBytedanceSeedreamV4p5EditImageNumImages::Two => 2,
|
||||
EnqueueBytedanceSeedreamV4p5EditImageNumImages::Three => 3,
|
||||
EnqueueBytedanceSeedreamV4p5EditImageNumImages::Four => 4,
|
||||
});
|
||||
|
||||
let max_images = args.max_images
|
||||
.map(|num_images| match num_images {
|
||||
EnqueueBytedanceSeedreamV4p5EditImageMaxImages::One => 1,
|
||||
EnqueueBytedanceSeedreamV4p5EditImageMaxImages::Two => 2,
|
||||
EnqueueBytedanceSeedreamV4p5EditImageMaxImages::Three => 3,
|
||||
EnqueueBytedanceSeedreamV4p5EditImageMaxImages::Four => 4,
|
||||
});
|
||||
|
||||
let image_size = args.image_size
|
||||
.map(|image_size| match image_size {
|
||||
EnqueueBytedanceSeedreamV4p5EditImageSize::Square => "square",
|
||||
EnqueueBytedanceSeedreamV4p5EditImageSize::SquareHd => "square_hd",
|
||||
EnqueueBytedanceSeedreamV4p5EditImageSize::PortraitFourThree => "portrait_4_3",
|
||||
EnqueueBytedanceSeedreamV4p5EditImageSize::PortraitSixteenNine => "portrait_16_9",
|
||||
EnqueueBytedanceSeedreamV4p5EditImageSize::LandscapeFourThree => "landscape_4_3",
|
||||
EnqueueBytedanceSeedreamV4p5EditImageSize::LandscapeSixteenNine => "landscape_16_9",
|
||||
EnqueueBytedanceSeedreamV4p5EditImageSize::Auto2k => "auto_2K",
|
||||
EnqueueBytedanceSeedreamV4p5EditImageSize::Auto4k => "auto_4K",
|
||||
})
|
||||
.map(|resolution| resolution.to_string());
|
||||
|
||||
let request = BytedanceSeedreamV4p5EditImageInput {
|
||||
prompt: args.prompt.to_string(),
|
||||
image_urls: args.image_urls,
|
||||
// Optionals
|
||||
num_images,
|
||||
max_images,
|
||||
image_size,
|
||||
// Constants
|
||||
enable_safety_checker: Some(false),
|
||||
seed: None,
|
||||
};
|
||||
|
||||
let result = bytedance_seedream_v4p5_edit_image(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::image::edit::enqueue_bytedance_seedream_v4p5_edit_image_webhook::{enqueue_bytedance_seedream_v4p5_edit_image_webhook, EnqueueBytedanceSeedreamV4p5EditImageArgs, EnqueueBytedanceSeedreamV4p5EditImageMaxImages, EnqueueBytedanceSeedreamV4p5EditImageNumImages, EnqueueBytedanceSeedreamV4p5EditImageSize};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use test_data::web::image_urls::{GHOST_IMAGE_URL, TREX_SKELETON_IMAGE_URL};
|
||||
|
||||
#[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 secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = EnqueueBytedanceSeedreamV4p5EditImageArgs {
|
||||
image_urls: vec![
|
||||
GHOST_IMAGE_URL.to_string(),
|
||||
TREX_SKELETON_IMAGE_URL.to_string(),
|
||||
],
|
||||
prompt: "add the ghost to the image of the t-rex skeleton, make it look spooky but friendly",
|
||||
num_images: Some(EnqueueBytedanceSeedreamV4p5EditImageNumImages::Two),
|
||||
max_images: Some(EnqueueBytedanceSeedreamV4p5EditImageMaxImages::Two),
|
||||
image_size: Some(EnqueueBytedanceSeedreamV4p5EditImageSize::Auto2k),
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let result = enqueue_bytedance_seedream_v4p5_edit_image_webhook(args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use fal::endpoints::fal_ai::nano_banana_pro::nano_banana_pro_image_edit::{nano_banana_pro_image_edit, NanoBananaProImageEditInput};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct EnqueueNanoBananaProEditImageArgs<'a, R: IntoUrl> {
|
||||
// Request required
|
||||
pub prompt: &'a str,
|
||||
pub image_urls: Vec<String>,
|
||||
pub num_images: EnqueueNanoBananaProEditImageNumImages,
|
||||
|
||||
// Optional args
|
||||
pub resolution: Option<EnqueueNanoBananaProEditImageResolution>,
|
||||
pub aspect_ratio: Option<EnqueueNanoBananaProEditImageAspectRatio>,
|
||||
|
||||
// Fulfillment
|
||||
pub webhook_url: R,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueNanoBananaProEditImageNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueNanoBananaProEditImageResolution {
|
||||
OneK,
|
||||
TwoK,
|
||||
FourK,
|
||||
}
|
||||
|
||||
/// 21:9, 16:9, 3:2, 4:3, 5:4, 1:1, 4:5, 3:4, 2:3, 9:16
|
||||
/// Default is "1:1"
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueNanoBananaProEditImageAspectRatio {
|
||||
// Square
|
||||
OneByOne,
|
||||
// Wide
|
||||
FiveByFour,
|
||||
FourByThree,
|
||||
ThreeByTwo,
|
||||
SixteenByNine,
|
||||
TwentyOneByNine,
|
||||
// Tall
|
||||
FourByFive,
|
||||
ThreeByFour,
|
||||
TwoByThree,
|
||||
NineBySixteen, // NB: No NineByTwentyOne ?
|
||||
}
|
||||
|
||||
pub async fn enqueue_nano_banana_pro_image_edit_webhook<R: IntoUrl>(
|
||||
args: EnqueueNanoBananaProEditImageArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
let num_images = match args.num_images {
|
||||
EnqueueNanoBananaProEditImageNumImages::One => 1,
|
||||
EnqueueNanoBananaProEditImageNumImages::Two => 2,
|
||||
EnqueueNanoBananaProEditImageNumImages::Three => 3,
|
||||
EnqueueNanoBananaProEditImageNumImages::Four => 4,
|
||||
};
|
||||
|
||||
let resolution = args.resolution
|
||||
.map(|resolution| match resolution {
|
||||
EnqueueNanoBananaProEditImageResolution::OneK => "1K",
|
||||
EnqueueNanoBananaProEditImageResolution::TwoK => "2K",
|
||||
EnqueueNanoBananaProEditImageResolution::FourK => "4K",
|
||||
})
|
||||
.map(|resolution| resolution.to_string());
|
||||
|
||||
let aspect_ratio = args.aspect_ratio
|
||||
.map(|aspect_ratio| match aspect_ratio {
|
||||
// Square
|
||||
EnqueueNanoBananaProEditImageAspectRatio::OneByOne => "1:1",
|
||||
// Wide
|
||||
EnqueueNanoBananaProEditImageAspectRatio::FiveByFour => "5:4",
|
||||
EnqueueNanoBananaProEditImageAspectRatio::FourByThree => "4:3",
|
||||
EnqueueNanoBananaProEditImageAspectRatio::ThreeByTwo => "3:2",
|
||||
EnqueueNanoBananaProEditImageAspectRatio::SixteenByNine => "16:9",
|
||||
EnqueueNanoBananaProEditImageAspectRatio::TwentyOneByNine => "21:9",
|
||||
// Tall
|
||||
EnqueueNanoBananaProEditImageAspectRatio::FourByFive => "4:5",
|
||||
EnqueueNanoBananaProEditImageAspectRatio::ThreeByFour => "3:4",
|
||||
EnqueueNanoBananaProEditImageAspectRatio::TwoByThree => "2:3",
|
||||
EnqueueNanoBananaProEditImageAspectRatio::NineBySixteen => "9:16",
|
||||
})
|
||||
.map(|aspect_ratio| aspect_ratio.to_string());
|
||||
|
||||
let request = NanoBananaProImageEditInput {
|
||||
prompt: args.prompt.to_string(),
|
||||
image_urls: args.image_urls,
|
||||
num_images: Some(num_images),
|
||||
// Optionals
|
||||
aspect_ratio,
|
||||
resolution,
|
||||
// Constants
|
||||
output_format: Some("png".to_string()),
|
||||
};
|
||||
|
||||
let result = nano_banana_pro_image_edit(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::image::edit::enqueue_nano_banana_pro_edit_image_webhook::{enqueue_nano_banana_pro_image_edit_webhook, EnqueueNanoBananaProEditImageArgs, EnqueueNanoBananaProEditImageAspectRatio, EnqueueNanoBananaProEditImageNumImages, EnqueueNanoBananaProEditImageResolution};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use test_data::web::image_urls::{ERNEST_SCARED_STUPID_IMAGE_URL, GHOST_IMAGE_URL, TREX_SKELETON_IMAGE_URL};
|
||||
|
||||
#[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 = EnqueueNanoBananaProEditImageArgs {
|
||||
image_urls: vec![
|
||||
GHOST_IMAGE_URL.to_string(),
|
||||
TREX_SKELETON_IMAGE_URL.to_string(),
|
||||
ERNEST_SCARED_STUPID_IMAGE_URL.to_string(),
|
||||
],
|
||||
prompt: "add the ghost and scared man to the image of the t-rex skeleton, make it look spooky but friendly",
|
||||
num_images: EnqueueNanoBananaProEditImageNumImages::Two,
|
||||
aspect_ratio: Some(EnqueueNanoBananaProEditImageAspectRatio::SixteenByNine),
|
||||
resolution: Some(EnqueueNanoBananaProEditImageResolution::TwoK),
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let result = enqueue_nano_banana_pro_image_edit_webhook(args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
pub mod enqueue_bytedance_seedream_v4_edit_image_webhook;
|
||||
pub mod enqueue_bytedance_seedream_v4p5_edit_image_webhook;
|
||||
pub mod enqueue_flux_pro_kontext_max_edit_webhook;
|
||||
pub mod enqueue_gemini_25_flash_edit_webhook;
|
||||
pub mod enqueue_gpt_image_1_edit_image_webhook;
|
||||
pub mod enqueue_nano_banana_pro_edit_image_webhook;
|
||||
pub mod enqueue_qwen_image_edit_webhook;
|
||||
pub mod enqueue_seededit_v3_edit_webhook;
|
||||
@@ -6,3 +6,4 @@ pub mod enqueue_flux_pro_11_ultra_text_to_image_webhook;
|
||||
pub mod enqueue_gpt_image_1_text_to_image_webhook;
|
||||
pub mod infill;
|
||||
pub mod remove_background_rembg_webhook;
|
||||
pub mod text;
|
||||
|
||||
+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 fal::endpoints::fal_ai::bytedance::seedream::bytedance_seedream_v4_text_to_image::BytedanceSeedreamV4TextToImageInput;
|
||||
use fal::prelude::fal_ai::bytedance::seedream::bytedance_seedream_v4_text_to_image::bytedance_seedream_v4_text_to_image;
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct EnqueueBytedanceSeedreamV4TextToImageArgs<'a, R: IntoUrl> {
|
||||
// Request required
|
||||
pub prompt: &'a str,
|
||||
|
||||
// Optional args
|
||||
pub num_images: Option<EnqueueBytedanceSeedreamV4TextToImageNumImages>,
|
||||
pub max_images: Option<EnqueueBytedanceSeedreamV4TextToImageMaxImages>,
|
||||
pub image_size: Option<EnqueueBytedanceSeedreamV4TextToImageSize>,
|
||||
|
||||
// Fulfillment
|
||||
pub webhook_url: R,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueBytedanceSeedreamV4TextToImageNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueBytedanceSeedreamV4TextToImageMaxImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueBytedanceSeedreamV4TextToImageSize {
|
||||
// Square
|
||||
Square,
|
||||
SquareHd,
|
||||
// Tall
|
||||
PortraitFourThree,
|
||||
PortraitSixteenNine,
|
||||
// Wide
|
||||
LandscapeFourThree,
|
||||
LandscapeSixteenNine,
|
||||
// Auto
|
||||
Auto,
|
||||
Auto2k,
|
||||
Auto4k,
|
||||
}
|
||||
|
||||
pub async fn enqueue_bytedance_seedream_v4_text_to_image_webhook<R: IntoUrl>(
|
||||
args: EnqueueBytedanceSeedreamV4TextToImageArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
let num_images = args.num_images
|
||||
.map(|num_images| match num_images {
|
||||
EnqueueBytedanceSeedreamV4TextToImageNumImages::One => 1,
|
||||
EnqueueBytedanceSeedreamV4TextToImageNumImages::Two => 2,
|
||||
EnqueueBytedanceSeedreamV4TextToImageNumImages::Three => 3,
|
||||
EnqueueBytedanceSeedreamV4TextToImageNumImages::Four => 4,
|
||||
});
|
||||
|
||||
let max_images = args.max_images
|
||||
.map(|num_images| match num_images {
|
||||
EnqueueBytedanceSeedreamV4TextToImageMaxImages::One => 1,
|
||||
EnqueueBytedanceSeedreamV4TextToImageMaxImages::Two => 2,
|
||||
EnqueueBytedanceSeedreamV4TextToImageMaxImages::Three => 3,
|
||||
EnqueueBytedanceSeedreamV4TextToImageMaxImages::Four => 4,
|
||||
});
|
||||
|
||||
let image_size = args.image_size
|
||||
.map(|image_size| match image_size {
|
||||
EnqueueBytedanceSeedreamV4TextToImageSize::Square => "square",
|
||||
EnqueueBytedanceSeedreamV4TextToImageSize::SquareHd => "square_hd",
|
||||
EnqueueBytedanceSeedreamV4TextToImageSize::PortraitFourThree => "portrait_4_3",
|
||||
EnqueueBytedanceSeedreamV4TextToImageSize::PortraitSixteenNine => "portrait_16_9",
|
||||
EnqueueBytedanceSeedreamV4TextToImageSize::LandscapeFourThree => "landscape_4_3",
|
||||
EnqueueBytedanceSeedreamV4TextToImageSize::LandscapeSixteenNine => "landscape_16_9",
|
||||
EnqueueBytedanceSeedreamV4TextToImageSize::Auto => "auto",
|
||||
EnqueueBytedanceSeedreamV4TextToImageSize::Auto2k => "auto_2K",
|
||||
EnqueueBytedanceSeedreamV4TextToImageSize::Auto4k => "auto_4K",
|
||||
})
|
||||
.map(|resolution| resolution.to_string());
|
||||
|
||||
let request = BytedanceSeedreamV4TextToImageInput {
|
||||
prompt: args.prompt.to_string(),
|
||||
// Optionals
|
||||
num_images,
|
||||
max_images,
|
||||
image_size,
|
||||
// Constants
|
||||
enhance_prompt_mode: Some("standard".to_string()),
|
||||
enable_safety_checker: Some(false),
|
||||
};
|
||||
|
||||
let result = bytedance_seedream_v4_text_to_image(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::image::text::enqueue_bytedance_seedream_v4_text_to_image_webhook::{enqueue_bytedance_seedream_v4_text_to_image_webhook, EnqueueBytedanceSeedreamV4TextToImageArgs, EnqueueBytedanceSeedreamV4TextToImageMaxImages, EnqueueBytedanceSeedreamV4TextToImageNumImages, EnqueueBytedanceSeedreamV4TextToImageSize};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
|
||||
#[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 = EnqueueBytedanceSeedreamV4TextToImageArgs {
|
||||
prompt: "an anime girl is riding a t-rex in the forest",
|
||||
num_images: Some(EnqueueBytedanceSeedreamV4TextToImageNumImages::Two),
|
||||
max_images: Some(EnqueueBytedanceSeedreamV4TextToImageMaxImages::Two),
|
||||
image_size: Some(EnqueueBytedanceSeedreamV4TextToImageSize::LandscapeSixteenNine),
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let result = enqueue_bytedance_seedream_v4_text_to_image_webhook(args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use fal::endpoints::fal_ai::bytedance::seedream::bytedance_seedream_v4p5_text_to_image::{bytedance_seedream_v4p5_text_to_image, BytedanceSeedreamV4p5TextToImageInput};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct EnqueueBytedanceSeedreamV4p5TextToImageArgs<'a, R: IntoUrl> {
|
||||
// Request required
|
||||
pub prompt: &'a str,
|
||||
|
||||
// Optional args
|
||||
pub num_images: Option<EnqueueBytedanceSeedreamV4p5TextToImageNumImages>,
|
||||
pub max_images: Option<EnqueueBytedanceSeedreamV4p5TextToImageMaxImages>,
|
||||
pub image_size: Option<EnqueueBytedanceSeedreamV4p5TextToImageSize>,
|
||||
|
||||
// Fulfillment
|
||||
pub webhook_url: R,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueBytedanceSeedreamV4p5TextToImageNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueBytedanceSeedreamV4p5TextToImageMaxImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueBytedanceSeedreamV4p5TextToImageSize {
|
||||
// Square
|
||||
Square,
|
||||
SquareHd,
|
||||
// Tall
|
||||
PortraitFourThree,
|
||||
PortraitSixteenNine,
|
||||
// Wide
|
||||
LandscapeFourThree,
|
||||
LandscapeSixteenNine,
|
||||
// Auto
|
||||
Auto2k,
|
||||
Auto4k,
|
||||
}
|
||||
|
||||
pub async fn enqueue_bytedance_seedream_v4p5_text_to_image_webhook<R: IntoUrl>(
|
||||
args: EnqueueBytedanceSeedreamV4p5TextToImageArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
let num_images = args.num_images
|
||||
.map(|num_images| match num_images {
|
||||
EnqueueBytedanceSeedreamV4p5TextToImageNumImages::One => 1,
|
||||
EnqueueBytedanceSeedreamV4p5TextToImageNumImages::Two => 2,
|
||||
EnqueueBytedanceSeedreamV4p5TextToImageNumImages::Three => 3,
|
||||
EnqueueBytedanceSeedreamV4p5TextToImageNumImages::Four => 4,
|
||||
});
|
||||
|
||||
let max_images = args.max_images
|
||||
.map(|num_images| match num_images {
|
||||
EnqueueBytedanceSeedreamV4p5TextToImageMaxImages::One => 1,
|
||||
EnqueueBytedanceSeedreamV4p5TextToImageMaxImages::Two => 2,
|
||||
EnqueueBytedanceSeedreamV4p5TextToImageMaxImages::Three => 3,
|
||||
EnqueueBytedanceSeedreamV4p5TextToImageMaxImages::Four => 4,
|
||||
});
|
||||
|
||||
let image_size = args.image_size
|
||||
.map(|image_size| match image_size {
|
||||
EnqueueBytedanceSeedreamV4p5TextToImageSize::Square => "square",
|
||||
EnqueueBytedanceSeedreamV4p5TextToImageSize::SquareHd => "square_hd",
|
||||
EnqueueBytedanceSeedreamV4p5TextToImageSize::PortraitFourThree => "portrait_4_3",
|
||||
EnqueueBytedanceSeedreamV4p5TextToImageSize::PortraitSixteenNine => "portrait_16_9",
|
||||
EnqueueBytedanceSeedreamV4p5TextToImageSize::LandscapeFourThree => "landscape_4_3",
|
||||
EnqueueBytedanceSeedreamV4p5TextToImageSize::LandscapeSixteenNine => "landscape_16_9",
|
||||
EnqueueBytedanceSeedreamV4p5TextToImageSize::Auto2k => "auto_2K",
|
||||
EnqueueBytedanceSeedreamV4p5TextToImageSize::Auto4k => "auto_4K",
|
||||
})
|
||||
.map(|resolution| resolution.to_string());
|
||||
|
||||
let request = BytedanceSeedreamV4p5TextToImageInput {
|
||||
prompt: args.prompt.to_string(),
|
||||
// Optionals
|
||||
num_images,
|
||||
max_images,
|
||||
image_size,
|
||||
// Constants
|
||||
enable_safety_checker: Some(false),
|
||||
seed: None,
|
||||
};
|
||||
|
||||
let result = bytedance_seedream_v4p5_text_to_image(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::image::text::enqueue_bytedance_seedream_v4p5_text_to_image_webhook::{enqueue_bytedance_seedream_v4p5_text_to_image_webhook, EnqueueBytedanceSeedreamV4p5TextToImageArgs, EnqueueBytedanceSeedreamV4p5TextToImageMaxImages, EnqueueBytedanceSeedreamV4p5TextToImageNumImages, EnqueueBytedanceSeedreamV4p5TextToImageSize};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test() -> AnyhowResult<()> {
|
||||
// XXX: Don't commit secrets!
|
||||
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = EnqueueBytedanceSeedreamV4p5TextToImageArgs {
|
||||
prompt: "an anime girl is riding a t-rex in the forest",
|
||||
num_images: Some(EnqueueBytedanceSeedreamV4p5TextToImageNumImages::Two),
|
||||
max_images: Some(EnqueueBytedanceSeedreamV4p5TextToImageMaxImages::Two),
|
||||
image_size: Some(EnqueueBytedanceSeedreamV4p5TextToImageSize::LandscapeSixteenNine),
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let result = enqueue_bytedance_seedream_v4p5_text_to_image_webhook(args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use fal::endpoints::fal_ai::nano_banana_pro::nano_banana_pro_text_to_image::{nano_banana_pro_text_to_image, NanoBananaProTextToImageInput};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct EnqueueNanoBananaProTextToImageArgs<'a, R: IntoUrl> {
|
||||
// Request required
|
||||
pub prompt: &'a str,
|
||||
pub num_images: EnqueueNanoBananaProTextToImageNumImages,
|
||||
|
||||
// Optional args
|
||||
pub resolution: Option<EnqueueNanoBananaProTextToImageResolution>,
|
||||
pub aspect_ratio: Option<EnqueueNanoBananaProTextToImageAspectRatio>,
|
||||
|
||||
// Fulfillment
|
||||
pub webhook_url: R,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueNanoBananaProTextToImageNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueNanoBananaProTextToImageResolution {
|
||||
OneK,
|
||||
TwoK,
|
||||
FourK,
|
||||
}
|
||||
|
||||
/// 21:9, 16:9, 3:2, 4:3, 5:4, 1:1, 4:5, 3:4, 2:3, 9:16
|
||||
/// Default is "1:1"
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueNanoBananaProTextToImageAspectRatio {
|
||||
// Square
|
||||
OneByOne,
|
||||
// Wide
|
||||
FiveByFour,
|
||||
FourByThree,
|
||||
ThreeByTwo,
|
||||
SixteenByNine,
|
||||
TwentyOneByNine,
|
||||
// Tall
|
||||
FourByFive,
|
||||
ThreeByFour,
|
||||
TwoByThree,
|
||||
NineBySixteen, // NB: No NineByTwentyOne ?
|
||||
}
|
||||
|
||||
pub async fn enqueue_nano_banana_pro_text_to_image_webhook<R: IntoUrl>(
|
||||
args: EnqueueNanoBananaProTextToImageArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
let num_images = match args.num_images {
|
||||
EnqueueNanoBananaProTextToImageNumImages::One => 1,
|
||||
EnqueueNanoBananaProTextToImageNumImages::Two => 2,
|
||||
EnqueueNanoBananaProTextToImageNumImages::Three => 3,
|
||||
EnqueueNanoBananaProTextToImageNumImages::Four => 4,
|
||||
};
|
||||
|
||||
let resolution = args.resolution
|
||||
.map(|resolution| match resolution {
|
||||
EnqueueNanoBananaProTextToImageResolution::OneK => "1K",
|
||||
EnqueueNanoBananaProTextToImageResolution::TwoK => "2K",
|
||||
EnqueueNanoBananaProTextToImageResolution::FourK => "4K",
|
||||
})
|
||||
.map(|resolution| resolution.to_string());
|
||||
|
||||
let aspect_ratio = args.aspect_ratio
|
||||
.map(|aspect_ratio| match aspect_ratio {
|
||||
// Square
|
||||
EnqueueNanoBananaProTextToImageAspectRatio::OneByOne => "1:1",
|
||||
// Wide
|
||||
EnqueueNanoBananaProTextToImageAspectRatio::FiveByFour => "5:4",
|
||||
EnqueueNanoBananaProTextToImageAspectRatio::FourByThree => "4:3",
|
||||
EnqueueNanoBananaProTextToImageAspectRatio::ThreeByTwo => "3:2",
|
||||
EnqueueNanoBananaProTextToImageAspectRatio::SixteenByNine => "16:9",
|
||||
EnqueueNanoBananaProTextToImageAspectRatio::TwentyOneByNine => "21:9",
|
||||
// Tall
|
||||
EnqueueNanoBananaProTextToImageAspectRatio::FourByFive => "4:5",
|
||||
EnqueueNanoBananaProTextToImageAspectRatio::ThreeByFour => "3:4",
|
||||
EnqueueNanoBananaProTextToImageAspectRatio::TwoByThree => "2:3",
|
||||
EnqueueNanoBananaProTextToImageAspectRatio::NineBySixteen => "9:16",
|
||||
})
|
||||
.map(|aspect_ratio| aspect_ratio.to_string());
|
||||
|
||||
let request = NanoBananaProTextToImageInput {
|
||||
prompt: args.prompt.to_string(),
|
||||
num_images: Some(num_images),
|
||||
// Optionals
|
||||
aspect_ratio,
|
||||
resolution,
|
||||
// Constants
|
||||
output_format: Some("png".to_string()),
|
||||
};
|
||||
|
||||
let result = nano_banana_pro_text_to_image(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::image::text::enqueue_nano_banana_pro_text_to_image_webhook::{enqueue_nano_banana_pro_text_to_image_webhook, EnqueueNanoBananaProTextToImageArgs, EnqueueNanoBananaProTextToImageAspectRatio, EnqueueNanoBananaProTextToImageNumImages, EnqueueNanoBananaProTextToImageResolution};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
|
||||
#[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 = EnqueueNanoBananaProTextToImageArgs {
|
||||
prompt: "an anime girl riding on the back of a t-rex",
|
||||
num_images: EnqueueNanoBananaProTextToImageNumImages::One,
|
||||
aspect_ratio: Some(EnqueueNanoBananaProTextToImageAspectRatio::SixteenByNine),
|
||||
resolution: Some(EnqueueNanoBananaProTextToImageResolution::TwoK),
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let result = enqueue_nano_banana_pro_text_to_image_webhook(args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod enqueue_bytedance_seedream_v4_text_to_image_webhook;
|
||||
pub mod enqueue_bytedance_seedream_v4p5_text_to_image_webhook;
|
||||
pub mod enqueue_nano_banana_pro_text_to_image_webhook;
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use fal::endpoints::fal_ai::minimax::hailuo::hailuo_v2p3_fast_pro_image_to_video::{hailuo_v2p3_fast_pro_image_to_video, HailuoV2p3FastProImageToVideoInput};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct EnqueueHailuoV2p3FastProImageToVideoArgs<'a, R: IntoUrl> {
|
||||
// Request required
|
||||
pub prompt: String,
|
||||
pub image_url: String,
|
||||
|
||||
// Optional args
|
||||
pub prompt_optimizer: Option<bool>,
|
||||
|
||||
// Fulfillment
|
||||
pub webhook_url: R,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
pub async fn enqueue_hailuo_v2p3_fast_pro_image_to_video_webhook<R: IntoUrl>(
|
||||
args: EnqueueHailuoV2p3FastProImageToVideoArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
let prompt_optimizer = args.prompt_optimizer.unwrap_or(true);
|
||||
|
||||
let request = HailuoV2p3FastProImageToVideoInput {
|
||||
prompt: args.prompt,
|
||||
image_url: args.image_url,
|
||||
// Optionals
|
||||
prompt_optimizer: Some(prompt_optimizer),
|
||||
};
|
||||
|
||||
let result = hailuo_v2p3_fast_pro_image_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::image::enqueue_hailuo_v2p3_fast_pro_image_to_video_webhook::{enqueue_hailuo_v2p3_fast_pro_image_to_video_webhook, EnqueueHailuoV2p3FastProImageToVideoArgs};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use test_data::web::image_urls::TREX_SKELETON_IMAGE_URL;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test() -> AnyhowResult<()> {
|
||||
// XXX: Don't commit secrets!
|
||||
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = EnqueueHailuoV2p3FastProImageToVideoArgs {
|
||||
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
|
||||
prompt: "the t-rex skeleton gets off the podium and begins walking to the camera. the camera orbits slightly. The t-rex gets close and then bites.".to_string(),
|
||||
prompt_optimizer: Some(true),
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let result = enqueue_hailuo_v2p3_fast_pro_image_to_video_webhook(args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use fal::endpoints::fal_ai::minimax::hailuo::hailuo_v2p3_pro_image_to_video::{hailuo_v2p3_pro_image_to_video, HailuoV2p3ProImageToVideoInput};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct EnqueueHailuoV2p3ProImageToVideoArgs<'a, R: IntoUrl> {
|
||||
// Request required
|
||||
pub prompt: String,
|
||||
pub image_url: String,
|
||||
|
||||
// Optional args
|
||||
pub prompt_optimizer: Option<bool>,
|
||||
|
||||
// Fulfillment
|
||||
pub webhook_url: R,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
pub async fn enqueue_hailuo_v2p3_pro_image_to_video_webhook<R: IntoUrl>(
|
||||
args: EnqueueHailuoV2p3ProImageToVideoArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
let prompt_optimizer = args.prompt_optimizer.unwrap_or(true);
|
||||
|
||||
let request = HailuoV2p3ProImageToVideoInput {
|
||||
prompt: args.prompt,
|
||||
image_url: args.image_url,
|
||||
// Optionals
|
||||
prompt_optimizer: Some(prompt_optimizer),
|
||||
};
|
||||
|
||||
let result = hailuo_v2p3_pro_image_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::image::enqueue_hailuo_v2p3_pro_image_to_video_webhook::{enqueue_hailuo_v2p3_pro_image_to_video_webhook, EnqueueHailuoV2p3ProImageToVideoArgs};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use test_data::web::image_urls::TREX_SKELETON_IMAGE_URL;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test() -> AnyhowResult<()> {
|
||||
// XXX: Don't commit secrets!
|
||||
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = EnqueueHailuoV2p3ProImageToVideoArgs {
|
||||
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
|
||||
prompt: "the t-rex skeleton gets off the podium and begins walking to the camera. the camera orbits slightly. The t-rex gets close and then bites.".to_string(),
|
||||
prompt_optimizer: Some(true),
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let result = enqueue_hailuo_v2p3_pro_image_to_video_webhook(args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use fal::endpoints::fal_ai::kling_video::v2_5::kling_v2p5_turbo_pro_image_to_video::{kling_v2p5_turbo_pro_image_to_video, KlingV2p5TurboProImageToVideoInput};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct EnqueueKlingV2p5TurboProImageToVideoArgs<'a, R: IntoUrl> {
|
||||
// Request required
|
||||
pub prompt: String,
|
||||
pub image_url: String,
|
||||
|
||||
// Optional args
|
||||
pub tail_image_url: Option<String>,
|
||||
pub negative_prompt: Option<String>,
|
||||
|
||||
pub duration: Option<EnqueueKlingV2p5TurboProImageToVideoDurationSeconds>,
|
||||
|
||||
// Fulfillment
|
||||
pub webhook_url: R,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueKlingV2p5TurboProImageToVideoDurationSeconds {
|
||||
Five,
|
||||
Ten,
|
||||
}
|
||||
|
||||
pub async fn enqueue_kling_v2p5_turbo_pro_image_to_video_webhook<R: IntoUrl>(
|
||||
args: EnqueueKlingV2p5TurboProImageToVideoArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
let duration = args.duration
|
||||
.map(|resolution| match resolution {
|
||||
EnqueueKlingV2p5TurboProImageToVideoDurationSeconds::Five => "5",
|
||||
EnqueueKlingV2p5TurboProImageToVideoDurationSeconds::Ten => "10",
|
||||
})
|
||||
.map(|resolution| resolution.to_string());
|
||||
|
||||
let request = KlingV2p5TurboProImageToVideoInput {
|
||||
prompt: args.prompt,
|
||||
image_url: args.image_url,
|
||||
// Optionals
|
||||
duration,
|
||||
tail_image_url: args.tail_image_url,
|
||||
negative_prompt: args.negative_prompt,
|
||||
// Constants
|
||||
cfg_scale: None,
|
||||
};
|
||||
|
||||
let result = kling_v2p5_turbo_pro_image_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::image::enqueue_kling_v2p5_turbo_pro_image_to_video_webhook::{enqueue_kling_v2p5_turbo_pro_image_to_video_webhook, EnqueueKlingV2p5TurboProImageToVideoArgs, EnqueueKlingV2p5TurboProImageToVideoDurationSeconds};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use test_data::web::image_urls::TREX_SKELETON_IMAGE_URL;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test() -> AnyhowResult<()> {
|
||||
// XXX: Don't commit secrets!
|
||||
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = EnqueueKlingV2p5TurboProImageToVideoArgs {
|
||||
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
|
||||
prompt: "the t-rex skeleton gets off the podium and begins walking to the camera. the camera orbits slightly. The t-rex gets close and then bites.".to_string(),
|
||||
negative_prompt: None,
|
||||
tail_image_url: None,
|
||||
duration: Some(EnqueueKlingV2p5TurboProImageToVideoDurationSeconds::Five),
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let result = enqueue_kling_v2p5_turbo_pro_image_to_video_webhook(args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
use fal::endpoints::fal_ai::kling_video::v2_5::kling_v2p5_turbo_standard_image_to_video::{kling_v2p5_turbo_standard_image_to_video, KlingV2p5TurboStandardImageToVideoInput};
|
||||
|
||||
pub struct EnqueueKlingV2p5TurboStandardImageToVideoArgs<'a, R: IntoUrl> {
|
||||
// Request required
|
||||
pub prompt: String,
|
||||
pub image_url: String,
|
||||
|
||||
// Optional args
|
||||
pub negative_prompt: Option<String>,
|
||||
pub duration: Option<EnqueueKlingV2p5TurboStandardImageToVideoDurationSeconds>,
|
||||
|
||||
// Fulfillment
|
||||
pub webhook_url: R,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueKlingV2p5TurboStandardImageToVideoDurationSeconds {
|
||||
Five,
|
||||
Ten,
|
||||
}
|
||||
|
||||
pub async fn enqueue_kling_v2p5_turbo_standard_image_to_video_webhook<R: IntoUrl>(
|
||||
args: EnqueueKlingV2p5TurboStandardImageToVideoArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
let duration = args.duration
|
||||
.map(|resolution| match resolution {
|
||||
EnqueueKlingV2p5TurboStandardImageToVideoDurationSeconds::Five => "5",
|
||||
EnqueueKlingV2p5TurboStandardImageToVideoDurationSeconds::Ten => "10",
|
||||
})
|
||||
.map(|resolution| resolution.to_string());
|
||||
|
||||
let request = KlingV2p5TurboStandardImageToVideoInput {
|
||||
prompt: args.prompt,
|
||||
image_url: args.image_url,
|
||||
// Optionals
|
||||
duration,
|
||||
negative_prompt: args.negative_prompt,
|
||||
// Constants
|
||||
cfg_scale: None,
|
||||
};
|
||||
|
||||
let result = kling_v2p5_turbo_standard_image_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 errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use test_data::web::image_urls::TREX_SKELETON_IMAGE_URL;
|
||||
use crate::requests::webhook::video::image::enqueue_kling_v2p5_turbo_standard_image_to_video_webhook::{enqueue_kling_v2p5_turbo_standard_image_to_video_webhook, EnqueueKlingV2p5TurboStandardImageToVideoArgs, EnqueueKlingV2p5TurboStandardImageToVideoDurationSeconds};
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test() -> AnyhowResult<()> {
|
||||
// XXX: Don't commit secrets!
|
||||
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = EnqueueKlingV2p5TurboStandardImageToVideoArgs {
|
||||
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
|
||||
prompt: "the t-rex skeleton gets off the podium and begins walking to the camera. the camera orbits slightly. The t-rex gets close and then bites.".to_string(),
|
||||
negative_prompt: None,
|
||||
duration: Some(EnqueueKlingV2p5TurboStandardImageToVideoDurationSeconds::Five),
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let result = enqueue_kling_v2p5_turbo_standard_image_to_video_webhook(args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use fal::endpoints::fal_ai::kling_video::v2_6::kling_v2p6_pro_image_to_video::{kling_v2p6_pro_image_to_video, KlingV2p6ProImageToVideoInput};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct EnqueueKlingV2p6ProImageToVideoArgs<'a, R: IntoUrl> {
|
||||
// Request required
|
||||
pub prompt: String,
|
||||
pub image_url: String,
|
||||
|
||||
// Optional args
|
||||
pub generate_audio: Option<bool>,
|
||||
pub negative_prompt: Option<String>,
|
||||
pub duration: Option<EnqueueKlingV2p6ProImageToVideoDurationSeconds>,
|
||||
|
||||
// Fulfillment
|
||||
pub webhook_url: R,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueKlingV2p6ProImageToVideoDurationSeconds {
|
||||
Five,
|
||||
Ten,
|
||||
}
|
||||
|
||||
pub async fn enqueue_kling_v2p6_pro_image_to_video_webhook<R: IntoUrl>(
|
||||
args: EnqueueKlingV2p6ProImageToVideoArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
let duration = args.duration
|
||||
.map(|resolution| match resolution {
|
||||
EnqueueKlingV2p6ProImageToVideoDurationSeconds::Five => "5",
|
||||
EnqueueKlingV2p6ProImageToVideoDurationSeconds::Ten => "10",
|
||||
})
|
||||
.map(|resolution| resolution.to_string());
|
||||
|
||||
let request = KlingV2p6ProImageToVideoInput {
|
||||
prompt: args.prompt,
|
||||
image_url: args.image_url,
|
||||
// Optionals
|
||||
duration,
|
||||
negative_prompt: args.negative_prompt,
|
||||
generate_audio: args.generate_audio,
|
||||
};
|
||||
|
||||
let result = kling_v2p6_pro_image_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::image::enqueue_kling_v2p6_pro_image_to_video_webhook::{enqueue_kling_v2p6_pro_image_to_video_webhook, EnqueueKlingV2p6ProImageToVideoArgs, EnqueueKlingV2p6ProImageToVideoDurationSeconds};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use test_data::web::image_urls::ERNEST_SCARED_STUPID_IMAGE_URL;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test() -> AnyhowResult<()> {
|
||||
// XXX: Don't commit secrets!
|
||||
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = EnqueueKlingV2p6ProImageToVideoArgs {
|
||||
image_url: ERNEST_SCARED_STUPID_IMAGE_URL.to_string(),
|
||||
prompt: "the man says, 'these ghosts sure are scary!' and then runs. behind him, a group of ghosts appear from the trees and chase him. the camera tracks the action. finally, the man yells, 'go away ghosts'.".to_string(),
|
||||
negative_prompt: None,
|
||||
generate_audio: Some(true),
|
||||
duration: Some(EnqueueKlingV2p6ProImageToVideoDurationSeconds::Ten),
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let result = enqueue_kling_v2p6_pro_image_to_video_webhook(args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use fal::endpoints::fal_ai::pixverse::v5::pixverse_v5_image_to_video::{pixverse_v5_image_to_video, PixverseV5ImageToVideoInput};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct EnqueuePixverseV5ImageToVideoArgs<'a, R: IntoUrl> {
|
||||
// Request required
|
||||
pub prompt: String,
|
||||
pub image_url: String,
|
||||
|
||||
// Optional args
|
||||
pub negative_prompt: Option<String>,
|
||||
|
||||
// NB: eight-second videos do not work with 1080P
|
||||
pub duration: Option<EnqueuePixverseV5ImageToVideoDurationSeconds>,
|
||||
pub aspect_ratio: Option<EnqueuePixverseV5ImageToVideoAspectRatio>,
|
||||
pub resolution: Option<EnqueuePixverseV5ImageToVideoResolution>,
|
||||
pub style: Option<EnqueuePixverseV5ImageToVideoStyle>,
|
||||
|
||||
// Fulfillment
|
||||
pub webhook_url: R,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueuePixverseV5ImageToVideoDurationSeconds {
|
||||
Five,
|
||||
Eight,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueuePixverseV5ImageToVideoResolution {
|
||||
ThreeSixtyP,
|
||||
FiveFortyP,
|
||||
SevenTwentyP,
|
||||
TenEightyP,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueuePixverseV5ImageToVideoAspectRatio {
|
||||
Square,
|
||||
SixteenByNine,
|
||||
FourByThree,
|
||||
NineBySixteen,
|
||||
ThreeByFour,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueuePixverseV5ImageToVideoStyle {
|
||||
Anime,
|
||||
Animation3d,
|
||||
Clay,
|
||||
Comic,
|
||||
Cyberpunk,
|
||||
}
|
||||
|
||||
pub async fn enqueue_pixverse_v5_image_to_video_webhook<R: IntoUrl>(
|
||||
args: EnqueuePixverseV5ImageToVideoArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
let duration = args.duration
|
||||
.map(|resolution| match resolution {
|
||||
EnqueuePixverseV5ImageToVideoDurationSeconds::Five => "5",
|
||||
EnqueuePixverseV5ImageToVideoDurationSeconds::Eight=> "8",
|
||||
})
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let aspect_ratio = args.aspect_ratio
|
||||
.map(|aspect_ratio| match aspect_ratio {
|
||||
EnqueuePixverseV5ImageToVideoAspectRatio::Square => "1:1",
|
||||
EnqueuePixverseV5ImageToVideoAspectRatio::SixteenByNine => "16:9",
|
||||
EnqueuePixverseV5ImageToVideoAspectRatio::FourByThree => "4:3",
|
||||
EnqueuePixverseV5ImageToVideoAspectRatio::NineBySixteen => "9:16",
|
||||
EnqueuePixverseV5ImageToVideoAspectRatio::ThreeByFour => "3:4",
|
||||
})
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let resolution = args.resolution
|
||||
.map(|resolution| match resolution {
|
||||
EnqueuePixverseV5ImageToVideoResolution::ThreeSixtyP => "360p",
|
||||
EnqueuePixverseV5ImageToVideoResolution::FiveFortyP => "540p",
|
||||
EnqueuePixverseV5ImageToVideoResolution::SevenTwentyP => "720p",
|
||||
EnqueuePixverseV5ImageToVideoResolution::TenEightyP => "1080p",
|
||||
})
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let style = args.style
|
||||
.map(|style| match style {
|
||||
EnqueuePixverseV5ImageToVideoStyle::Anime => "anime",
|
||||
EnqueuePixverseV5ImageToVideoStyle::Animation3d => "3d_animation",
|
||||
EnqueuePixverseV5ImageToVideoStyle::Clay => "clay",
|
||||
EnqueuePixverseV5ImageToVideoStyle::Comic => "comic",
|
||||
EnqueuePixverseV5ImageToVideoStyle::Cyberpunk => "cyberpunk",
|
||||
})
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let request = PixverseV5ImageToVideoInput {
|
||||
prompt: args.prompt,
|
||||
image_url: args.image_url,
|
||||
// Optionals
|
||||
duration,
|
||||
negative_prompt: args.negative_prompt,
|
||||
aspect_ratio,
|
||||
resolution,
|
||||
style,
|
||||
// Constant
|
||||
seed: None,
|
||||
};
|
||||
|
||||
let result = pixverse_v5_image_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::image::enqueue_pixverse_v5_image_to_video_webhook::{enqueue_pixverse_v5_image_to_video_webhook, EnqueuePixverseV5ImageToVideoArgs, EnqueuePixverseV5ImageToVideoAspectRatio, EnqueuePixverseV5ImageToVideoDurationSeconds, EnqueuePixverseV5ImageToVideoResolution, EnqueuePixverseV5ImageToVideoStyle};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use test_data::web::image_urls::TREX_SKELETON_IMAGE_URL;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test() -> AnyhowResult<()> {
|
||||
// XXX: Don't commit secrets!
|
||||
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = EnqueuePixverseV5ImageToVideoArgs {
|
||||
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
|
||||
prompt: "the t-rex skeleton gets off the podium and begins walking to the camera. the camera orbits slightly. The t-rex gets close and then bites.".to_string(),
|
||||
negative_prompt: None,
|
||||
duration: Some(EnqueuePixverseV5ImageToVideoDurationSeconds::Five),
|
||||
style: Some(EnqueuePixverseV5ImageToVideoStyle::Cyberpunk),
|
||||
aspect_ratio: Some(EnqueuePixverseV5ImageToVideoAspectRatio::FourByThree),
|
||||
resolution: Some(EnqueuePixverseV5ImageToVideoResolution::TenEightyP),
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let result = enqueue_pixverse_v5_image_to_video_webhook(args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use fal::endpoints::fal_ai::sora::sora2::sora_2_image_to_video::{sora_2_image_to_video, Sora2ImageToVideoInput};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct EnqueueSora2ImageToVideoArgs<'a, R: IntoUrl> {
|
||||
// Request required
|
||||
pub prompt: String,
|
||||
pub image_url: String,
|
||||
|
||||
// Optional args
|
||||
pub resolution: Option<EnqueueSora2ImageToVideoResolution>,
|
||||
pub duration: Option<EnqueueSora2ImageToVideoDurationSeconds>,
|
||||
pub aspect_ratio: Option<EnqueueSora2ImageToVideoAspectRatio>,
|
||||
|
||||
// Fulfillment
|
||||
pub webhook_url: R,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueSora2ImageToVideoDurationSeconds {
|
||||
Four,
|
||||
Eight,
|
||||
Twelve,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueSora2ImageToVideoResolution {
|
||||
Auto,
|
||||
SevenTwentyP,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueSora2ImageToVideoAspectRatio {
|
||||
Auto,
|
||||
NineBySixteen,
|
||||
SixteenByNine,
|
||||
}
|
||||
|
||||
pub async fn enqueue_sora_2_image_to_video_webhook<R: IntoUrl>(
|
||||
args: EnqueueSora2ImageToVideoArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
let duration = args.duration
|
||||
.map(|resolution| match resolution {
|
||||
EnqueueSora2ImageToVideoDurationSeconds::Four => 4,
|
||||
EnqueueSora2ImageToVideoDurationSeconds::Eight => 8,
|
||||
EnqueueSora2ImageToVideoDurationSeconds::Twelve => 12,
|
||||
});
|
||||
|
||||
let resolution = args.resolution
|
||||
.map(|resolution| match resolution {
|
||||
EnqueueSora2ImageToVideoResolution::Auto => "auto",
|
||||
EnqueueSora2ImageToVideoResolution::SevenTwentyP => "720p",
|
||||
})
|
||||
.map(|resolution| resolution.to_string());
|
||||
|
||||
let aspect_ratio = args.aspect_ratio
|
||||
.map(|aspect_ratio| match aspect_ratio {
|
||||
EnqueueSora2ImageToVideoAspectRatio::Auto => "auto",
|
||||
EnqueueSora2ImageToVideoAspectRatio::NineBySixteen => "9:16",
|
||||
EnqueueSora2ImageToVideoAspectRatio::SixteenByNine => "16:9",
|
||||
})
|
||||
.map(|resolution| resolution.to_string());
|
||||
|
||||
let request = Sora2ImageToVideoInput {
|
||||
prompt: args.prompt,
|
||||
image_url: args.image_url,
|
||||
// Optionals
|
||||
duration,
|
||||
resolution,
|
||||
aspect_ratio,
|
||||
// Constants
|
||||
delete_video: Some(false),
|
||||
};
|
||||
|
||||
let result = sora_2_image_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::image::enqueue_sora_2_image_to_video_webhook::{enqueue_sora_2_image_to_video_webhook, EnqueueSora2ImageToVideoArgs, EnqueueSora2ImageToVideoAspectRatio, EnqueueSora2ImageToVideoDurationSeconds, EnqueueSora2ImageToVideoResolution};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use test_data::web::image_urls::TREX_SKELETON_IMAGE_URL;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test() -> AnyhowResult<()> {
|
||||
// XXX: Don't commit secrets!
|
||||
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = EnqueueSora2ImageToVideoArgs {
|
||||
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
|
||||
prompt: "the t-rex skeleton gets off the podium and begins walking to the camera. the camera orbits slightly. The t-rex gets close and then bites.".to_string(),
|
||||
duration: Some(EnqueueSora2ImageToVideoDurationSeconds::Twelve),
|
||||
aspect_ratio: Some(EnqueueSora2ImageToVideoAspectRatio::SixteenByNine),
|
||||
resolution: Some(EnqueueSora2ImageToVideoResolution::SevenTwentyP),
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let result = enqueue_sora_2_image_to_video_webhook(args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use fal::endpoints::fal_ai::sora::sora2::sora_2_pro_image_to_video::{sora_2_pro_image_to_video, Sora2ProImageToVideoInput};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct EnqueueSora2ProImageToVideoArgs<'a, R: IntoUrl> {
|
||||
// Request required
|
||||
pub prompt: String,
|
||||
pub image_url: String,
|
||||
|
||||
// Optional args
|
||||
pub resolution: Option<EnqueueSora2ProImageToVideoResolution>,
|
||||
pub duration: Option<EnqueueSora2ProImageToVideoDurationSeconds>,
|
||||
pub aspect_ratio: Option<EnqueueSora2ProImageToVideoAspectRatio>,
|
||||
|
||||
// Fulfillment
|
||||
pub webhook_url: R,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueSora2ProImageToVideoDurationSeconds {
|
||||
Four,
|
||||
Eight,
|
||||
Twelve,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueSora2ProImageToVideoResolution {
|
||||
Auto,
|
||||
SevenTwentyP,
|
||||
TenEightyP,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueSora2ProImageToVideoAspectRatio {
|
||||
Auto,
|
||||
NineBySixteen,
|
||||
SixteenByNine,
|
||||
}
|
||||
|
||||
pub async fn enqueue_sora_2_pro_image_to_video_webhook<R: IntoUrl>(
|
||||
args: EnqueueSora2ProImageToVideoArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
let duration = args.duration
|
||||
.map(|resolution| match resolution {
|
||||
EnqueueSora2ProImageToVideoDurationSeconds::Four => 4,
|
||||
EnqueueSora2ProImageToVideoDurationSeconds::Eight => 8,
|
||||
EnqueueSora2ProImageToVideoDurationSeconds::Twelve => 12,
|
||||
});
|
||||
|
||||
let resolution = args.resolution
|
||||
.map(|resolution| match resolution {
|
||||
EnqueueSora2ProImageToVideoResolution::Auto => "auto",
|
||||
EnqueueSora2ProImageToVideoResolution::SevenTwentyP => "720p",
|
||||
EnqueueSora2ProImageToVideoResolution::TenEightyP => "1080p",
|
||||
})
|
||||
.map(|resolution| resolution.to_string());
|
||||
|
||||
let aspect_ratio = args.aspect_ratio
|
||||
.map(|aspect_ratio| match aspect_ratio {
|
||||
EnqueueSora2ProImageToVideoAspectRatio::Auto => "auto",
|
||||
EnqueueSora2ProImageToVideoAspectRatio::NineBySixteen => "9:16",
|
||||
EnqueueSora2ProImageToVideoAspectRatio::SixteenByNine => "16:9",
|
||||
})
|
||||
.map(|resolution| resolution.to_string());
|
||||
|
||||
let request = Sora2ProImageToVideoInput {
|
||||
prompt: args.prompt,
|
||||
image_url: args.image_url,
|
||||
// Optionals
|
||||
duration,
|
||||
resolution,
|
||||
aspect_ratio,
|
||||
// Constants
|
||||
delete_video: Some(false),
|
||||
};
|
||||
|
||||
let result = sora_2_pro_image_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::image::enqueue_sora_2_pro_image_to_video_webhook::{enqueue_sora_2_pro_image_to_video_webhook, EnqueueSora2ProImageToVideoArgs, EnqueueSora2ProImageToVideoAspectRatio, EnqueueSora2ProImageToVideoDurationSeconds, EnqueueSora2ProImageToVideoResolution};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use test_data::web::image_urls::TREX_SKELETON_IMAGE_URL;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test() -> AnyhowResult<()> {
|
||||
// XXX: Don't commit secrets!
|
||||
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = EnqueueSora2ProImageToVideoArgs {
|
||||
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
|
||||
prompt: "the t-rex skeleton gets off the podium and begins walking to the camera. the camera orbits slightly. The t-rex gets close and then bites.".to_string(),
|
||||
duration: Some(EnqueueSora2ProImageToVideoDurationSeconds::Twelve),
|
||||
aspect_ratio: Some(EnqueueSora2ProImageToVideoAspectRatio::SixteenByNine),
|
||||
resolution: Some(EnqueueSora2ProImageToVideoResolution::TenEightyP),
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let result = enqueue_sora_2_pro_image_to_video_webhook(args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use fal::endpoints::fal_ai::veo::veo3_1::veo_3p1_fast_first_last_frame_image_to_video::{veo_3p1_fast_first_last_frame_image_to_video, Veo3p1FastFirstLastFrameImageToVideoInput};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct EnqueueVeo3p1FastFirstLastFrameImageToVideoArgs<'a, R: IntoUrl> {
|
||||
// Request required
|
||||
pub prompt: String,
|
||||
|
||||
// Starting frame
|
||||
pub first_frame_url: String,
|
||||
|
||||
// Ending frame
|
||||
pub last_frame_url: String,
|
||||
|
||||
// Optional args
|
||||
pub duration: Option<EnqueueVeo3p1FastFirstLastFrameImageToVideoDurationSeconds>,
|
||||
pub aspect_ratio: Option<EnqueueVeo3p1FastFirstLastFrameImageToVideoAspectRatio>,
|
||||
pub resolution: Option<EnqueueVeo3p1FastFirstLastFrameImageToVideoResolution>,
|
||||
pub generate_audio: Option<bool>,
|
||||
|
||||
// Fulfillment
|
||||
pub webhook_url: R,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueVeo3p1FastFirstLastFrameImageToVideoDurationSeconds {
|
||||
Four,
|
||||
Six,
|
||||
Eight,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueVeo3p1FastFirstLastFrameImageToVideoAspectRatio {
|
||||
Auto,
|
||||
Square,
|
||||
SixteenByNine,
|
||||
NineBySixteen,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueVeo3p1FastFirstLastFrameImageToVideoResolution {
|
||||
SevenTwentyP,
|
||||
TenEightyP,
|
||||
}
|
||||
|
||||
pub async fn enqueue_veo_3p1_fast_first_last_frame_image_to_video_webhook<R: IntoUrl>(
|
||||
args: EnqueueVeo3p1FastFirstLastFrameImageToVideoArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
let duration = args.duration
|
||||
.map(|resolution| match resolution {
|
||||
EnqueueVeo3p1FastFirstLastFrameImageToVideoDurationSeconds::Four => "4s",
|
||||
EnqueueVeo3p1FastFirstLastFrameImageToVideoDurationSeconds::Six => "6s",
|
||||
EnqueueVeo3p1FastFirstLastFrameImageToVideoDurationSeconds::Eight=> "8s",
|
||||
})
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let aspect_ratio = args.aspect_ratio
|
||||
.map(|aspect_ratio| match aspect_ratio {
|
||||
EnqueueVeo3p1FastFirstLastFrameImageToVideoAspectRatio::Auto => "auto",
|
||||
EnqueueVeo3p1FastFirstLastFrameImageToVideoAspectRatio::Square => "1:1",
|
||||
EnqueueVeo3p1FastFirstLastFrameImageToVideoAspectRatio::SixteenByNine => "16:9",
|
||||
EnqueueVeo3p1FastFirstLastFrameImageToVideoAspectRatio::NineBySixteen => "9:16",
|
||||
})
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let resolution = args.resolution
|
||||
.map(|resolution| match resolution {
|
||||
EnqueueVeo3p1FastFirstLastFrameImageToVideoResolution::SevenTwentyP => "720p",
|
||||
EnqueueVeo3p1FastFirstLastFrameImageToVideoResolution::TenEightyP => "1080p",
|
||||
})
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let request = Veo3p1FastFirstLastFrameImageToVideoInput {
|
||||
prompt: args.prompt,
|
||||
first_frame_url: args.first_frame_url,
|
||||
last_frame_url: args.last_frame_url,
|
||||
generate_audio: args.generate_audio,
|
||||
// Optionals
|
||||
duration,
|
||||
aspect_ratio,
|
||||
resolution,
|
||||
};
|
||||
|
||||
let result = veo_3p1_fast_first_last_frame_image_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::image::enqueue_veo_3p1_fast_first_last_frame_image_to_video_webhook::{enqueue_veo_3p1_fast_first_last_frame_image_to_video_webhook, EnqueueVeo3p1FastFirstLastFrameImageToVideoArgs, EnqueueVeo3p1FastFirstLastFrameImageToVideoAspectRatio, EnqueueVeo3p1FastFirstLastFrameImageToVideoDurationSeconds, EnqueueVeo3p1FastFirstLastFrameImageToVideoResolution};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use test_data::web::image_urls::{TALL_CORGI_SHIBA_TREASURE_OCEAN_URL, TALL_CORGI_SHIBA_TREASURE_SKY_URL};
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test() -> AnyhowResult<()> {
|
||||
// XXX: Don't commit secrets!
|
||||
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = EnqueueVeo3p1FastFirstLastFrameImageToVideoArgs {
|
||||
first_frame_url: TALL_CORGI_SHIBA_TREASURE_OCEAN_URL.to_string(),
|
||||
last_frame_url: TALL_CORGI_SHIBA_TREASURE_SKY_URL.to_string(),
|
||||
prompt: "There is a tiny ocean island with a corgi and shiba and treasure chest on it. The corgi and shiba are barking at the chest, when suddenly the island launches itself into the air. The camera tracks the island and follows it up high in the sky. The sun beams over the horizon. The dogs are happy and bark. The gold coins gleam in the sun.".to_string(),
|
||||
duration: Some(EnqueueVeo3p1FastFirstLastFrameImageToVideoDurationSeconds::Six),
|
||||
aspect_ratio: Some(EnqueueVeo3p1FastFirstLastFrameImageToVideoAspectRatio::NineBySixteen),
|
||||
resolution: Some(EnqueueVeo3p1FastFirstLastFrameImageToVideoResolution::SevenTwentyP),
|
||||
generate_audio: Some(true),
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let result = enqueue_veo_3p1_fast_first_last_frame_image_to_video_webhook(args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use fal::endpoints::fal_ai::veo::veo3_1::veo_3p1_fast_first_frame_image_to_video::{veo_3p1_fast_first_frame_image_to_video, Veo3p1FastFirstFrameImageToVideoInput};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct EnqueueVeo3p1FastImageToVideoArgs<'a, R: IntoUrl> {
|
||||
// Request required
|
||||
pub prompt: String,
|
||||
|
||||
// Starting frame
|
||||
pub image_url: String,
|
||||
|
||||
// Optional args
|
||||
pub duration: Option<EnqueueVeo3p1FastImageToVideoDurationSeconds>,
|
||||
pub aspect_ratio: Option<EnqueueVeo3p1FastImageToVideoAspectRatio>,
|
||||
pub resolution: Option<EnqueueVeo3p1FastImageToVideoResolution>,
|
||||
pub generate_audio: Option<bool>,
|
||||
|
||||
// Fulfillment
|
||||
pub webhook_url: R,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueVeo3p1FastImageToVideoDurationSeconds {
|
||||
Four,
|
||||
Six,
|
||||
Eight,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueVeo3p1FastImageToVideoAspectRatio {
|
||||
Auto,
|
||||
Square,
|
||||
SixteenByNine,
|
||||
NineBySixteen,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueVeo3p1FastImageToVideoResolution {
|
||||
SevenTwentyP,
|
||||
TenEightyP,
|
||||
}
|
||||
|
||||
pub async fn enqueue_veo_3p1_fast_image_to_video_webhook<R: IntoUrl>(
|
||||
args: EnqueueVeo3p1FastImageToVideoArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
let duration = args.duration
|
||||
.map(|resolution| match resolution {
|
||||
EnqueueVeo3p1FastImageToVideoDurationSeconds::Four => "4s",
|
||||
EnqueueVeo3p1FastImageToVideoDurationSeconds::Six => "6s",
|
||||
EnqueueVeo3p1FastImageToVideoDurationSeconds::Eight=> "8s",
|
||||
})
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let aspect_ratio = args.aspect_ratio
|
||||
.map(|aspect_ratio| match aspect_ratio {
|
||||
EnqueueVeo3p1FastImageToVideoAspectRatio::Auto => "auto",
|
||||
EnqueueVeo3p1FastImageToVideoAspectRatio::Square => "1:1",
|
||||
EnqueueVeo3p1FastImageToVideoAspectRatio::SixteenByNine => "16:9",
|
||||
EnqueueVeo3p1FastImageToVideoAspectRatio::NineBySixteen => "9:16",
|
||||
})
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let resolution = args.resolution
|
||||
.map(|resolution| match resolution {
|
||||
EnqueueVeo3p1FastImageToVideoResolution::SevenTwentyP => "720p",
|
||||
EnqueueVeo3p1FastImageToVideoResolution::TenEightyP => "1080p",
|
||||
})
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let request = Veo3p1FastFirstFrameImageToVideoInput {
|
||||
prompt: args.prompt,
|
||||
image_url: args.image_url,
|
||||
generate_audio: args.generate_audio,
|
||||
// Optionals
|
||||
duration,
|
||||
aspect_ratio,
|
||||
resolution,
|
||||
};
|
||||
|
||||
let result = veo_3p1_fast_first_frame_image_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::image::enqueue_veo_3p1_fast_image_to_video_webhook::{enqueue_veo_3p1_fast_image_to_video_webhook, EnqueueVeo3p1FastImageToVideoArgs, EnqueueVeo3p1FastImageToVideoAspectRatio, EnqueueVeo3p1FastImageToVideoDurationSeconds, EnqueueVeo3p1FastImageToVideoResolution};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use test_data::web::image_urls::TALL_CORGI_SHIBA_TREASURE_OCEAN_URL;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test() -> AnyhowResult<()> {
|
||||
// XXX: Don't commit secrets!
|
||||
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = EnqueueVeo3p1FastImageToVideoArgs {
|
||||
image_url: TALL_CORGI_SHIBA_TREASURE_OCEAN_URL.to_string(),
|
||||
prompt: "There is a tiny ocean island with a corgi and shiba and treasure chest on it. The corgi and shiba are barking at the chest, when suddenly the island launches itself into the air. The camera tracks the island and follows it up high in the sky. The sun beams over the horizon. The dogs are happy and bark. The gold coins gleam in the sun.".to_string(),
|
||||
duration: Some(EnqueueVeo3p1FastImageToVideoDurationSeconds::Four),
|
||||
aspect_ratio: Some(EnqueueVeo3p1FastImageToVideoAspectRatio::SixteenByNine),
|
||||
resolution: Some(EnqueueVeo3p1FastImageToVideoResolution::TenEightyP),
|
||||
generate_audio: Some(true),
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let result = enqueue_veo_3p1_fast_image_to_video_webhook(args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use fal::endpoints::fal_ai::veo::veo3_1::veo_3p1_first_last_frame_image_to_video::{veo_3p1_first_last_frame_image_to_video, Veo3p1FirstLastFrameImageToVideoInput};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct EnqueueVeo3p1FirstLastFrameImageToVideoArgs<'a, R: IntoUrl> {
|
||||
// Request required
|
||||
pub prompt: String,
|
||||
|
||||
// Starting frame
|
||||
pub first_frame_url: String,
|
||||
|
||||
// Ending frame
|
||||
pub last_frame_url: String,
|
||||
|
||||
// Optional args
|
||||
pub duration: Option<EnqueueVeo3p1FirstLastFrameImageToVideoDurationSeconds>,
|
||||
pub aspect_ratio: Option<EnqueueVeo3p1FirstLastFrameImageToVideoAspectRatio>,
|
||||
pub resolution: Option<EnqueueVeo3p1FirstLastFrameImageToVideoResolution>,
|
||||
pub generate_audio: Option<bool>,
|
||||
|
||||
// Fulfillment
|
||||
pub webhook_url: R,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueVeo3p1FirstLastFrameImageToVideoDurationSeconds {
|
||||
Four,
|
||||
Six,
|
||||
Eight,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueVeo3p1FirstLastFrameImageToVideoAspectRatio {
|
||||
Auto,
|
||||
Square,
|
||||
SixteenByNine,
|
||||
NineBySixteen,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueVeo3p1FirstLastFrameImageToVideoResolution {
|
||||
SevenTwentyP,
|
||||
TenEightyP,
|
||||
}
|
||||
|
||||
pub async fn enqueue_veo_3p1_first_last_frame_image_to_video_webhook<R: IntoUrl>(
|
||||
args: EnqueueVeo3p1FirstLastFrameImageToVideoArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
let duration = args.duration
|
||||
.map(|resolution| match resolution {
|
||||
EnqueueVeo3p1FirstLastFrameImageToVideoDurationSeconds::Four => "4s",
|
||||
EnqueueVeo3p1FirstLastFrameImageToVideoDurationSeconds::Six => "6s",
|
||||
EnqueueVeo3p1FirstLastFrameImageToVideoDurationSeconds::Eight=> "8s",
|
||||
})
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let aspect_ratio = args.aspect_ratio
|
||||
.map(|aspect_ratio| match aspect_ratio {
|
||||
EnqueueVeo3p1FirstLastFrameImageToVideoAspectRatio::Auto => "auto",
|
||||
EnqueueVeo3p1FirstLastFrameImageToVideoAspectRatio::Square => "1:1",
|
||||
EnqueueVeo3p1FirstLastFrameImageToVideoAspectRatio::SixteenByNine => "16:9",
|
||||
EnqueueVeo3p1FirstLastFrameImageToVideoAspectRatio::NineBySixteen => "9:16",
|
||||
})
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let resolution = args.resolution
|
||||
.map(|resolution| match resolution {
|
||||
EnqueueVeo3p1FirstLastFrameImageToVideoResolution::SevenTwentyP => "720p",
|
||||
EnqueueVeo3p1FirstLastFrameImageToVideoResolution::TenEightyP => "1080p",
|
||||
})
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let request = Veo3p1FirstLastFrameImageToVideoInput {
|
||||
prompt: args.prompt,
|
||||
first_frame_url: args.first_frame_url,
|
||||
last_frame_url: args.last_frame_url,
|
||||
generate_audio: args.generate_audio,
|
||||
// Optionals
|
||||
duration,
|
||||
aspect_ratio,
|
||||
resolution,
|
||||
};
|
||||
|
||||
let result = veo_3p1_first_last_frame_image_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::image::enqueue_veo_3p1_first_last_frame_image_to_video_webhook::{enqueue_veo_3p1_first_last_frame_image_to_video_webhook, EnqueueVeo3p1FirstLastFrameImageToVideoArgs, EnqueueVeo3p1FirstLastFrameImageToVideoAspectRatio, EnqueueVeo3p1FirstLastFrameImageToVideoDurationSeconds, EnqueueVeo3p1FirstLastFrameImageToVideoResolution};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use test_data::web::image_urls::{TALL_CORGI_SHIBA_TREASURE_OCEAN_URL, TALL_CORGI_SHIBA_TREASURE_SKY_URL};
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test() -> AnyhowResult<()> {
|
||||
// XXX: Don't commit secrets!
|
||||
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = EnqueueVeo3p1FirstLastFrameImageToVideoArgs {
|
||||
first_frame_url: TALL_CORGI_SHIBA_TREASURE_OCEAN_URL.to_string(),
|
||||
last_frame_url: TALL_CORGI_SHIBA_TREASURE_SKY_URL.to_string(),
|
||||
prompt: "There is a tiny ocean island with a corgi and shiba and treasure chest on it. The corgi and shiba are barking at the chest, when suddenly the island launches itself into the air. The camera tracks the island and follows it up high in the sky. The sun beams over the horizon. The dogs are happy and bark. The gold coins gleam in the sun.".to_string(),
|
||||
duration: Some(EnqueueVeo3p1FirstLastFrameImageToVideoDurationSeconds::Eight),
|
||||
aspect_ratio: Some(EnqueueVeo3p1FirstLastFrameImageToVideoAspectRatio::NineBySixteen),
|
||||
resolution: Some(EnqueueVeo3p1FirstLastFrameImageToVideoResolution::TenEightyP),
|
||||
generate_audio: Some(true),
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let result = enqueue_veo_3p1_first_last_frame_image_to_video_webhook(args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use fal::endpoints::fal_ai::veo::veo3_1::veo_3p1_first_frame_image_to_video::{veo_3p1_first_frame_image_to_video, Veo3p1FirstFrameImageToVideoInput};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct EnqueueVeo3p1ImageToVideoArgs<'a, R: IntoUrl> {
|
||||
// Request required
|
||||
pub prompt: String,
|
||||
|
||||
// Starting frame
|
||||
pub image_url: String,
|
||||
|
||||
// Optional args
|
||||
pub duration: Option<EnqueueVeo3p1ImageToVideoDurationSeconds>,
|
||||
pub aspect_ratio: Option<EnqueueVeo3p1ImageToVideoAspectRatio>,
|
||||
pub resolution: Option<EnqueueVeo3p1ImageToVideoResolution>,
|
||||
pub generate_audio: Option<bool>,
|
||||
|
||||
// Fulfillment
|
||||
pub webhook_url: R,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueVeo3p1ImageToVideoDurationSeconds {
|
||||
Four,
|
||||
Six,
|
||||
Eight,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueVeo3p1ImageToVideoAspectRatio {
|
||||
Auto,
|
||||
Square,
|
||||
SixteenByNine,
|
||||
NineBySixteen,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueVeo3p1ImageToVideoResolution {
|
||||
SevenTwentyP,
|
||||
TenEightyP,
|
||||
}
|
||||
|
||||
pub async fn enqueue_veo_3p1_image_to_video_webhook<R: IntoUrl>(
|
||||
args: EnqueueVeo3p1ImageToVideoArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
let duration = args.duration
|
||||
.map(|resolution| match resolution {
|
||||
EnqueueVeo3p1ImageToVideoDurationSeconds::Four => "4s",
|
||||
EnqueueVeo3p1ImageToVideoDurationSeconds::Six => "6s",
|
||||
EnqueueVeo3p1ImageToVideoDurationSeconds::Eight=> "8s",
|
||||
})
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let aspect_ratio = args.aspect_ratio
|
||||
.map(|aspect_ratio| match aspect_ratio {
|
||||
EnqueueVeo3p1ImageToVideoAspectRatio::Auto => "auto",
|
||||
EnqueueVeo3p1ImageToVideoAspectRatio::Square => "1:1",
|
||||
EnqueueVeo3p1ImageToVideoAspectRatio::SixteenByNine => "16:9",
|
||||
EnqueueVeo3p1ImageToVideoAspectRatio::NineBySixteen => "9:16",
|
||||
})
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let resolution = args.resolution
|
||||
.map(|resolution| match resolution {
|
||||
EnqueueVeo3p1ImageToVideoResolution::SevenTwentyP => "720p",
|
||||
EnqueueVeo3p1ImageToVideoResolution::TenEightyP => "1080p",
|
||||
})
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let request = Veo3p1FirstFrameImageToVideoInput {
|
||||
prompt: args.prompt,
|
||||
image_url: args.image_url,
|
||||
generate_audio: args.generate_audio,
|
||||
// Optionals
|
||||
duration,
|
||||
aspect_ratio,
|
||||
resolution,
|
||||
};
|
||||
|
||||
let result = veo_3p1_first_frame_image_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::image::enqueue_veo_3p1_image_to_video_webhook::{enqueue_veo_3p1_image_to_video_webhook, EnqueueVeo3p1ImageToVideoArgs, EnqueueVeo3p1ImageToVideoAspectRatio, EnqueueVeo3p1ImageToVideoDurationSeconds, EnqueueVeo3p1ImageToVideoResolution};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use test_data::web::image_urls::TALL_CORGI_SHIBA_TREASURE_OCEAN_URL;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test() -> AnyhowResult<()> {
|
||||
// XXX: Don't commit secrets!
|
||||
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = EnqueueVeo3p1ImageToVideoArgs {
|
||||
image_url: TALL_CORGI_SHIBA_TREASURE_OCEAN_URL.to_string(),
|
||||
prompt: "There is a tiny ocean island with a corgi and shiba and treasure chest on it. The corgi and shiba are barking at the chest, when suddenly the island launches itself into the air. The camera tracks the island and follows it up high in the sky. The sun beams over the horizon. The dogs are happy and bark. The gold coins gleam in the sun.".to_string(),
|
||||
duration: Some(EnqueueVeo3p1ImageToVideoDurationSeconds::Eight),
|
||||
aspect_ratio: Some(EnqueueVeo3p1ImageToVideoAspectRatio::NineBySixteen),
|
||||
resolution: Some(EnqueueVeo3p1ImageToVideoResolution::TenEightyP),
|
||||
generate_audio: Some(true),
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let result = enqueue_veo_3p1_image_to_video_webhook(args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
pub mod enqueue_hailuo_v2p3_fast_pro_image_to_video_webhook;
|
||||
pub mod enqueue_hailuo_v2p3_pro_image_to_video_webhook;
|
||||
pub mod enqueue_kling_v2p5_turbo_pro_image_to_video_webhook;
|
||||
pub mod enqueue_kling_v2p5_turbo_standard_image_to_video_webhook;
|
||||
pub mod enqueue_kling_v2p6_pro_image_to_video_webhook;
|
||||
pub mod enqueue_pixverse_v5_image_to_video_webhook;
|
||||
pub mod enqueue_sora_2_image_to_video_webhook;
|
||||
pub mod enqueue_sora_2_pro_image_to_video_webhook;
|
||||
pub mod enqueue_veo_3p1_fast_first_last_frame_image_to_video_webhook;
|
||||
pub mod enqueue_veo_3p1_fast_image_to_video_webhook;
|
||||
pub mod enqueue_veo_3p1_first_last_frame_image_to_video_webhook;
|
||||
pub mod enqueue_veo_3p1_image_to_video_webhook;
|
||||
@@ -5,4 +5,6 @@ pub mod enqueue_seedance_1_lite_image_to_video_webhook;
|
||||
pub mod enqueue_seedance_1_pro_image_to_video_webhook;
|
||||
pub mod enqueue_veo_2_image_to_video_webhook;
|
||||
pub mod enqueue_veo_3_fast_image_to_video_webhook;
|
||||
pub mod enqueue_veo_3_image_to_video_webhook;
|
||||
pub mod enqueue_veo_3_image_to_video_webhook;
|
||||
pub mod image;
|
||||
pub mod text;
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use fal::endpoints::fal_ai::minimax::hailuo::hailuo_v2p3_pro_text_to_video::{hailuo_v2p3_pro_text_to_video, HailuoV2p3ProTextToVideoInput};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct EnqueueHailuoV2p3ProTextToVideoArgs<'a, R: IntoUrl> {
|
||||
// Request required
|
||||
pub prompt: String,
|
||||
|
||||
// Optional args
|
||||
pub prompt_optimizer: Option<bool>,
|
||||
|
||||
// Fulfillment
|
||||
pub webhook_url: R,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
pub async fn enqueue_hailuo_v2p3_pro_text_to_video_webhook<R: IntoUrl>(
|
||||
args: EnqueueHailuoV2p3ProTextToVideoArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
let prompt_optimizer = args.prompt_optimizer.unwrap_or(true);
|
||||
|
||||
let request = HailuoV2p3ProTextToVideoInput {
|
||||
prompt: args.prompt,
|
||||
// Optionals
|
||||
prompt_optimizer: Some(prompt_optimizer),
|
||||
};
|
||||
|
||||
let result = hailuo_v2p3_pro_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_hailuo_v2p3_pro_text_to_video_webhook::{enqueue_hailuo_v2p3_pro_text_to_video_webhook, EnqueueHailuoV2p3ProTextToVideoArgs};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test() -> AnyhowResult<()> {
|
||||
// XXX: Don't commit secrets!
|
||||
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = EnqueueHailuoV2p3ProTextToVideoArgs {
|
||||
prompt: "a gray alien with big eyes dressed in an american flag tank top gives the peace symbol, it then barbecues some hot dogs on the grill".to_string(),
|
||||
prompt_optimizer: Some(true),
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let result = enqueue_hailuo_v2p3_pro_text_to_video_webhook(args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use fal::endpoints::fal_ai::kling_video::v2_5::kling_v2p5_turbo_pro_text_to_video::{kling_v2p5_turbo_pro_text_to_video, KlingV2p5TurboProTextToVideoInput};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct EnqueueKlingV2p5TurboProTextToVideoArgs<'a, R: IntoUrl> {
|
||||
// Request required
|
||||
pub prompt: String,
|
||||
|
||||
// Optional args
|
||||
pub negative_prompt: Option<String>,
|
||||
pub duration: Option<EnqueueKlingV2p5TurboProTextToVideoDurationSeconds>,
|
||||
pub aspect_ratio: Option<EnqueueKlingV2p5TurboProTextToVideoAspectRatio>,
|
||||
|
||||
// Fulfillment
|
||||
pub webhook_url: R,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueKlingV2p5TurboProTextToVideoDurationSeconds {
|
||||
Five,
|
||||
Ten,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueKlingV2p5TurboProTextToVideoAspectRatio {
|
||||
Square,
|
||||
SixteenByNine,
|
||||
NineBySixteen,
|
||||
}
|
||||
|
||||
pub async fn enqueue_kling_v2p5_turbo_pro_text_to_video_webhook<R: IntoUrl>(
|
||||
args: EnqueueKlingV2p5TurboProTextToVideoArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
let duration = args.duration
|
||||
.map(|resolution| match resolution {
|
||||
EnqueueKlingV2p5TurboProTextToVideoDurationSeconds::Five => "5",
|
||||
EnqueueKlingV2p5TurboProTextToVideoDurationSeconds::Ten => "10",
|
||||
})
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let aspect_ratio = args.aspect_ratio
|
||||
.map(|aspect| match aspect {
|
||||
EnqueueKlingV2p5TurboProTextToVideoAspectRatio::Square => "1:1",
|
||||
EnqueueKlingV2p5TurboProTextToVideoAspectRatio::SixteenByNine => "16:9",
|
||||
EnqueueKlingV2p5TurboProTextToVideoAspectRatio::NineBySixteen => "9:16",
|
||||
})
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let request = KlingV2p5TurboProTextToVideoInput {
|
||||
prompt: args.prompt,
|
||||
// Optionals
|
||||
duration,
|
||||
aspect_ratio,
|
||||
negative_prompt: args.negative_prompt,
|
||||
// Constants
|
||||
cfg_scale: None,
|
||||
};
|
||||
|
||||
let result = kling_v2p5_turbo_pro_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_kling_v2p5_turbo_pro_text_to_video_webhook::{enqueue_kling_v2p5_turbo_pro_text_to_video_webhook, EnqueueKlingV2p5TurboProTextToVideoArgs, EnqueueKlingV2p5TurboProTextToVideoAspectRatio, EnqueueKlingV2p5TurboProTextToVideoDurationSeconds};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test() -> AnyhowResult<()> {
|
||||
// XXX: Don't commit secrets!
|
||||
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = EnqueueKlingV2p5TurboProTextToVideoArgs {
|
||||
prompt: "an owl flies by an abandoned castle at dusk. the camera tracks the flying owl. the castle stones are damp and gritty. there are lots of trees. fireflies dance over the fields".to_string(),
|
||||
negative_prompt: None,
|
||||
aspect_ratio: Some(EnqueueKlingV2p5TurboProTextToVideoAspectRatio::SixteenByNine),
|
||||
duration: Some(EnqueueKlingV2p5TurboProTextToVideoDurationSeconds::Five),
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let result = enqueue_kling_v2p5_turbo_pro_text_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 fal::endpoints::fal_ai::kling_video::v2_6::kling_v2p6_pro_text_to_video::{kling_v2p6_pro_text_to_video, KlingV2p6ProTextToVideoInput};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct EnqueueKlingV2p6ProTextToVideoArgs<'a, R: IntoUrl> {
|
||||
// Request required
|
||||
pub prompt: String,
|
||||
|
||||
// Optional args
|
||||
pub generate_audio: Option<bool>,
|
||||
pub negative_prompt: Option<String>,
|
||||
pub duration: Option<EnqueueKlingV2p6ProTextToVideoDurationSeconds>,
|
||||
pub aspect_ratio: Option<EnqueueKlingV2p6ProTextToVideoAspectRatio>,
|
||||
|
||||
// Fulfillment
|
||||
pub webhook_url: R,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueKlingV2p6ProTextToVideoDurationSeconds {
|
||||
Five,
|
||||
Ten,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueKlingV2p6ProTextToVideoAspectRatio {
|
||||
Square,
|
||||
SixteenByNine,
|
||||
NineBySixteen,
|
||||
}
|
||||
|
||||
pub async fn enqueue_kling_v2p6_pro_text_to_video_webhook<R: IntoUrl>(
|
||||
args: EnqueueKlingV2p6ProTextToVideoArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
let duration = args.duration
|
||||
.map(|resolution| match resolution {
|
||||
EnqueueKlingV2p6ProTextToVideoDurationSeconds::Five => "5",
|
||||
EnqueueKlingV2p6ProTextToVideoDurationSeconds::Ten => "10",
|
||||
})
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let aspect_ratio = args.aspect_ratio
|
||||
.map(|aspect| match aspect {
|
||||
EnqueueKlingV2p6ProTextToVideoAspectRatio::Square => "1:1",
|
||||
EnqueueKlingV2p6ProTextToVideoAspectRatio::SixteenByNine => "16:9",
|
||||
EnqueueKlingV2p6ProTextToVideoAspectRatio::NineBySixteen => "9:16",
|
||||
})
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let request = KlingV2p6ProTextToVideoInput {
|
||||
prompt: args.prompt,
|
||||
// Optionals
|
||||
generate_audio: args.generate_audio,
|
||||
duration,
|
||||
aspect_ratio,
|
||||
negative_prompt: args.negative_prompt,
|
||||
// Constants
|
||||
cfg_scale: None,
|
||||
};
|
||||
|
||||
let result = kling_v2p6_pro_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 errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use crate::requests::webhook::video::text::enqueue_kling_v2p6_pro_text_to_video_webhook::{enqueue_kling_v2p6_pro_text_to_video_webhook, EnqueueKlingV2p6ProTextToVideoArgs, EnqueueKlingV2p6ProTextToVideoAspectRatio, EnqueueKlingV2p6ProTextToVideoDurationSeconds};
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test() -> AnyhowResult<()> {
|
||||
// XXX: Don't commit secrets!
|
||||
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = EnqueueKlingV2p6ProTextToVideoArgs {
|
||||
prompt: "a humanoid lizard creature walks around on the surface of an alien planet. the sky is purple and there are yellow plants. The lizard says, 'You should consider day trading'. Suddenly a giant t-rex comes in from off screen and eats the lizard creature.".to_string(),
|
||||
negative_prompt: None,
|
||||
aspect_ratio: Some(EnqueueKlingV2p6ProTextToVideoAspectRatio::SixteenByNine),
|
||||
duration: Some(EnqueueKlingV2p6ProTextToVideoDurationSeconds::Ten),
|
||||
generate_audio: Some(true),
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let result = enqueue_kling_v2p6_pro_text_to_video_webhook(args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use fal::endpoints::fal_ai::pixverse::v5::pixverse_v5_text_to_video::{pixverse_v5_text_to_video, PixverseV5TextToVideoInput};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct EnqueuePixverseV5TextToVideoArgs<'a, R: IntoUrl> {
|
||||
// Request required
|
||||
pub prompt: String,
|
||||
|
||||
// Optional args
|
||||
pub negative_prompt: Option<String>,
|
||||
|
||||
// NB: eight-second videos do not work with 1080P
|
||||
pub duration: Option<EnqueuePixverseV5TextToVideoDurationSeconds>,
|
||||
pub aspect_ratio: Option<EnqueuePixverseV5TextToVideoAspectRatio>,
|
||||
pub resolution: Option<EnqueuePixverseV5TextToVideoResolution>,
|
||||
pub style: Option<EnqueuePixverseV5TextToVideoStyle>,
|
||||
|
||||
// Fulfillment
|
||||
pub webhook_url: R,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueuePixverseV5TextToVideoDurationSeconds {
|
||||
Five,
|
||||
Eight,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueuePixverseV5TextToVideoResolution {
|
||||
ThreeSixtyP,
|
||||
FiveFortyP,
|
||||
SevenTwentyP,
|
||||
TenEightyP,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueuePixverseV5TextToVideoAspectRatio {
|
||||
Square,
|
||||
SixteenByNine,
|
||||
FourByThree,
|
||||
NineBySixteen,
|
||||
ThreeByFour,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueuePixverseV5TextToVideoStyle {
|
||||
Anime,
|
||||
Animation3d,
|
||||
Clay,
|
||||
Comic,
|
||||
Cyberpunk,
|
||||
}
|
||||
|
||||
pub async fn enqueue_pixverse_v5_text_to_video_webhook<R: IntoUrl>(
|
||||
args: EnqueuePixverseV5TextToVideoArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
let duration = args.duration
|
||||
.map(|resolution| match resolution {
|
||||
EnqueuePixverseV5TextToVideoDurationSeconds::Five => "5",
|
||||
EnqueuePixverseV5TextToVideoDurationSeconds::Eight=> "8",
|
||||
})
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let aspect_ratio = args.aspect_ratio
|
||||
.map(|aspect_ratio| match aspect_ratio {
|
||||
EnqueuePixverseV5TextToVideoAspectRatio::Square => "1:1",
|
||||
EnqueuePixverseV5TextToVideoAspectRatio::SixteenByNine => "16:9",
|
||||
EnqueuePixverseV5TextToVideoAspectRatio::FourByThree => "4:3",
|
||||
EnqueuePixverseV5TextToVideoAspectRatio::NineBySixteen => "9:16",
|
||||
EnqueuePixverseV5TextToVideoAspectRatio::ThreeByFour => "3:4",
|
||||
})
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let resolution = args.resolution
|
||||
.map(|resolution| match resolution {
|
||||
EnqueuePixverseV5TextToVideoResolution::ThreeSixtyP => "360p",
|
||||
EnqueuePixverseV5TextToVideoResolution::FiveFortyP => "540p",
|
||||
EnqueuePixverseV5TextToVideoResolution::SevenTwentyP => "720p",
|
||||
EnqueuePixverseV5TextToVideoResolution::TenEightyP => "1080p",
|
||||
})
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let style = args.style
|
||||
.map(|style| match style {
|
||||
EnqueuePixverseV5TextToVideoStyle::Anime => "anime",
|
||||
EnqueuePixverseV5TextToVideoStyle::Animation3d => "3d_animation",
|
||||
EnqueuePixverseV5TextToVideoStyle::Clay => "clay",
|
||||
EnqueuePixverseV5TextToVideoStyle::Comic => "comic",
|
||||
EnqueuePixverseV5TextToVideoStyle::Cyberpunk => "cyberpunk",
|
||||
})
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let request = PixverseV5TextToVideoInput {
|
||||
prompt: args.prompt,
|
||||
// Optionals
|
||||
duration,
|
||||
negative_prompt: args.negative_prompt,
|
||||
aspect_ratio,
|
||||
resolution,
|
||||
style,
|
||||
// Constant
|
||||
seed: None,
|
||||
};
|
||||
|
||||
let result = pixverse_v5_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_pixverse_v5_text_to_video_webhook::{enqueue_pixverse_v5_text_to_video_webhook, EnqueuePixverseV5TextToVideoArgs, EnqueuePixverseV5TextToVideoAspectRatio, EnqueuePixverseV5TextToVideoDurationSeconds, EnqueuePixverseV5TextToVideoResolution, EnqueuePixverseV5TextToVideoStyle};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test() -> AnyhowResult<()> {
|
||||
// XXX: Don't commit secrets!
|
||||
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = EnqueuePixverseV5TextToVideoArgs {
|
||||
prompt: "an angry racoon shakes its fist at the garbage truck as it drives away, the camera orbits the racoon. the racoon sighs".to_string(),
|
||||
negative_prompt: None,
|
||||
duration: Some(EnqueuePixverseV5TextToVideoDurationSeconds::Eight),
|
||||
style: Some(EnqueuePixverseV5TextToVideoStyle::Anime),
|
||||
aspect_ratio: Some(EnqueuePixverseV5TextToVideoAspectRatio::FourByThree),
|
||||
resolution: Some(EnqueuePixverseV5TextToVideoResolution::SevenTwentyP),
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let result = enqueue_pixverse_v5_text_to_video_webhook(args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
use fal::endpoints::fal_ai::sora::sora2::sora_2_pro_text_to_video::{sora_2_pro_text_to_video, Sora2ProTextToVideoInput};
|
||||
|
||||
pub struct EnqueueSora2ProTextToVideoArgs<'a, R: IntoUrl> {
|
||||
// Request required
|
||||
pub prompt: String,
|
||||
|
||||
// Optional args
|
||||
pub resolution: Option<EnqueueSora2ProTextToVideoResolution>,
|
||||
pub duration: Option<EnqueueSora2ProTextToVideoDurationSeconds>,
|
||||
pub aspect_ratio: Option<EnqueueSora2ProTextToVideoAspectRatio>,
|
||||
|
||||
// Fulfillment
|
||||
pub webhook_url: R,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueSora2ProTextToVideoDurationSeconds {
|
||||
Four,
|
||||
Eight,
|
||||
Twelve,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueSora2ProTextToVideoResolution {
|
||||
Auto,
|
||||
SevenTwentyP,
|
||||
TenEightyP,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueSora2ProTextToVideoAspectRatio {
|
||||
Auto,
|
||||
NineBySixteen,
|
||||
SixteenByNine,
|
||||
}
|
||||
|
||||
pub async fn enqueue_sora_2_pro_text_to_video_webhook<R: IntoUrl>(
|
||||
args: EnqueueSora2ProTextToVideoArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
let duration = args.duration
|
||||
.map(|resolution| match resolution {
|
||||
EnqueueSora2ProTextToVideoDurationSeconds::Four => 4,
|
||||
EnqueueSora2ProTextToVideoDurationSeconds::Eight => 8,
|
||||
EnqueueSora2ProTextToVideoDurationSeconds::Twelve => 12,
|
||||
});
|
||||
|
||||
let resolution = args.resolution
|
||||
.map(|resolution| match resolution {
|
||||
EnqueueSora2ProTextToVideoResolution::Auto => "auto",
|
||||
EnqueueSora2ProTextToVideoResolution::SevenTwentyP => "720p",
|
||||
EnqueueSora2ProTextToVideoResolution::TenEightyP => "1080p",
|
||||
})
|
||||
.map(|resolution| resolution.to_string());
|
||||
|
||||
let aspect_ratio = args.aspect_ratio
|
||||
.map(|aspect_ratio| match aspect_ratio {
|
||||
EnqueueSora2ProTextToVideoAspectRatio::Auto => "auto",
|
||||
EnqueueSora2ProTextToVideoAspectRatio::NineBySixteen => "9:16",
|
||||
EnqueueSora2ProTextToVideoAspectRatio::SixteenByNine => "16:9",
|
||||
})
|
||||
.map(|resolution| resolution.to_string());
|
||||
|
||||
let request = Sora2ProTextToVideoInput {
|
||||
prompt: args.prompt,
|
||||
// Optionals
|
||||
duration,
|
||||
resolution,
|
||||
aspect_ratio,
|
||||
// Constants
|
||||
delete_video: Some(false),
|
||||
};
|
||||
|
||||
let result = sora_2_pro_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 errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use crate::requests::webhook::video::text::enqueue_sora_2_pro_text_to_video_webhook::{enqueue_sora_2_pro_text_to_video_webhook, EnqueueSora2ProTextToVideoArgs, EnqueueSora2ProTextToVideoAspectRatio, EnqueueSora2ProTextToVideoDurationSeconds, EnqueueSora2ProTextToVideoResolution};
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test() -> AnyhowResult<()> {
|
||||
// XXX: Don't commit secrets!
|
||||
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = EnqueueSora2ProTextToVideoArgs {
|
||||
prompt: "a dinosaur turns to the camera and asks, 'do you have adequate car insurance?' it then stomps off and attacks a brontosaurus".to_string(),
|
||||
duration: Some(EnqueueSora2ProTextToVideoDurationSeconds::Eight),
|
||||
aspect_ratio: Some(EnqueueSora2ProTextToVideoAspectRatio::NineBySixteen),
|
||||
resolution: Some(EnqueueSora2ProTextToVideoResolution::SevenTwentyP),
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let result = enqueue_sora_2_pro_text_to_video_webhook(args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use fal::endpoints::fal_ai::sora::sora2::sora_2_text_to_video::{sora_2_text_to_video, Sora2TextToVideoInput};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct EnqueueSora2TextToVideoArgs<'a, R: IntoUrl> {
|
||||
// Request required
|
||||
pub prompt: String,
|
||||
|
||||
// Optional args
|
||||
pub resolution: Option<EnqueueSora2TextToVideoResolution>,
|
||||
pub duration: Option<EnqueueSora2TextToVideoDurationSeconds>,
|
||||
pub aspect_ratio: Option<EnqueueSora2TextToVideoAspectRatio>,
|
||||
|
||||
// Fulfillment
|
||||
pub webhook_url: R,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueSora2TextToVideoDurationSeconds {
|
||||
Four,
|
||||
Eight,
|
||||
Twelve,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueSora2TextToVideoResolution {
|
||||
Auto,
|
||||
SevenTwentyP,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueSora2TextToVideoAspectRatio {
|
||||
Auto,
|
||||
NineBySixteen,
|
||||
SixteenByNine,
|
||||
}
|
||||
|
||||
pub async fn enqueue_sora_2_text_to_video_webhook<R: IntoUrl>(
|
||||
args: EnqueueSora2TextToVideoArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
let duration = args.duration
|
||||
.map(|resolution| match resolution {
|
||||
EnqueueSora2TextToVideoDurationSeconds::Four => 4,
|
||||
EnqueueSora2TextToVideoDurationSeconds::Eight => 8,
|
||||
EnqueueSora2TextToVideoDurationSeconds::Twelve => 12,
|
||||
});
|
||||
|
||||
let resolution = args.resolution
|
||||
.map(|resolution| match resolution {
|
||||
EnqueueSora2TextToVideoResolution::Auto => "auto",
|
||||
EnqueueSora2TextToVideoResolution::SevenTwentyP => "720p",
|
||||
})
|
||||
.map(|resolution| resolution.to_string());
|
||||
|
||||
let aspect_ratio = args.aspect_ratio
|
||||
.map(|aspect_ratio| match aspect_ratio {
|
||||
EnqueueSora2TextToVideoAspectRatio::Auto => "auto",
|
||||
EnqueueSora2TextToVideoAspectRatio::NineBySixteen => "9:16",
|
||||
EnqueueSora2TextToVideoAspectRatio::SixteenByNine => "16:9",
|
||||
})
|
||||
.map(|resolution| resolution.to_string());
|
||||
|
||||
let request = Sora2TextToVideoInput {
|
||||
prompt: args.prompt,
|
||||
// Optionals
|
||||
duration,
|
||||
resolution,
|
||||
aspect_ratio,
|
||||
// Constants
|
||||
delete_video: Some(false),
|
||||
};
|
||||
|
||||
let result = sora_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_sora_2_text_to_video_webhook::{enqueue_sora_2_text_to_video_webhook, EnqueueSora2TextToVideoArgs, EnqueueSora2TextToVideoAspectRatio, EnqueueSora2TextToVideoDurationSeconds, EnqueueSora2TextToVideoResolution};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test() -> AnyhowResult<()> {
|
||||
// XXX: Don't commit secrets!
|
||||
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = EnqueueSora2TextToVideoArgs {
|
||||
prompt: "a dinosaur turns to the camera and asks, 'do you have adequate car insurance?' it then stomps off and attacks a brontosaurus".to_string(),
|
||||
duration: Some(EnqueueSora2TextToVideoDurationSeconds::Eight),
|
||||
aspect_ratio: Some(EnqueueSora2TextToVideoAspectRatio::NineBySixteen),
|
||||
resolution: Some(EnqueueSora2TextToVideoResolution::SevenTwentyP),
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let result = enqueue_sora_2_text_to_video_webhook(args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use fal::endpoints::fal_ai::veo::veo3_1::veo_3p1_fast_text_to_video::{veo_3p1_fast_text_to_video, Veo3p1FastTextToVideoInput};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct EnqueueVeo3p1FastTextToVideoArgs<'a, R: IntoUrl> {
|
||||
// Request required
|
||||
pub prompt: String,
|
||||
|
||||
// Optional args
|
||||
pub duration: Option<EnqueueVeo3p1FastTextToVideoDurationSeconds>,
|
||||
pub aspect_ratio: Option<EnqueueVeo3p1FastTextToVideoAspectRatio>,
|
||||
pub resolution: Option<EnqueueVeo3p1FastTextToVideoResolution>,
|
||||
pub generate_audio: Option<bool>,
|
||||
pub enhance_prompt: Option<bool>,
|
||||
pub negative_prompt: Option<String>,
|
||||
pub seed: Option<i64>,
|
||||
pub auto_fix: Option<bool>,
|
||||
|
||||
// Fulfillment
|
||||
pub webhook_url: R,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueVeo3p1FastTextToVideoDurationSeconds {
|
||||
Four,
|
||||
Six,
|
||||
Eight,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueVeo3p1FastTextToVideoAspectRatio {
|
||||
Auto,
|
||||
Square,
|
||||
SixteenByNine,
|
||||
NineBySixteen,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueVeo3p1FastTextToVideoResolution {
|
||||
SevenTwentyP,
|
||||
TenEightyP,
|
||||
}
|
||||
|
||||
pub async fn enqueue_veo_3p1_fast_text_to_video_webhook<R: IntoUrl>(
|
||||
args: EnqueueVeo3p1FastTextToVideoArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
let duration = args.duration
|
||||
.map(|resolution| match resolution {
|
||||
EnqueueVeo3p1FastTextToVideoDurationSeconds::Four => "4s",
|
||||
EnqueueVeo3p1FastTextToVideoDurationSeconds::Six => "6s",
|
||||
EnqueueVeo3p1FastTextToVideoDurationSeconds::Eight=> "8s",
|
||||
})
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let aspect_ratio = args.aspect_ratio
|
||||
.map(|aspect_ratio| match aspect_ratio {
|
||||
EnqueueVeo3p1FastTextToVideoAspectRatio::Auto => "auto",
|
||||
EnqueueVeo3p1FastTextToVideoAspectRatio::Square => "1:1",
|
||||
EnqueueVeo3p1FastTextToVideoAspectRatio::SixteenByNine => "16:9",
|
||||
EnqueueVeo3p1FastTextToVideoAspectRatio::NineBySixteen => "9:16",
|
||||
})
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let resolution = args.resolution
|
||||
.map(|resolution| match resolution {
|
||||
EnqueueVeo3p1FastTextToVideoResolution::SevenTwentyP => "720p",
|
||||
EnqueueVeo3p1FastTextToVideoResolution::TenEightyP => "1080p",
|
||||
})
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let request = Veo3p1FastTextToVideoInput {
|
||||
prompt: args.prompt,
|
||||
// Optionals
|
||||
duration,
|
||||
aspect_ratio,
|
||||
resolution,
|
||||
negative_prompt: args.negative_prompt,
|
||||
generate_audio: args.generate_audio,
|
||||
enhance_prompt: args.enhance_prompt,
|
||||
seed: args.seed,
|
||||
auto_fix: args.auto_fix,
|
||||
};
|
||||
|
||||
let result = veo_3p1_fast_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_3p1_fast_text_to_video_webhook::{enqueue_veo_3p1_fast_text_to_video_webhook, EnqueueVeo3p1FastTextToVideoArgs, EnqueueVeo3p1FastTextToVideoAspectRatio, EnqueueVeo3p1FastTextToVideoDurationSeconds, EnqueueVeo3p1FastTextToVideoResolution};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test() -> AnyhowResult<()> {
|
||||
// XXX: Don't commit secrets!
|
||||
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = EnqueueVeo3p1FastTextToVideoArgs {
|
||||
prompt: "An alien space ship hovers over new york city. it looks ominous, ready to attack. suddenly, it drops a bunch of ping pong balls on the city".to_string(),
|
||||
duration: Some(EnqueueVeo3p1FastTextToVideoDurationSeconds::Eight),
|
||||
aspect_ratio: Some(EnqueueVeo3p1FastTextToVideoAspectRatio::SixteenByNine),
|
||||
resolution: Some(EnqueueVeo3p1FastTextToVideoResolution::TenEightyP),
|
||||
generate_audio: Some(true),
|
||||
negative_prompt: None,
|
||||
enhance_prompt: Some(true),
|
||||
auto_fix: Some(true),
|
||||
seed: None,
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let result = enqueue_veo_3p1_fast_text_to_video_webhook(args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use fal::endpoints::fal_ai::veo::veo3_1::veo_3p1_text_to_video::{veo_3p1_text_to_video, Veo3p1TextToVideoInput};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct EnqueueVeo3p1TextToVideoArgs<'a, R: IntoUrl> {
|
||||
// Request required
|
||||
pub prompt: String,
|
||||
|
||||
// Optional args
|
||||
pub duration: Option<EnqueueVeo3p1TextToVideoDurationSeconds>,
|
||||
pub aspect_ratio: Option<EnqueueVeo3p1TextToVideoAspectRatio>,
|
||||
pub resolution: Option<EnqueueVeo3p1TextToVideoResolution>,
|
||||
pub generate_audio: Option<bool>,
|
||||
pub enhance_prompt: Option<bool>,
|
||||
pub negative_prompt: Option<String>,
|
||||
pub seed: Option<i64>,
|
||||
pub auto_fix: Option<bool>,
|
||||
|
||||
// Fulfillment
|
||||
pub webhook_url: R,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueVeo3p1TextToVideoDurationSeconds {
|
||||
Four,
|
||||
Six,
|
||||
Eight,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueVeo3p1TextToVideoAspectRatio {
|
||||
Auto,
|
||||
Square,
|
||||
SixteenByNine,
|
||||
NineBySixteen,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueVeo3p1TextToVideoResolution {
|
||||
SevenTwentyP,
|
||||
TenEightyP,
|
||||
}
|
||||
|
||||
pub async fn enqueue_veo_3p1_text_to_video_webhook<R: IntoUrl>(
|
||||
args: EnqueueVeo3p1TextToVideoArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
let duration = args.duration
|
||||
.map(|resolution| match resolution {
|
||||
EnqueueVeo3p1TextToVideoDurationSeconds::Four => "4s",
|
||||
EnqueueVeo3p1TextToVideoDurationSeconds::Six => "6s",
|
||||
EnqueueVeo3p1TextToVideoDurationSeconds::Eight=> "8s",
|
||||
})
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let aspect_ratio = args.aspect_ratio
|
||||
.map(|aspect_ratio| match aspect_ratio {
|
||||
EnqueueVeo3p1TextToVideoAspectRatio::Auto => "auto",
|
||||
EnqueueVeo3p1TextToVideoAspectRatio::Square => "1:1",
|
||||
EnqueueVeo3p1TextToVideoAspectRatio::SixteenByNine => "16:9",
|
||||
EnqueueVeo3p1TextToVideoAspectRatio::NineBySixteen => "9:16",
|
||||
})
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let resolution = args.resolution
|
||||
.map(|resolution| match resolution {
|
||||
EnqueueVeo3p1TextToVideoResolution::SevenTwentyP => "720p",
|
||||
EnqueueVeo3p1TextToVideoResolution::TenEightyP => "1080p",
|
||||
})
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let request = Veo3p1TextToVideoInput {
|
||||
prompt: args.prompt,
|
||||
// Optionals
|
||||
duration,
|
||||
aspect_ratio,
|
||||
resolution,
|
||||
negative_prompt: args.negative_prompt,
|
||||
generate_audio: args.generate_audio,
|
||||
enhance_prompt: args.enhance_prompt,
|
||||
seed: args.seed,
|
||||
auto_fix: args.auto_fix,
|
||||
};
|
||||
|
||||
let result = veo_3p1_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_3p1_text_to_video_webhook::{enqueue_veo_3p1_text_to_video_webhook, EnqueueVeo3p1TextToVideoArgs, EnqueueVeo3p1TextToVideoAspectRatio, EnqueueVeo3p1TextToVideoDurationSeconds, EnqueueVeo3p1TextToVideoResolution};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test() -> AnyhowResult<()> {
|
||||
// XXX: Don't commit secrets!
|
||||
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = EnqueueVeo3p1TextToVideoArgs {
|
||||
prompt: "An alien space ship hovers over new york city. it looks ominous, ready to attack. suddenly, it drops a bunch of ping pong balls on the city".to_string(),
|
||||
duration: Some(EnqueueVeo3p1TextToVideoDurationSeconds::Eight),
|
||||
aspect_ratio: Some(EnqueueVeo3p1TextToVideoAspectRatio::SixteenByNine),
|
||||
resolution: Some(EnqueueVeo3p1TextToVideoResolution::TenEightyP),
|
||||
generate_audio: Some(true),
|
||||
negative_prompt: None,
|
||||
enhance_prompt: Some(true),
|
||||
auto_fix: Some(true),
|
||||
seed: None,
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let result = enqueue_veo_3p1_text_to_video_webhook(args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
pub mod enqueue_hailuo_v2p3_pro_text_to_video_webhook;
|
||||
pub mod enqueue_kling_v2p5_turbo_pro_text_to_video_webhook;
|
||||
pub mod enqueue_kling_v2p6_pro_text_to_video_webhook;
|
||||
pub mod enqueue_pixverse_v5_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_3p1_fast_text_to_video_webhook;
|
||||
pub mod enqueue_veo_3p1_text_to_video_webhook;
|
||||
@@ -0,0 +1,52 @@
|
||||
[package]
|
||||
name = "grok_client"
|
||||
edition = "2024"
|
||||
version = "0.0.1"
|
||||
publish = false
|
||||
|
||||
[package.metadata.cargo-udeps.ignore]
|
||||
normal = ["workspace-build-acceleration"]
|
||||
|
||||
[lib]
|
||||
name = "grok_client"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
# Workspace hack (faster compile times)
|
||||
workspace-build-acceleration.workspace = true
|
||||
|
||||
# Internal
|
||||
cloudflare_errors.workspace = true
|
||||
cloudflare_mitigation.workspace = true
|
||||
cookie_store.workspace = true
|
||||
errors.workspace = true
|
||||
|
||||
# External
|
||||
anyhow.workspace = true
|
||||
base64.workspace = true
|
||||
bstr.workspace = true
|
||||
byteorder = "1.5.0"
|
||||
chrono.workspace = true
|
||||
futures.workspace = true
|
||||
log.workspace = true
|
||||
once_cell.workspace = true
|
||||
regex.workspace = true
|
||||
scraper = "0.24.0"
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sha2.workspace = true
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
url.workspace = true
|
||||
uuid.workspace = true
|
||||
wreq-util.workspace = true
|
||||
wreq.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
filesys.workspace = true
|
||||
test_data.workspace = true
|
||||
testing.workspace = true
|
||||
log4rs = "1.4.0"
|
||||
env_logger.workspace = true
|
||||
env.workspace = true
|
||||
chrono.workspace = true
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
|
||||
/// Firefox 139 (2025) on macOS, which matches wreq's Emulation::Firefox139
|
||||
pub const FIREFOX_139_MAC_USER_AGENT : &str =
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0";
|
||||
|
||||
pub const FIREFOX_143_MAC_USER_AGENT : &str =
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:143.0) Gecko/20100101 Firefox/143.0";
|
||||
|
||||
pub const FIREFOX_144_MAC_USER_AGENT_UNSUPPORTED_TLS_FINGERPRINTING : &str =
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:144.0) Gecko/20100101 Firefox/144.0";
|
||||
@@ -0,0 +1 @@
|
||||
pub mod browser_user_agents;
|
||||
+517
@@ -0,0 +1,517 @@
|
||||
#!/usr/bin/env python3
|
||||
# From https://github.com/kbtit25/grok2api/blob/73c917b15438e12699531ccf9411d3a7d1110cbf/xStatsigIDGenerator.py
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import base64
|
||||
import struct
|
||||
import hashlib
|
||||
import time
|
||||
import secrets
|
||||
import requests
|
||||
import re
|
||||
import json
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
class XStatsigIDGenerator:
|
||||
"""x-statsig-id 生成器"""
|
||||
|
||||
def __init__(self):
|
||||
self.base_timestamp = int(time.time()) # 使用当前系统时间
|
||||
self.grok_url = "https://grok.com"
|
||||
|
||||
def get_grok_meta_content(self) -> bytes:
|
||||
"""
|
||||
从 grok.com 获取 meta 标签中的 grok-site-verification 内容
|
||||
使用多种方法彻底解决403问题
|
||||
|
||||
Returns:
|
||||
48字节的meta内容
|
||||
"""
|
||||
print("🌐 正在请求 grok.com...")
|
||||
|
||||
# 定义多种绕过策略
|
||||
strategies = [
|
||||
#self._try_curl_with_proxy,
|
||||
self._try_curl_with_different_ua,
|
||||
#self._try_requests_with_session,
|
||||
#self._try_curl_cffi_advanced,
|
||||
#self._try_alternative_endpoints,
|
||||
#self._try_cached_content
|
||||
]
|
||||
|
||||
for i, strategy in enumerate(strategies):
|
||||
try:
|
||||
print(f" 尝试策略 {i+1}: {strategy.__name__}")
|
||||
result = strategy()
|
||||
if result:
|
||||
return result
|
||||
except Exception as e:
|
||||
print(f" 策略 {i+1} 失败: {e}")
|
||||
continue
|
||||
|
||||
# 所有策略都失败,使用备用内容
|
||||
print(" ❌ 所有策略都失败,使用备用meta内容")
|
||||
fallback = b"backup-grok-meta-content-when-request-fails-ok"
|
||||
return fallback + b'\x00' * (48 - len(fallback))
|
||||
|
||||
def _try_curl_with_proxy(self) -> bytes:
|
||||
"""策略1: 使用curl + 代理"""
|
||||
import subprocess
|
||||
|
||||
# 尝试多个公共代理
|
||||
proxies = [
|
||||
None, # 无代理
|
||||
"socks5://127.0.0.1:1080", # 本地代理
|
||||
"http://127.0.0.1:8080", # 本地HTTP代理
|
||||
]
|
||||
|
||||
for proxy in proxies:
|
||||
try:
|
||||
curl_command = [
|
||||
'curl', '-s', '-L', '--max-time', '15',
|
||||
'--user-agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'--header', 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
'--header', 'Accept-Language: en-US,en;q=0.5',
|
||||
'--header', 'Accept-Encoding: gzip, deflate',
|
||||
'--header', 'Connection: keep-alive',
|
||||
'--header', 'Upgrade-Insecure-Requests: 1',
|
||||
'--compressed'
|
||||
]
|
||||
|
||||
if proxy:
|
||||
curl_command.extend(['--proxy', proxy])
|
||||
|
||||
curl_command.append(self.grok_url)
|
||||
|
||||
result = subprocess.run(curl_command, capture_output=True, text=True, timeout=15)
|
||||
|
||||
if result.returncode == 0 and len(result.stdout) > 1000:
|
||||
print(f" ✅ curl成功 (代理: {proxy or '无'})")
|
||||
return self._extract_meta_from_html(result.stdout)
|
||||
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return None
|
||||
|
||||
def _try_curl_with_different_ua(self) -> bytes:
|
||||
"""策略2: 使用不同的User-Agent"""
|
||||
import subprocess
|
||||
|
||||
user_agents = [
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:143.0) Gecko/20100101 Firefox/143.0',
|
||||
#'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1',
|
||||
#'Mozilla/5.0 (Android 13; Mobile; rv:109.0) Gecko/109.0 Firefox/119.0',
|
||||
#'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
#'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15'
|
||||
]
|
||||
|
||||
for ua in user_agents:
|
||||
try:
|
||||
curl_command = [
|
||||
'curl-impersonate-ff', '-s', '-L', '--max-time', '10',
|
||||
'--user-agent', ua,
|
||||
'--header', 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
'--header', 'Accept-Language: en-US,en;q=0.9',
|
||||
'--header', 'Cache-Control: no-cache',
|
||||
'--header', 'Pragma: no-cache',
|
||||
'--compressed',
|
||||
self.grok_url
|
||||
]
|
||||
|
||||
result = subprocess.run(curl_command, capture_output=True, text=True, timeout=10)
|
||||
|
||||
if result.returncode == 0 and len(result.stdout) > 1000:
|
||||
print(f" ✅ 不同UA成功")
|
||||
return self._extract_meta_from_html(result.stdout)
|
||||
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return None
|
||||
|
||||
def _try_requests_with_session(self) -> bytes:
|
||||
"""策略3: 使用requests session模拟真实浏览器行为"""
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
from urllib3.util.retry import Retry
|
||||
|
||||
session = requests.Session()
|
||||
|
||||
# 配置重试策略
|
||||
retry_strategy = Retry(
|
||||
total=3,
|
||||
backoff_factor=1,
|
||||
status_forcelist=[429, 500, 502, 503, 504],
|
||||
)
|
||||
adapter = HTTPAdapter(max_retries=retry_strategy)
|
||||
session.mount("http://", adapter)
|
||||
session.mount("https://", adapter)
|
||||
|
||||
# 模拟真实浏览器行为
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
'Accept-Encoding': 'gzip, deflate, br',
|
||||
'DNT': '1',
|
||||
'Connection': 'keep-alive',
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
'Sec-Fetch-Dest': 'document',
|
||||
'Sec-Fetch-Mode': 'navigate',
|
||||
'Sec-Fetch-Site': 'none',
|
||||
'Sec-Fetch-User': '?1',
|
||||
'Cache-Control': 'max-age=0'
|
||||
}
|
||||
|
||||
try:
|
||||
# 先访问主页建立session
|
||||
session.get('https://x.com', headers=headers, timeout=5)
|
||||
|
||||
# 再访问grok
|
||||
response = session.get(self.grok_url, headers=headers, timeout=10)
|
||||
|
||||
if response.status_code == 200 and len(response.text) > 1000:
|
||||
print(f" ✅ session请求成功")
|
||||
return self._extract_meta_from_html(response.text)
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def _try_curl_cffi_advanced(self) -> bytes:
|
||||
"""策略4: 使用curl_cffi高级模拟"""
|
||||
try:
|
||||
from curl_cffi import requests as curl_requests
|
||||
|
||||
# 尝试不同的浏览器模拟
|
||||
impersonations = ['chrome120', 'chrome119', 'safari17', 'firefox119']
|
||||
|
||||
for imp in impersonations:
|
||||
try:
|
||||
response = curl_requests.get(
|
||||
self.grok_url,
|
||||
impersonate=imp,
|
||||
timeout=10,
|
||||
headers={
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
'Cache-Control': 'no-cache'
|
||||
}
|
||||
)
|
||||
|
||||
if response.status_code == 200 and len(response.text) > 1000:
|
||||
print(f" ✅ curl_cffi成功 ({imp})")
|
||||
return self._extract_meta_from_html(response.text)
|
||||
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def _try_alternative_endpoints(self) -> bytes:
|
||||
"""策略5: 尝试替代端点"""
|
||||
import subprocess
|
||||
|
||||
# 尝试不同的URL路径
|
||||
alternative_urls = [
|
||||
'https://grok.com/',
|
||||
'https://grok.com/login',
|
||||
'https://grok.com/home',
|
||||
'https://x.com/i/grok'
|
||||
]
|
||||
|
||||
for url in alternative_urls:
|
||||
try:
|
||||
curl_command = [
|
||||
'curl', '-s', '-L', '--max-time', '8',
|
||||
'--user-agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
'--header', 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
'--compressed',
|
||||
url
|
||||
]
|
||||
|
||||
result = subprocess.run(curl_command, capture_output=True, text=True, timeout=8)
|
||||
|
||||
if result.returncode == 0 and len(result.stdout) > 500:
|
||||
print(f" ✅ 替代端点成功: {url}")
|
||||
meta_result = self._extract_meta_from_html(result.stdout)
|
||||
if meta_result:
|
||||
return meta_result
|
||||
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return None
|
||||
|
||||
def _try_cached_content(self) -> bytes:
|
||||
"""策略6: 使用缓存或预设内容"""
|
||||
# 如果有真实的grok meta内容,可以在这里硬编码
|
||||
known_meta_contents = [
|
||||
"grok-site-verification-content-2024-production-v1",
|
||||
"x-grok-verification-meta-content-stable-release",
|
||||
"grok-meta-verification-string-for-api-access"
|
||||
]
|
||||
|
||||
for content in known_meta_contents:
|
||||
try:
|
||||
meta_bytes = content.encode('utf-8')
|
||||
if len(meta_bytes) < 48:
|
||||
meta_bytes = meta_bytes + b'\x00' * (48 - len(meta_bytes))
|
||||
elif len(meta_bytes) > 48:
|
||||
meta_bytes = meta_bytes[:48]
|
||||
|
||||
print(f" ✅ 使用预设内容: {content[:30]}...")
|
||||
return meta_bytes
|
||||
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return None
|
||||
|
||||
def _extract_meta_from_html(self, html_content: str) -> bytes:
|
||||
"""从HTML中提取meta内容"""
|
||||
patterns = [
|
||||
r'<meta\s+name=["\']grok-site-verification["\']\s+content=["\']([^"\']+)["\']',
|
||||
r'<meta\s+content=["\']([^"\']+)["\']\s+name=["\']grok-site-verification["\']',
|
||||
r'grok-site-verification["\']?\s*(?:content|value)\s*=\s*["\']([^"\']+)["\']',
|
||||
# 扩展模式,查找其他可能的meta标签
|
||||
r'<meta\s+name=["\']verification["\']\s+content=["\']([^"\']+)["\']',
|
||||
r'<meta\s+name=["\']site-verification["\']\s+content=["\']([^"\']+)["\']'
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, html_content, re.IGNORECASE)
|
||||
if match:
|
||||
verification_content = match.group(1)
|
||||
print(f" ✅ 找到meta内容: {verification_content[:50]}...")
|
||||
|
||||
# 转换为48字节
|
||||
meta_bytes = verification_content.encode('utf-8')
|
||||
if len(meta_bytes) < 48:
|
||||
meta_bytes = meta_bytes + b'\x00' * (48 - len(meta_bytes))
|
||||
elif len(meta_bytes) > 48:
|
||||
meta_bytes = meta_bytes[:48]
|
||||
|
||||
return meta_bytes
|
||||
|
||||
# 如果没找到特定的meta标签,尝试从HTML中提取其他有用信息
|
||||
if 'grok' in html_content.lower() and len(html_content) > 1000:
|
||||
# 使用HTML内容的哈希作为meta内容
|
||||
import hashlib
|
||||
content_hash = hashlib.sha256(html_content.encode()).hexdigest()[:48]
|
||||
meta_bytes = content_hash.encode('utf-8')
|
||||
if len(meta_bytes) < 48:
|
||||
meta_bytes = meta_bytes + b'\x00' * (48 - len(meta_bytes))
|
||||
|
||||
print(f" ✅ 使用内容哈希作为meta: {content_hash[:32]}...")
|
||||
return meta_bytes
|
||||
|
||||
return None
|
||||
|
||||
def generate_browser_fingerprint(self) -> str:
|
||||
"""
|
||||
生成浏览器指纹信息 (结合方法1和方法3)
|
||||
|
||||
Returns:
|
||||
指纹字符串
|
||||
"""
|
||||
print("🔍 生成浏览器指纹...")
|
||||
|
||||
# 模拟浏览器指纹信息
|
||||
fingerprint_data = {
|
||||
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:143.0) Gecko/20100101 Firefox/143.0",
|
||||
"language": "en",
|
||||
"languages": ["en", "en-US"],
|
||||
"platform": "MacIntel",
|
||||
"cookieEnabled": True,
|
||||
"doNotTrack": None,
|
||||
"screenWidth": 450,
|
||||
"screenHeight": 654,
|
||||
"screenColorDepth": 24,
|
||||
"screenPixelDepth": 24,
|
||||
"screenAvailWidth": 450,
|
||||
"screenAvailHeight": 654,
|
||||
"innerWidth": 450,
|
||||
"innerHeight": 654,
|
||||
"outerWidth": 1920,
|
||||
"outerHeight": 1055,
|
||||
"timezone": "America/New York",
|
||||
"timezoneOffset": -400,
|
||||
"hardwareConcurrency": 14,
|
||||
"deviceMemory": 16,
|
||||
"maxTouchPoints": 0
|
||||
}
|
||||
|
||||
# 方法3: 生成指纹哈希
|
||||
fingerprint_string = json.dumps(fingerprint_data, sort_keys=True, separators=(',', ':'))
|
||||
fingerprint_hash = hashlib.sha256(fingerprint_string.encode('utf-8')).hexdigest()
|
||||
|
||||
print(f" 指纹数据长度: {len(fingerprint_string)} 字符")
|
||||
print(f" 指纹哈希: {fingerprint_hash[:32]}...")
|
||||
|
||||
return fingerprint_hash
|
||||
|
||||
def generate_x_statsig_id(self, method: str = "GET", pathname: str = "/") -> str:
|
||||
"""
|
||||
生成完整的 x-statsig-id
|
||||
|
||||
Args:
|
||||
method: 请求方式 (GET/POST)
|
||||
pathname: 请求路径
|
||||
|
||||
Returns:
|
||||
生成的 x-statsig-id 字符串
|
||||
"""
|
||||
print("=" * 60)
|
||||
print("🚀 开始生成 x-statsig-id")
|
||||
print("=" * 60)
|
||||
|
||||
print(f"📋 生成参数:")
|
||||
print(f" Method: {method}")
|
||||
print(f" Pathname: {pathname}")
|
||||
|
||||
# 1. 获取 grok.com 的 meta content
|
||||
meta_content = self.get_grok_meta_content()
|
||||
|
||||
# 2. 生成浏览器指纹
|
||||
fingerprint = self.generate_browser_fingerprint()
|
||||
|
||||
# 3. 生成当前时间戳
|
||||
current_timestamp = int(time.time())
|
||||
relative_timestamp = current_timestamp - self.base_timestamp
|
||||
|
||||
print(f"⏰ 时间信息:")
|
||||
print(f" 当前时间戳: {current_timestamp}")
|
||||
print(f" 基准时间戳: {self.base_timestamp}")
|
||||
print(f" 相对时间戳: {relative_timestamp}")
|
||||
|
||||
# 4. 生成时间戳字节 (小端序)
|
||||
timestamp_bytes = struct.pack('<I', relative_timestamp)
|
||||
print(f" 时间戳字节: {timestamp_bytes.hex()}")
|
||||
|
||||
# 5. 生成SHA256
|
||||
sha_input = f"{method}!{pathname}!{relative_timestamp}{fingerprint}"
|
||||
sha256_hash = hashlib.sha256(sha_input.encode('utf-8')).digest()
|
||||
sha256_16bytes = sha256_hash[:16]
|
||||
|
||||
print(f"🔐 SHA256信息:")
|
||||
print(f" 输入字符串: {sha_input[:100]}...")
|
||||
print(f" SHA256前16字节: {sha256_16bytes.hex()}")
|
||||
|
||||
# 6. 固定值
|
||||
fixed_byte = b'\x03'
|
||||
|
||||
# 7. 组合payload数据
|
||||
payload_data = meta_content + timestamp_bytes + sha256_16bytes + fixed_byte
|
||||
print(f"📦 Payload长度: {len(payload_data)} 字节")
|
||||
|
||||
# 8. 生成异或key并加密
|
||||
xor_key = secrets.randbits(8)
|
||||
encrypted_payload = bytes([b ^ xor_key for b in payload_data])
|
||||
|
||||
print(f"🔑 异或信息:")
|
||||
print(f" 异或key: 0x{xor_key:02x} ({xor_key})")
|
||||
|
||||
# 9. 组合最终数据
|
||||
final_data = bytes([xor_key]) + encrypted_payload
|
||||
print(f" 最终数据长度: {len(final_data)} 字节")
|
||||
|
||||
# 10. Base64编码
|
||||
result = base64.b64encode(final_data).decode('utf-8')
|
||||
|
||||
print(f"✅ 生成结果:")
|
||||
print(f" x-statsig-id: {result}")
|
||||
print(f" 长度: {len(result)} 字符")
|
||||
|
||||
return result
|
||||
|
||||
def verify_generated_id(self, statsig_id: str) -> bool:
|
||||
"""
|
||||
验证生成的ID结构是否正确
|
||||
|
||||
Args:
|
||||
statsig_id: 要验证的ID
|
||||
|
||||
Returns:
|
||||
验证是否通过
|
||||
"""
|
||||
print("\n" + "=" * 60)
|
||||
print("🔍 验证生成的ID结构")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
# Base64解码
|
||||
decoded_bytes = base64.b64decode(statsig_id)
|
||||
print(f"✅ Base64解码成功,长度: {len(decoded_bytes)} 字节")
|
||||
|
||||
# 提取异或key
|
||||
xor_key = decoded_bytes[0]
|
||||
print(f"✅ 异或key: 0x{xor_key:02x} ({xor_key})")
|
||||
|
||||
# 异或解密
|
||||
decrypted = bytearray()
|
||||
for i in range(1, len(decoded_bytes)):
|
||||
decrypted.append(decoded_bytes[i] ^ xor_key)
|
||||
|
||||
print(f"✅ 解密后长度: {len(decrypted)} 字节")
|
||||
|
||||
# 验证数据结构
|
||||
expected_length = 48 + 4 + 16 + 1 # meta + timestamp + sha256 + fixed
|
||||
if len(decrypted) == expected_length:
|
||||
print(f"✅ 数据长度正确: {len(decrypted)}/{expected_length}")
|
||||
else:
|
||||
print(f"❌ 数据长度错误: {len(decrypted)}/{expected_length}")
|
||||
return False
|
||||
|
||||
# 检查固定值
|
||||
fixed_val = decrypted[-1]
|
||||
if fixed_val == 3:
|
||||
print(f"✅ 固定值正确: {fixed_val}")
|
||||
else:
|
||||
print(f"❌ 固定值错误: {fixed_val} (期望: 3)")
|
||||
return False
|
||||
|
||||
# 解析时间戳
|
||||
timestamp_bytes = decrypted[48:52]
|
||||
timestamp = struct.unpack('<I', timestamp_bytes)[0]
|
||||
actual_time = self.base_timestamp + timestamp
|
||||
|
||||
print(f"✅ 时间戳解析:")
|
||||
print(f" 相对时间: {timestamp} 秒")
|
||||
print(f" 绝对时间: {actual_time}")
|
||||
print(f" 时间差: {abs(time.time() - actual_time):.1f} 秒")
|
||||
|
||||
print("🎉 ID结构验证通过!")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 验证失败: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""主函数 - 演示完整流程"""
|
||||
generator = XStatsigIDGenerator()
|
||||
|
||||
# 生成 x-statsig-id
|
||||
method = "POST"
|
||||
pathname = "/rest/app-chat/conversations/new"
|
||||
|
||||
statsig_id = generator.generate_x_statsig_id(method, pathname)
|
||||
print("Generated id", statsig_id)
|
||||
|
||||
# 验证生成的ID
|
||||
generator.verify_generated_id(statsig_id)
|
||||
|
||||
print(f"\n" + "=" * 60)
|
||||
print("📋 使用说明")
|
||||
print("=" * 60)
|
||||
print("在HTTP请求中使用:")
|
||||
print(f"Headers: {{'x-statsig-id': '{statsig_id}'}}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# From https://github.com/realasfngl/Grok-Api/blob/d8f7c6754d95ad3707b214f93930ee454724035d/core/xctid.py
|
||||
|
||||
from math import floor, copysign, pi, cos, sin
|
||||
from base64 import b64decode, b64encode
|
||||
from re import findall, sub
|
||||
from typing import List, Dict
|
||||
from random import random
|
||||
from hashlib import sha256
|
||||
from struct import pack
|
||||
from time import time
|
||||
|
||||
|
||||
class Signature:
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _h(x: float, _param: float, c: float, e: bool):
|
||||
f = ((x * (c - _param)) / 255.0) + _param
|
||||
if e:
|
||||
return floor(f)
|
||||
rounded = round(float(f), 2)
|
||||
if rounded == 0.0:
|
||||
return 0.0
|
||||
return rounded
|
||||
|
||||
@staticmethod
|
||||
def cubicBezierEased(t: float, x1: float, y1: float, x2: float, y2: float) -> float:
|
||||
def bezier(u: float):
|
||||
omu = 1.0 - u
|
||||
b1 = 3.0 * omu * omu * u
|
||||
b2 = 3.0 * omu * u * u
|
||||
b3 = u * u * u
|
||||
x = b1 * x1 + b2 * x2 + b3
|
||||
y = b1 * y1 + b2 * y2 + b3
|
||||
return x, y
|
||||
|
||||
lo, hi = 0.0, 1.0
|
||||
for _ in range(80):
|
||||
mid = 0.5 * (lo + hi)
|
||||
if bezier(mid)[0] < t:
|
||||
lo = mid
|
||||
else:
|
||||
hi = mid
|
||||
u = 0.5 * (lo + hi)
|
||||
return bezier(u)[1]
|
||||
|
||||
@staticmethod
|
||||
def xa(svg: str) -> List[List[int]]:
|
||||
s = (svg)
|
||||
substr = s[9:]
|
||||
parts = substr.split("C")
|
||||
out = []
|
||||
for part in parts:
|
||||
cleaned = sub(r"[^\d]+", " ", part).strip()
|
||||
if cleaned == "":
|
||||
nums = [0]
|
||||
else:
|
||||
nums = [int(tok) for tok in cleaned.split() if tok != ""]
|
||||
out.append(nums)
|
||||
return out
|
||||
|
||||
@staticmethod
|
||||
def tohex(num: float) -> str:
|
||||
rounded = round(float(num), 2)
|
||||
if rounded == 0.0:
|
||||
return "0"
|
||||
sign = "-" if copysign(1.0, rounded) < 0 else ""
|
||||
absval = abs(rounded)
|
||||
intpart = int(floor(absval))
|
||||
frac = absval - intpart
|
||||
if frac == 0.0:
|
||||
return sign + format(intpart, "x")
|
||||
frac_digits = []
|
||||
f = frac
|
||||
for _ in range(20):
|
||||
f *= 16
|
||||
digit = int(floor(f + 1e-12))
|
||||
frac_digits.append(format(digit, "x"))
|
||||
f -= digit
|
||||
if abs(f) < 1e-12:
|
||||
break
|
||||
frac_str = "".join(frac_digits).rstrip("0")
|
||||
if frac_str == "":
|
||||
return sign + format(intpart, "x")
|
||||
return sign + format(intpart, "x") + "." + frac_str
|
||||
|
||||
@staticmethod
|
||||
def simulateStyle(values: List[int], c: int) -> Dict[str,str]:
|
||||
duration = 4096
|
||||
currentTime = round(c / 10.0) * 10
|
||||
t = currentTime / duration
|
||||
|
||||
cp = [Signature._h(v, -1 if (i % 2) else 0, 1, False) for i, v in enumerate(values[7:])]
|
||||
|
||||
easedY = Signature.cubicBezierEased(t, cp[0], cp[1], cp[2], cp[3])
|
||||
|
||||
start = [float(x) for x in values[0:3]]
|
||||
end = [float(x) for x in values[3:6]]
|
||||
r = round(start[0] + (end[0] - start[0]) * easedY)
|
||||
g = round(start[1] + (end[1] - start[1]) * easedY)
|
||||
b = round(start[2] + (end[2] - start[2]) * easedY)
|
||||
color = f"rgb({r}, {g}, {b})"
|
||||
|
||||
endAngle = Signature._h(values[6], 60, 360, True)
|
||||
angle = endAngle * easedY
|
||||
rad = angle * pi / 180.0
|
||||
|
||||
def is_effectively_zero(val: float) -> bool:
|
||||
return abs(val) < 1e-7
|
||||
|
||||
def is_effectively_integer(val: float) -> bool:
|
||||
return abs(val - round(val)) < 1e-7
|
||||
|
||||
cosv = cos(rad)
|
||||
sinv = sin(rad)
|
||||
|
||||
if is_effectively_zero(cosv):
|
||||
a = 0
|
||||
d = 0
|
||||
else:
|
||||
if is_effectively_integer(cosv):
|
||||
a = int(round(cosv))
|
||||
d = int(round(cosv))
|
||||
else:
|
||||
a = f"{cosv:.6f}"
|
||||
d = f"{cosv:.6f}"
|
||||
|
||||
if is_effectively_zero(sinv):
|
||||
bval = 0
|
||||
cval = 0
|
||||
else:
|
||||
if is_effectively_integer(sinv):
|
||||
bval = int(round(sinv))
|
||||
cval = int(round(-sinv))
|
||||
else:
|
||||
bval = f"{sinv:.7f}"
|
||||
cval = f"{(-sinv):.7f}"
|
||||
|
||||
transform = f"matrix({a}, {bval}, {cval}, {d}, 0, 0)"
|
||||
return {"color": color, "transform": transform}
|
||||
|
||||
@staticmethod
|
||||
def xs(x_bytes: bytes, svg: str, x_values: list) -> str:
|
||||
arr = list(x_bytes)
|
||||
idx = arr[x_values[0]] % 16
|
||||
c = ((arr[x_values[1]] % 16) * (arr[x_values[2]] % 16)) * (arr[x_values[3]] % 16)
|
||||
o = Signature.xa(svg)
|
||||
vals = o[idx]
|
||||
k = Signature.simulateStyle(vals, c)
|
||||
|
||||
concat = str(k["color"]) + str(k["transform"])
|
||||
matches = findall(r"[\d\.\-]+", concat)
|
||||
converted = []
|
||||
for m in matches:
|
||||
num = float(m)
|
||||
hexstr = Signature.tohex(num)
|
||||
converted.append(hexstr)
|
||||
joined = "".join(converted)
|
||||
cleaned = joined.replace(".", "").replace("-", "")
|
||||
return cleaned
|
||||
|
||||
@staticmethod
|
||||
def generate_sign(path: str, method: str, verification: str, svg: str, x_values: list, time_n: int = None, random_float: float = None) -> str:
|
||||
|
||||
n = int(time() - 1682924400) if not time_n else time_n
|
||||
t = pack('<I', n)
|
||||
r = b64decode(verification)
|
||||
o = Signature.xs(r, svg, x_values)
|
||||
|
||||
msg = "!".join([method, path, str(n)]) + "obfiowerehiring" + o
|
||||
digest = sha256(msg.encode('utf-8')).digest()[:16]
|
||||
|
||||
prefix_byte = int(floor(random() if not random_float else random_float * 256))
|
||||
assembled = bytes([prefix_byte]) + r + t + digest + bytes([3])
|
||||
|
||||
arr = bytearray(assembled)
|
||||
if len(arr) > 0:
|
||||
first = arr[0]
|
||||
for i in range(1, len(arr)):
|
||||
arr[i] = arr[i] ^ first
|
||||
|
||||
return b64encode(bytes(arr)).decode('ascii').replace('=', '')
|
||||
|
||||
|
||||
def main():
|
||||
sig = Signature.generate_sign('/rest/app-chat/conversations/new', 'POST', self.verification_token, self.svg_data, self.numbers)
|
||||
print('Signature', sig)
|
||||
pass
|
||||
|
||||
if __name__ == 'main':
|
||||
main()
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
https://grok.com/ --> https://accounts.x.ai/sign-in?redirect=grok-com
|
||||
|
||||
--> Success --> https://grok.com/
|
||||
|
||||
|
||||
https://grok.com/imagine
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
use crate::datatypes::api::baggage::Baggage;
|
||||
use crate::datatypes::api::sentry_trace::SentryTrace;
|
||||
use crate::datatypes::api::svg_path_data::SvgPathData;
|
||||
use crate::datatypes::api::user_email::UserEmail;
|
||||
use crate::datatypes::api::user_id::UserId;
|
||||
use crate::datatypes::api::verification_token::VerificationToken;
|
||||
use crate::datatypes::api::xsid_numbers::XsidNumbers;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GrokClientSecrets {
|
||||
/// Baggage is <meta> tag data from the index.html
|
||||
pub baggage: Baggage,
|
||||
|
||||
/// Baggage is <meta> tag data from the index.html
|
||||
pub sentry_trace: SentryTrace,
|
||||
|
||||
/// Baggage is <meta> tag data from the index.html
|
||||
pub verification_token: VerificationToken,
|
||||
|
||||
/// The SVG path data to use (chosen via verification_token)
|
||||
/// We selected one of four-ish possible SVG paths (of length >= 200) by
|
||||
/// the `verification token -> animation index` algo.
|
||||
pub svg_path_data: SvgPathData,
|
||||
|
||||
/// This doesn't come from the index.html
|
||||
/// These numbers are separately loaded from the xsid javascript
|
||||
pub numbers: XsidNumbers,
|
||||
|
||||
/// From index.html
|
||||
/// Not strictly needed to sign requests, but typically needed to generate URLs.
|
||||
pub user_id: UserId,
|
||||
|
||||
/// From index.html
|
||||
/// Not needed, but returned alongside other details.
|
||||
pub user_email: Option<UserEmail>,
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GrokCookies {
|
||||
/// The entire cookies header (for now)
|
||||
cookies: String,
|
||||
}
|
||||
|
||||
impl GrokCookies {
|
||||
pub fn new(cookies: String) -> Self {
|
||||
Self { cookies }
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.cookies
|
||||
}
|
||||
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
self.cookies.as_bytes()
|
||||
}
|
||||
|
||||
pub fn to_string(&self) -> String {
|
||||
self.cookies.clone()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
use crate::credentials::grok_client_secrets::GrokClientSecrets;
|
||||
use crate::credentials::grok_cookies::GrokCookies;
|
||||
use crate::datatypes::api::user_id::UserId;
|
||||
|
||||
/// Includes the cookies and all the magic other bits needed to call the API.
|
||||
#[derive(Clone)]
|
||||
pub struct GrokFullCredentials {
|
||||
/// Entire cookie payload
|
||||
pub cookies: GrokCookies,
|
||||
|
||||
/// Secrets from `index.html` and the xsid javascript.
|
||||
/// Required for video generation.
|
||||
pub client_secrets: GrokClientSecrets,
|
||||
}
|
||||
|
||||
impl GrokFullCredentials {
|
||||
pub fn from_cookies_and_client_secrets(
|
||||
cookies: GrokCookies,
|
||||
client_secrets: GrokClientSecrets
|
||||
) -> Self {
|
||||
Self {
|
||||
cookies,
|
||||
client_secrets,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_user_id_ref(&self) -> &UserId {
|
||||
&self.client_secrets.user_id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
use crate::datatypes::api::user_email::UserEmail;
|
||||
use crate::datatypes::api::user_id::UserId;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GrokUserData {
|
||||
/// From index.html
|
||||
/// Not strictly needed to sign requests, but typically needed to generate URLs.
|
||||
pub user_id: UserId,
|
||||
|
||||
/// From index.html
|
||||
/// Not needed, but returned alongside other details.
|
||||
pub user_email: Option<UserEmail>,
|
||||
}
|
||||
|
||||
impl GrokUserData {
|
||||
pub fn from_id_and_email(id: String, email: Option<String>) -> Self {
|
||||
Self {
|
||||
user_id: UserId(id),
|
||||
user_email: email.map(|email| UserEmail(email)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod grok_client_secrets;
|
||||
pub mod grok_cookies;
|
||||
pub mod grok_full_credentials;
|
||||
pub mod grok_user_data;
|
||||
@@ -0,0 +1,13 @@
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize, Clone, Copy, Debug)]
|
||||
pub enum AspectRatio {
|
||||
#[serde(rename = "2:3")]
|
||||
TallTwoByThree,
|
||||
|
||||
#[serde(rename = "3:2")]
|
||||
WideThreeByTwo,
|
||||
|
||||
#[serde(rename = "1:1")]
|
||||
Square,
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
/// "baggage" data from the <meta> tags
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Baggage(pub(crate) String);
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
/// Type for File IDs.
|
||||
/// File IDs are UUIds.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FileId(pub String);
|
||||
@@ -0,0 +1,12 @@
|
||||
pub mod aspect_ratio;
|
||||
pub mod baggage;
|
||||
pub mod file_id;
|
||||
pub mod video_generation_mode;
|
||||
pub mod post_id;
|
||||
pub mod request_id;
|
||||
pub mod sentry_trace;
|
||||
pub mod svg_path_data;
|
||||
pub mod user_email;
|
||||
pub mod user_id;
|
||||
pub mod verification_token;
|
||||
pub mod xsid_numbers;
|
||||
@@ -0,0 +1,12 @@
|
||||
|
||||
/// Type for Post IDs.
|
||||
/// Post IDs are UUIds.
|
||||
/// These are often shared with media/image uploads' `FileId`s, but seldom video generation `FileId`s
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PostId(pub String);
|
||||
|
||||
impl PostId {
|
||||
pub fn to_string(&self) -> String {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
|
||||
/// Request IDs.
|
||||
/// Type for image websocket "tasks"
|
||||
/// These are for image generation websockets.
|
||||
/// Request IDs are UUIds.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RequestId(pub String);
|
||||
|
||||
impl RequestId {
|
||||
pub fn to_string(&self) -> String {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use uuid::Uuid;
|
||||
|
||||
/// "sentry trace" data from the <meta> tags
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SentryTrace(pub (crate) String);
|
||||
|
||||
impl SentryTrace {
|
||||
|
||||
/// Generate the format of trace sent in the HTTP request headers
|
||||
pub fn to_http_request_header(&self) -> String {
|
||||
// Python reference:
|
||||
// f'{self.sentry_trace}-{str(uuid4()).replace("-", "")[:16]}-0',
|
||||
// Inner bit:
|
||||
// str(uuid4()).replace("-", "")[:16]
|
||||
let sentry_start = &self.0;
|
||||
let sentry_uuid = Uuid::new_v4().to_string();
|
||||
let sentry_inner = sentry_uuid.replace("-", "")[..16].to_string();
|
||||
|
||||
format!("{sentry_start}-{sentry_inner}-0")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::datatypes::api::sentry_trace::SentryTrace;
|
||||
|
||||
#[test]
|
||||
fn test_real_trace() {
|
||||
let short = SentryTrace("d6d7c55e4a489c0dabaed06e5c7b257b".to_string());
|
||||
let header = short.to_http_request_header();
|
||||
assert_eq!(header.len(), 51);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_trace() {
|
||||
let short = SentryTrace("".to_string());
|
||||
let header = short.to_http_request_header();
|
||||
assert_eq!(header.len(), 19); // header is invalid, we're just making sure Rust doesn't panic
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
/// Data on an SVG path, eg. <svg d={data} />
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SvgPathData(pub(crate) String);
|
||||
@@ -0,0 +1,9 @@
|
||||
/// Type for User Emails
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct UserEmail(pub String);
|
||||
|
||||
impl UserEmail {
|
||||
pub fn to_string(&self) -> String {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
|
||||
/// Type for User IDs.
|
||||
/// User IDs are UUIDs.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct UserId(pub String);
|
||||
|
||||
impl UserId {
|
||||
pub fn to_string(&self) -> String {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VerificationToken(pub(crate) String);
|
||||
@@ -0,0 +1,27 @@
|
||||
|
||||
/// The video generation mode selector.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum VideoGenerationMode {
|
||||
/// Supposedly the "NSFW" mode.
|
||||
Spicy,
|
||||
|
||||
Fun,
|
||||
|
||||
Normal,
|
||||
|
||||
/// This might be the only mode that allows for a custom text prompt
|
||||
Custom,
|
||||
}
|
||||
|
||||
impl VideoGenerationMode {
|
||||
|
||||
/// This is the value for `--mode` in video generation chats.
|
||||
pub fn as_api_mode_arg(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Spicy => "extremely-spicy-or-crazy",
|
||||
Self::Fun => "extremely-crazy",
|
||||
Self::Normal => "normal",
|
||||
Self::Custom => "custom",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
|
||||
/// Magic numbers pulled from Javascript
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct XsidNumbers {
|
||||
pub numbers: Vec<u32>,
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
use std::path::Path;
|
||||
|
||||
/// Descriptor of a file to be uploaded
|
||||
/// Includes necessary metadata
|
||||
pub enum FileUploadSpec<P: AsRef<Path>> {
|
||||
/// Path to the file
|
||||
Path(P),
|
||||
|
||||
/// Bytes of the file
|
||||
Bytes {
|
||||
/// Must be owned.
|
||||
bytes: Vec<u8>,
|
||||
/// Must be owned.
|
||||
filename: String,
|
||||
/// Mime type
|
||||
mimetype: String,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod api;
|
||||
pub mod file_upload_spec;
|
||||
@@ -0,0 +1 @@
|
||||
{"error":{"code":7,"message":"Request rejected by anti-bot rules.","details":[]}}
|
||||
@@ -0,0 +1,37 @@
|
||||
use crate::error::grok_error::GrokError;
|
||||
use crate::error::grok_generic_api_error::GrokGenericApiError;
|
||||
use crate::error::grok_specific_api_error::GrokSpecificApiError;
|
||||
use cloudflare_errors::filter_cloudflare_errors::filter_cloudflare_errors;
|
||||
use wreq::StatusCode;
|
||||
|
||||
/// We know the response is an error at this point.
|
||||
/// We're just turning it into the right error.
|
||||
pub fn categorize_grok_http_error(status_code: StatusCode, maybe_body: Option<&str>) -> GrokError {
|
||||
|
||||
if let Some(body) = maybe_body {
|
||||
if let Err(err) = filter_cloudflare_errors(status_code.as_u16(), body) {
|
||||
return GrokGenericApiError::CloudflareError(err).into();
|
||||
}
|
||||
|
||||
let body = body.to_lowercase();
|
||||
let anti_bot = body.contains("anti-bot") || body.contains("rejected");
|
||||
|
||||
if anti_bot {
|
||||
return GrokSpecificApiError::AutomationBlocked.into();
|
||||
}
|
||||
|
||||
match status_code {
|
||||
StatusCode::TOO_MANY_REQUESTS => {
|
||||
return GrokSpecificApiError::TooManyVideos.into();
|
||||
}
|
||||
_ => {},
|
||||
}
|
||||
|
||||
return GrokGenericApiError::UncategorizedBadResponseWithStatusAndBody {
|
||||
status_code,
|
||||
body: body.to_string(),
|
||||
}.into();
|
||||
}
|
||||
|
||||
GrokGenericApiError::UncategorizedBadResponseWithStatus(status_code).into()
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
use base64::DecodeError;
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum GrokClientError {
|
||||
|
||||
/// An error was encountered in building the Wreq client
|
||||
WreqClientError(wreq::Error),
|
||||
|
||||
/// Error serializing a message to send the websocket
|
||||
WebsocketRequestSerializationError(serde_json::Error),
|
||||
|
||||
/// Error locking the websocket for sending/receiving
|
||||
WebsocketLockError,
|
||||
|
||||
/// Error reading from a websocket.
|
||||
WebsocketReadError(wreq::Error),
|
||||
|
||||
/// Error sending to an open websocket.
|
||||
WebsocketSendError(wreq::Error),
|
||||
|
||||
/// Can't open a local file for uploading.
|
||||
CannotOpenLocalFileForUpload(std::io::Error),
|
||||
|
||||
/// Can't read a local file for uploading.
|
||||
CannotReadLocalFileForUpload(std::io::Error),
|
||||
|
||||
/// Couldn't write to the file
|
||||
CannotOpenLocalFileForWriting(std::io::Error),
|
||||
|
||||
/// Couldn't write to the file
|
||||
CannotWriteLocalFile(std::io::Error),
|
||||
|
||||
/// File for upload has an invalid path.
|
||||
FileForUploadHasInvalidPath,
|
||||
|
||||
/// Error parsing HTML
|
||||
HtmlParsingError,
|
||||
|
||||
/// Error decoding verification token
|
||||
FailedToDecodeVerificationToken(DecodeError),
|
||||
|
||||
/// Verification token bytes are invalid
|
||||
InvalidVerificationTokenBytes,
|
||||
|
||||
/// Our script logic is out of date
|
||||
ScriptLogicOutOfDate,
|
||||
|
||||
/// Our script logic is out of date (SVG)
|
||||
ScriptSvgLogicOutOfDate,
|
||||
|
||||
/// Our script logic is out of date (script 1)
|
||||
Script1LogicOutOfDate,
|
||||
|
||||
/// Our script logic is out of date (script 2)
|
||||
Script2LogicOutOfDate,
|
||||
|
||||
/// Our script logic is out of date (xsid script)
|
||||
ScriptXsidLogicOutOfDate,
|
||||
|
||||
/// Signature algorithm isn't working with inputs
|
||||
BadSignatureInputs,
|
||||
|
||||
/// Something is broken with timeout math
|
||||
TimeoutMathBroken,
|
||||
|
||||
/// Can't make request because cookies aren't present
|
||||
NoCookiesPresent,
|
||||
|
||||
/// Unknown error generating video
|
||||
ErrorGeneratingVideo,
|
||||
|
||||
// /// An error reading the file for upload.
|
||||
// FileForUploadReadError(std::io::Error),
|
||||
//
|
||||
// /// The file path provided for upload is invalid.
|
||||
// FileForUploadHasInvalidPath,
|
||||
//
|
||||
// /// Something is wrong with the JWT bearer token.
|
||||
// /// This error originates on our end as we try to parse the JWT.
|
||||
// LocalJwtClaimsParseError(String),
|
||||
//
|
||||
// /// There was an error constructing the form-multipart request.
|
||||
// MultipartFormError(wreq::Error),
|
||||
//
|
||||
// /// We haven't received a bearer token yet
|
||||
// /// This is our own internal application state error, not something Sora returns.
|
||||
// /// We know our client can't make the request, so we preemptively fail it.
|
||||
// NoBearerTokenForRequest,
|
||||
//
|
||||
// /// A sentinel token is not present in the client, which is required for some requests.
|
||||
// NoSentinelTokenForRequest,
|
||||
//
|
||||
// /// Issue with using the SoraCredentialBuilder.
|
||||
// SoraCredentialBuilderError(&'static str),
|
||||
//
|
||||
// /// Error parsing a request URL.
|
||||
// UrlParseError(url::ParseError),
|
||||
//
|
||||
// /// Error serializing the sentinel store token to JSON (typically for persistent storage).
|
||||
// CouldNotSerializeSentinelTokenStore(serde_json::Error),
|
||||
//
|
||||
// /// Error deserializing the sentinel token from JSON (typically for persistent storage).
|
||||
// CouldNotDeserializeSentinelTokenStore {
|
||||
// error: serde_json::Error,
|
||||
// raw_json: String
|
||||
// },
|
||||
}
|
||||
|
||||
impl Error for GrokClientError {}
|
||||
|
||||
impl Display for GrokClientError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::WreqClientError(err) => write!(f, "Wreq client error (during client creation): {}", err),
|
||||
Self::WebsocketRequestSerializationError(err) => write!(f, "Websocket request serialization error: {}", err),
|
||||
Self::WebsocketLockError => write!(f, "Websocket lock error"),
|
||||
Self::WebsocketReadError(err) => write!(f, "Websocket read error: {}", err),
|
||||
Self::WebsocketSendError(err) => write!(f, "Websocket send error: {}", err),
|
||||
Self::CannotOpenLocalFileForUpload(err) => write!(f, "Cannot open local file for upload: {}", err),
|
||||
Self::CannotReadLocalFileForUpload(err) => write!(f, "Cannot read local file for upload: {}", err),
|
||||
Self::CannotOpenLocalFileForWriting(err) => write!(f, "Cannot open local file for writing: {}", err),
|
||||
Self::CannotWriteLocalFile(err) => write!(f, "Cannot write local file: {}", err),
|
||||
Self::FileForUploadHasInvalidPath => write!(f, "File for upload has invalid path"),
|
||||
Self::HtmlParsingError => write!(f, "Html parsing error"),
|
||||
Self::FailedToDecodeVerificationToken(err) => write!(f, "Failed to decode verification token: {}", err),
|
||||
Self::InvalidVerificationTokenBytes => write!(f, "Invalid verification token bytes"),
|
||||
Self::ScriptLogicOutOfDate => write!(f, "Script logic out of date"),
|
||||
Self::ScriptSvgLogicOutOfDate => write!(f, "Script logic out of date (SVG parsing)"),
|
||||
Self::Script1LogicOutOfDate => write!(f, "Script logic out of date (script 1)"),
|
||||
Self::Script2LogicOutOfDate => write!(f, "Script logic out of date (script 2)"),
|
||||
Self::ScriptXsidLogicOutOfDate => write!(f, "Script logic out of date (xsid script)"),
|
||||
Self::BadSignatureInputs => write!(f, "Bad signature inputs"),
|
||||
Self::TimeoutMathBroken => write!(f, "Timeout math is broken"),
|
||||
Self::NoCookiesPresent => write!(f, "No cookies present"),
|
||||
Self::ErrorGeneratingVideo => write!(f, "Error generating video"),
|
||||
|
||||
//Self::FileForUploadReadError(err) => write!(f, "Error reading file for upload: {}", err),
|
||||
//Self::FileForUploadHasInvalidPath => write!(f, "The file path provided for upload is invalid."),
|
||||
//Self::LocalJwtClaimsParseError(msg) => write!(f, "Local JWT claims parse error: {}", msg),
|
||||
//Self::MultipartFormError(err) => write!(f, "Multipart form error: {}", err),
|
||||
//Self::NoBearerTokenForRequest => write!(f, "No bearer token available. The client needs a bearer token to make the request."),
|
||||
//Self::NoSentinelTokenForRequest => write!(f, "No sentinel token available. The client needs a sentinel token to make the request."),
|
||||
//Self::SoraCredentialBuilderError(msg) => write!(f, "Sora Credential Builder error: {}", msg),
|
||||
//Self::UrlParseError(err) => write!(f, "URL parse error: {}", err),
|
||||
//Self::CouldNotSerializeSentinelToken(err) => write!(f, "Could not serialize sentinel token to JSON: {}", err),
|
||||
//Self::CouldNotSerializeSentinelTokenStore(err) => write!(f, "Could not serialize sentinel token store to JSON: {}", err),
|
||||
//Self::CouldNotDeserializeSentinelTokenStore { error, raw_json} => {
|
||||
// write!(f, "Could not deserialize sentinel token store from JSON: {:?}, raw JSON: {}", error, raw_json)
|
||||
//},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
use crate::error::grok_client_error::GrokClientError;
|
||||
use crate::error::grok_generic_api_error::GrokGenericApiError;
|
||||
use crate::error::grok_specific_api_error::GrokSpecificApiError;
|
||||
use cloudflare_errors::cloudflare_error::CloudflareError;
|
||||
use std::error::Error;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum GrokError {
|
||||
Client(GrokClientError),
|
||||
ApiSpecific(GrokSpecificApiError),
|
||||
ApiGeneric(GrokGenericApiError),
|
||||
}
|
||||
|
||||
impl GrokError {
|
||||
pub fn is_grok_having_downtime_issues(&self) -> bool {
|
||||
match self {
|
||||
Self::ApiGeneric(GrokGenericApiError::CloudflareError(CloudflareError::BadGateway502)) => true,
|
||||
Self::ApiGeneric(GrokGenericApiError::CloudflareError(CloudflareError::GatewayTimeout504)) => true,
|
||||
Self::ApiGeneric(GrokGenericApiError::CloudflareError(CloudflareError::TimeoutOccurred524)) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for GrokError {}
|
||||
|
||||
impl std::fmt::Display for GrokError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Client(e) => write!(f, "GrokClientError: {:?}", e),
|
||||
Self::ApiSpecific(e) => write!(f, "GrokSpecificApiError: {:?}", e),
|
||||
Self::ApiGeneric(e) => write!(f, "GrokGenericApiError: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GrokClientError> for GrokError {
|
||||
fn from(error: GrokClientError) -> Self {
|
||||
Self::Client(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GrokSpecificApiError> for GrokError {
|
||||
fn from(error: GrokSpecificApiError) -> Self {
|
||||
Self::ApiSpecific(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GrokGenericApiError> for GrokError {
|
||||
fn from(error: GrokGenericApiError) -> Self {
|
||||
Self::ApiGeneric(error)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
use cloudflare_errors::cloudflare_error::CloudflareError;
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use wreq::StatusCode;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum GrokGenericApiError {
|
||||
/// Specific Cloudflare errors.
|
||||
CloudflareError(CloudflareError),
|
||||
|
||||
/// The downloaded index.html did not include the expected data.
|
||||
/// This is necessary for handshakes between client and server.
|
||||
IndexHtmlDidNotIncludeExpectedData {
|
||||
message: String,
|
||||
},
|
||||
|
||||
/// The downloaded javascript did not include the expected data.
|
||||
/// This is necessary for handshakes between client and server.
|
||||
ScriptBodyDidNotIncludeExpectedData {
|
||||
message: String,
|
||||
},
|
||||
|
||||
/// Error creating the websocket due to 401 Unauthorized, likely an authentication error with Grok.
|
||||
/// Unfortunately we can't read the response body text to diagnose more because wreq has some API design issues.
|
||||
LikelyWebsocketAuthentication401,
|
||||
|
||||
/// We received a 403 on websocket upgrade, likely from Cloudflare.
|
||||
/// Unfortunately we can't read the response body text to diagnose more because wreq has some API design issues.
|
||||
LikelyWebsocketCloudflare403,
|
||||
|
||||
/// Unknown status code on websocket upgrade.
|
||||
UnexpectedWebsocketUpgradeStatusCode(StatusCode),
|
||||
|
||||
/// serde_json::Error, likely from JSON deserialization schema mismatch.
|
||||
/// Includes the original body.
|
||||
SerdeResponseParseErrorWithBody(serde_json::Error, String),
|
||||
|
||||
/// serde_json::Error, likely from JSON deserialization schema mismatch.
|
||||
/// Includes the original body.
|
||||
/// Specifically, this is for non-200 (error) responses.
|
||||
SerdeParseErrorWithBodyOnNon200(serde_json::Error, String),
|
||||
|
||||
/// An uncategorized bad HTTP response from Grok.
|
||||
UncategorizedBadResponseWithStatus(StatusCode),
|
||||
|
||||
/// An uncategorized bad HTTP response from Grok.
|
||||
UncategorizedBadResponseWithStatusAndBody {
|
||||
status_code: StatusCode,
|
||||
body: String,
|
||||
},
|
||||
|
||||
/// Our previous upload failed
|
||||
UploadFailed,
|
||||
|
||||
/// An error upgrading the connection to a websocket.
|
||||
WreqWebsocketUpgradeError(wreq::Error),
|
||||
|
||||
/// An uncaught error from the API client.
|
||||
WreqError(wreq::Error),
|
||||
}
|
||||
|
||||
impl Error for GrokGenericApiError {}
|
||||
|
||||
impl Display for GrokGenericApiError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::CloudflareError(err) => write!(f, "Cloudflare error: {}", err),
|
||||
Self::IndexHtmlDidNotIncludeExpectedData { message } => write!(f, "IndexHtmlDidNotIncludeExpectedData ( message: {} )", message),
|
||||
Self::ScriptBodyDidNotIncludeExpectedData { message } => write!(f, "ScriptBodyDidNotIncludeExpectedData ( message: {} )", message),
|
||||
Self::LikelyWebsocketAuthentication401 => write!(f, "Likely websocket authentication 401 error (probably Grok authentication issue with cookies)"),
|
||||
Self::LikelyWebsocketCloudflare403 => write!(f, "Likely Cloudflare 403 error on websocket upgrade"),
|
||||
Self::UnexpectedWebsocketUpgradeStatusCode(status) => write!(f, "Unexpected websocket upgrade status code: {}", status),
|
||||
Self::SerdeResponseParseErrorWithBody(err, body) => write!(f, "Failed to parse response body: {:?}. Body: {}", err, body),
|
||||
Self::SerdeParseErrorWithBodyOnNon200(err, body) => write!(f, "Failed to parse non-200 response body: {:?}. Body: {}", err, body),
|
||||
Self::UncategorizedBadResponseWithStatus(status) => write!(f, "Uncategorized with status code: {}", status),
|
||||
Self::UncategorizedBadResponseWithStatusAndBody { status_code, body } => write!(f, "Uncategorized bad response: status code {}, body: {}", status_code, body),
|
||||
Self::UploadFailed => write!(f, "Upload failed"),
|
||||
Self::WreqWebsocketUpgradeError(err) => write!(f, "Websocket upgrade error: {}", err),
|
||||
Self::WreqError(err) => write!(f, "Wreq client error: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum GrokSpecificApiError {
|
||||
/// Grok is detecting and blocking our bot, likely due to new secrets use.
|
||||
AutomationBlocked,
|
||||
|
||||
/// Too many videos are being generated.
|
||||
TooManyVideos,
|
||||
|
||||
|
||||
// /// Failed to generate a bearer token despite the `200 OK` response.
|
||||
// /// This has been generated by sending old/expired session cookies or empty cookies.
|
||||
// FailedToGenerateBearerToken,
|
||||
//
|
||||
// /// The endpoint told us we used an invalid JWT.
|
||||
// InvalidJwt,
|
||||
//
|
||||
// /// Sora has responded with "sentinel block".
|
||||
// /// This usually means we need to refresh our sentinel token.
|
||||
// SentinelBlockError,
|
||||
//
|
||||
// /// Sora requires users to set up an account username before they can generate images.
|
||||
// /// This requires the user to set up that username.
|
||||
// SoraUsernameNotYetCreated,
|
||||
//
|
||||
// /// Sora has responded with "token expired".
|
||||
// /// This (probably?) means we need to refresh our JWT bearer token.
|
||||
// TokenExpiredError,
|
||||
//
|
||||
// /// We're sending too many tasks to Sora.
|
||||
// /// Sora is telling us to cool it.
|
||||
// TooManyConcurrentTasks,
|
||||
//
|
||||
// // TODO: Try to make concrete error types for these distinct cases.
|
||||
// /// Unauthorized, cookie and/or bearer token expired. We'll need to ask for a refreshed login.
|
||||
// ///
|
||||
// /// Example message, e.g. from the upload endpoint:
|
||||
// /// {
|
||||
// /// "error": {
|
||||
// /// "message": "Your authentication token has expired. Please try signing in again.",
|
||||
// /// "type": "invalid_request_error",
|
||||
// /// "param": null,
|
||||
// /// "code": "token_expired"
|
||||
// /// }
|
||||
// /// }
|
||||
// UnauthorizedCookieOrBearerExpired,
|
||||
//
|
||||
// /// The Sora sentinel token response is missing fields
|
||||
// SentinelResponseIsMissingFields {
|
||||
// missing_token: bool,
|
||||
// missing_turnstile: bool,
|
||||
// raw_response: String,
|
||||
// },
|
||||
}
|
||||
|
||||
impl Error for GrokSpecificApiError {}
|
||||
|
||||
impl Display for GrokSpecificApiError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::AutomationBlocked => write!(f, "Automation blocked"),
|
||||
Self::TooManyVideos => write!(f, "Too many videos are being generated"),
|
||||
|
||||
//Self::FailedToGenerateBearerToken => write!(f, "Failed to generate bearer token. Probably invalid or expired session cookies."),
|
||||
//Self::InvalidJwt => write!(f, "Invalid JWT token. Please refresh the token."),
|
||||
//Self::SentinelBlockError => write!(f, "Request blocked by Sentinel Token Block. Please refresh the sentinel token."),
|
||||
//Self::SoraUsernameNotYetCreated => write!(f, "Sora username not yet created. Please create a username in the Sora app or website."),
|
||||
//Self::TokenExpiredError => write!(f, "JWT token has expired. Please refresh the token."),
|
||||
//Self::TooManyConcurrentTasks => write!(f, "Too many concurrent tasks. Please wait."),
|
||||
//Self::UnauthorizedCookieOrBearerExpired => write!(f, "Unauthorized: cookie and/or bearer token expired"),
|
||||
//Self::SentinelResponseIsMissingFields { missing_token: token_missing, missing_turnstile: turnstile_missing, raw_response } => {
|
||||
// write!(f, "Sentinel response is missing fields. token_missing: {}, turnstile_missing: {}. Raw response: {}", token_missing, turnstile_missing, raw_response)
|
||||
//},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod grok_client_error;
|
||||
pub mod grok_error;
|
||||
pub mod grok_generic_api_error;
|
||||
pub mod grok_specific_api_error;
|
||||
pub (crate) mod categorize_grok_http_error;
|
||||
@@ -0,0 +1,15 @@
|
||||
//! A Grok API client.
|
||||
|
||||
#[cfg(test)]
|
||||
pub (crate) mod test_utils;
|
||||
|
||||
// Library utils
|
||||
pub (crate) mod client;
|
||||
|
||||
// User lib
|
||||
pub mod credentials;
|
||||
pub mod datatypes;
|
||||
pub mod error;
|
||||
pub mod recipes;
|
||||
pub mod requests;
|
||||
pub mod utils;
|
||||
@@ -0,0 +1,93 @@
|
||||
Upload Image -> Video Flow
|
||||
==========================
|
||||
|
||||
1. Upload Image
|
||||
|
||||
Endpoint: https://grok.com/rest/app-chat/upload-file
|
||||
|
||||
Response Fields:
|
||||
{
|
||||
**"fileMetadataId": "21a79085-e206-4b0b-88ac-5f2b7a453e45",**
|
||||
**"fileUri": "users/85980643-ffab-4984-a3de-59a608c47d7f/21a79085-e206-4b0b-88ac-5f2b7a453e45/content",**
|
||||
"fileMimeType": "image/jpeg",
|
||||
"fileName": "0_0.jpeg",
|
||||
"parsedFileUri": "",
|
||||
"createTime": "2025-10-21T23:40:41.784448Z",
|
||||
"fileSource": "SELF_UPLOAD_FILE_SOURCE"
|
||||
}
|
||||
|
||||
|
||||
File becomes a 480x640 copy:
|
||||
|
||||
https://assets.grok.com/users/85980643-ffab-4984-a3de-59a608c47d7f/21a79085-e206-4b0b-88ac-5f2b7a453e45/content
|
||||
|
||||
2. "Create Media Post" **(needed?)**
|
||||
|
||||
Endpoint: https://grok.com/rest/media/post/create
|
||||
|
||||
Request Fields:
|
||||
{
|
||||
"mediaType": "MEDIA_POST_TYPE_IMAGE",
|
||||
"mediaUrl": "https://assets.grok.com/users/85980643-ffab-4984-a3de-59a608c47d7f/21a79085-e206-4b0b-88ac-5f2b7a453e45/content"
|
||||
}
|
||||
|
||||
Response:
|
||||
{
|
||||
"post": {
|
||||
"id": "21a79085-e206-4b0b-88ac-5f2b7a453e45",
|
||||
"userId": "85980643-ffab-4984-a3de-59a608c47d7f",
|
||||
"createTime": "2025-10-21T23:40:41.993980477Z",
|
||||
"prompt": "",
|
||||
"mediaType": "MEDIA_POST_TYPE_IMAGE",
|
||||
"mediaUrl": "https://assets.grok.com/users/85980643-ffab-4984-a3de-59a608c47d7f/21a79085-e206-4b0b-88ac-5f2b7a453e45/content",
|
||||
"mimeType": "image/jpeg",
|
||||
"audioUrls": [],
|
||||
"childPosts": []
|
||||
}
|
||||
}
|
||||
|
||||
3. Chat Conversation for Video
|
||||
|
||||
Endpoint: https://grok.com/rest/app-chat/conversations/new
|
||||
|
||||
Request:
|
||||
{
|
||||
"temporary":true,
|
||||
"modelName":"grok-3",
|
||||
"message":"https://assets.grok.com/users/85980643-ffab-4984-a3de-59a608c47d7f/21a79085-e206-4b0b-88ac-5f2b7a453e45/content --mode=normal",
|
||||
"fileAttachments":["21a79085-e206-4b0b-88ac-5f2b7a453e45"],
|
||||
"toolOverrides": {
|
||||
"videoGen":true
|
||||
}
|
||||
}
|
||||
|
||||
Header 'x-xai-request-id: 7cb93340-5fb6-4fb3-b88d-ff9f5d99d456' is just a random UUID and it can be changed.
|
||||
|
||||
|
||||
Generate Image -> Video Flow
|
||||
============================
|
||||
|
||||
(TODO)
|
||||
|
||||
2. Create Media Post
|
||||
|
||||
Request
|
||||
{
|
||||
"mediaType": "MEDIA_POST_TYPE_IMAGE",
|
||||
"mediaUrl":"https://imagine-public.x.ai/imagine-public/images/2d8260d7-3b7c-4bc1-84ca-6e82dd892c39.png"
|
||||
}
|
||||
|
||||
|
||||
3. Chat Conversation for Video
|
||||
|
||||
Endpoint: https://grok.com/rest/app-chat/conversations/new
|
||||
|
||||
Request:
|
||||
{
|
||||
"temporary": true,
|
||||
"modelName": "grok-3",
|
||||
"message": "https://imagine-public.x.ai/imagine-public/images/2d8260d7-3b7c-4bc1-84ca-6e82dd892c39.png alien walks down the hallway --mode=custom",
|
||||
"toolOverrides": {
|
||||
"videoGen":true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod prompt_websocket_image_with_retry;
|
||||
pub mod request_client_secrets;
|
||||
pub mod upload_image_and_generate_video;
|
||||
pub mod upload_image_and_generate_video_with_retry;
|
||||
@@ -0,0 +1,201 @@
|
||||
use crate::credentials::grok_cookies::GrokCookies;
|
||||
use crate::error::grok_client_error::GrokClientError;
|
||||
use crate::error::grok_error::GrokError;
|
||||
use crate::requests::image_websocket::create_listen_websocket::{create_listen_websocket, CreateListenWebsocketArgs};
|
||||
use crate::requests::image_websocket::grok_websocket::GrokWebsocket;
|
||||
use crate::requests::image_websocket::messages::websocket_client_message::{ClientMessageAspectRatio, WebsocketClientMessage};
|
||||
use log::{error, info};
|
||||
|
||||
pub struct PromptWebsocketImageWithRetryArgs<'a> {
|
||||
pub websocket: &'a mut GrokWebsocket,
|
||||
pub prompt: &'a str,
|
||||
pub aspect_ratio: ClientMessageAspectRatio,
|
||||
pub cookies: &'a GrokCookies,
|
||||
}
|
||||
|
||||
pub struct PromptWebsocketImageWithRetryResult {
|
||||
pub maybe_new_websocket: Option<GrokWebsocket>,
|
||||
}
|
||||
|
||||
|
||||
pub async fn prompt_websocket_image_with_retry(args: PromptWebsocketImageWithRetryArgs<'_>) -> Result<PromptWebsocketImageWithRetryResult, GrokError>
|
||||
{
|
||||
let mut maybe_new_websocket : Option<GrokWebsocket> = None;
|
||||
|
||||
for i in 0..3 {
|
||||
info!("Starting Grok Websocket image generation (attempt {}) ...", (i+1));
|
||||
|
||||
let message = WebsocketClientMessage::new_image_prompt(args.prompt, args.aspect_ratio);
|
||||
|
||||
let result =
|
||||
if let Some(websocket) = maybe_new_websocket.as_mut() {
|
||||
websocket.send_serializable(&message).await
|
||||
} else {
|
||||
args.websocket.send_serializable(&message).await
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(()) => {
|
||||
break;
|
||||
}
|
||||
Err(GrokError::Client(GrokClientError::WreqClientError(err))) => {
|
||||
error!("Error sending message to websocket (WreqClientError): {}", err);
|
||||
}
|
||||
Err(GrokError::Client(err)) => {
|
||||
error!("Error sending message to websocket (GrokClientError): {}", err);
|
||||
}
|
||||
Err(GrokError::ApiSpecific(err)) => {
|
||||
error!("Error sending message to websocket (GrokSpecificApiError): {}", err);
|
||||
}
|
||||
Err(GrokError::ApiGeneric(err)) => {
|
||||
error!("Error sending message to websocket (GrokGenericApiError): {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
let result = create_listen_websocket(CreateListenWebsocketArgs {
|
||||
cookies: args.cookies.as_str(),
|
||||
}).await?;
|
||||
|
||||
maybe_new_websocket = Some(GrokWebsocket::new(result));
|
||||
}
|
||||
|
||||
Ok(PromptWebsocketImageWithRetryResult {
|
||||
maybe_new_websocket,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::recipes::prompt_websocket_image_with_retry::{prompt_websocket_image_with_retry, PromptWebsocketImageWithRetryArgs};
|
||||
use crate::requests::image_websocket::create_listen_websocket::{create_listen_websocket, CreateListenWebsocketArgs};
|
||||
use crate::requests::image_websocket::grok_websocket::GrokWebsocket;
|
||||
use crate::requests::image_websocket::grok_wrapped_websocket::GrokWrappedWebsocket;
|
||||
use crate::requests::image_websocket::messages::websocket_client_message::ClientMessageAspectRatio;
|
||||
use crate::requests::image_websocket::messages::websocket_server_message::WebsocketServerMessage;
|
||||
use crate::requests::image_websocket::prompt_websocket_image::{prompt_websocket_image, PromptWebsocketImageArgs};
|
||||
use crate::test_utils::get_test_cookies::{get_test_cookies, get_typed_test_cookies};
|
||||
use errors::AnyhowResult;
|
||||
use futures::SinkExt;
|
||||
use log::warn;
|
||||
use std::io::Write;
|
||||
use std::time::Duration;
|
||||
use wreq::ws::message::{CloseCode, Message, Utf8Bytes};
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test
|
||||
async fn prompt() -> AnyhowResult<()> {
|
||||
//setup_test_logging(LevelFilter::Trace);
|
||||
|
||||
let cookies = get_typed_test_cookies()?;
|
||||
|
||||
let websocket = create_listen_websocket(CreateListenWebsocketArgs {
|
||||
cookies: cookies.as_str(),
|
||||
}).await?;
|
||||
|
||||
let prompt = "A dinosaur on stilts walking on the beach";
|
||||
|
||||
//let websocket = GrokWrappedWebsocket::new(websocket);
|
||||
let mut websocket = GrokWebsocket::new(websocket);
|
||||
|
||||
//println!("Sending...");
|
||||
//std::io::stdout().flush()?;
|
||||
|
||||
//println!("For our test, we'll close the websocket and retry...");
|
||||
//websocket.websocket
|
||||
// .close(CloseCode::NORMAL, Utf8Bytes::try_from(Vec::new())?).await?;
|
||||
|
||||
//let mut websocket = GrokWebsocket::new(websocket.websocket);
|
||||
|
||||
let result = prompt_websocket_image_with_retry(PromptWebsocketImageWithRetryArgs {
|
||||
websocket: &mut websocket,
|
||||
prompt,
|
||||
aspect_ratio: ClientMessageAspectRatio::WideThreeByTwo,
|
||||
cookies: &cookies,
|
||||
}).await?;
|
||||
|
||||
println!("Reading...");
|
||||
std::io::stdout().flush()?;
|
||||
|
||||
let mut count = 0;
|
||||
|
||||
println!("Polling.");
|
||||
|
||||
loop {
|
||||
//let maybe_message =
|
||||
// websocket.get_response_with_timeout(Duration::from_millis(1000)).await?;
|
||||
|
||||
let maybe_message =
|
||||
websocket.try_next_timeout(Duration::from_millis(1000)).await?;
|
||||
|
||||
let mut maybe_raw_text : Option<String> = None;
|
||||
|
||||
let maybe_message = match maybe_message {
|
||||
None => None,
|
||||
Some(Message::Text(text)) => {
|
||||
let maybe_message = WebsocketServerMessage::from_json_str(text.as_str())?;
|
||||
let mut text = text.as_str().to_string();
|
||||
text.truncate(100);
|
||||
//println!("Received message: {}", text);
|
||||
maybe_raw_text = Some(text);
|
||||
Some(maybe_message)
|
||||
},
|
||||
Some(_) => {
|
||||
warn!("Received non-text websocket message.");
|
||||
None
|
||||
},
|
||||
};
|
||||
|
||||
match maybe_message {
|
||||
None => {
|
||||
println!("No message received within timeout.");
|
||||
count = count + 1;
|
||||
}
|
||||
Some(message) => {
|
||||
match message {
|
||||
WebsocketServerMessage::Image(image) => {
|
||||
println!("IMAGE: {:?}", image.percentage_complete);
|
||||
if let Some(percent) = image.percentage_complete {
|
||||
if percent > 90.0 {
|
||||
println!("Image: {:?}", image);
|
||||
}
|
||||
}
|
||||
}
|
||||
WebsocketServerMessage::Json(json) => {
|
||||
println!("JSON : {:?}", json.percentage_complete);
|
||||
if let Some(percent) = json.percentage_complete {
|
||||
if percent > 90.0 {
|
||||
println!("Image: {:?}", json);
|
||||
}
|
||||
}
|
||||
}
|
||||
WebsocketServerMessage::Unknown(unknown) => {
|
||||
//let typ = unknown.get("type");
|
||||
let unknown_string = unknown.as_str();
|
||||
println!("[UNKNOWN] websocket message: {:?}", maybe_raw_text);
|
||||
}
|
||||
}
|
||||
count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Make sure we capture all the events for images
|
||||
// TODO: Explore the other APIs
|
||||
// TODO understand the IDs
|
||||
// TODO: Video websocket APIs
|
||||
// TODO: Upload APIs
|
||||
|
||||
if count > 5 {
|
||||
println!("No messages after 5 seconds");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
println!("Done polling.");
|
||||
|
||||
log::logger().flush();
|
||||
|
||||
assert_eq!(1,2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
use crate::credentials::grok_client_secrets::GrokClientSecrets;
|
||||
use crate::credentials::grok_cookies::GrokCookies;
|
||||
use crate::error::grok_error::GrokError;
|
||||
use crate::error::grok_generic_api_error::GrokGenericApiError;
|
||||
use crate::requests::index_page::get_index_page_and_scripts::{get_index_page_and_scripts, GetIndexPageAndScriptsArgs};
|
||||
use crate::requests::index_page::get_xsid_script::{get_xsid_script, GetXsidScriptArgs};
|
||||
use crate::requests::index_page::parsers::index::parse_index_baggage::parse_index_baggage;
|
||||
use crate::requests::index_page::parsers::index::parse_index_sentry_trace::parse_index_sentry_trace;
|
||||
use crate::requests::index_page::parsers::index::parse_index_svg_paths::parse_svg_paths_from_index_html;
|
||||
use crate::requests::index_page::parsers::index::parse_index_user_email::parse_index_user_email;
|
||||
use crate::requests::index_page::parsers::index::parse_index_user_id::parse_index_user_id;
|
||||
use crate::requests::index_page::parsers::index::parse_index_verification_token::parse_index_verification_token;
|
||||
use crate::requests::index_page::parsers::script::parse_script_actions_and_xsid_script_path::parse_script_actions_and_xsid_script_path;
|
||||
use crate::requests::index_page::parsers::script::parse_xsid_script_numbers::parse_xsid_script_numbers;
|
||||
use crate::requests::index_page::utils::convert_verification_token_to_loading_anim::convert_verification_token_to_loading_anim;
|
||||
use crate::requests::index_page::utils::select_svg_path_by_loading_anim::select_svg_path_by_loading_anim;
|
||||
|
||||
pub struct RequestClientSecretsArgs<'a> {
|
||||
pub cookies: &'a GrokCookies,
|
||||
}
|
||||
|
||||
// Video generation needs:
|
||||
// 1) baggage -- on index page
|
||||
// 2) sentry_trace -- on index page
|
||||
// 3) x_statsig
|
||||
|
||||
/// Load all the things needed to make requests.
|
||||
pub async fn request_client_secrets(args: RequestClientSecretsArgs<'_>) -> Result<GrokClientSecrets, GrokError> {
|
||||
|
||||
let payloads = get_index_page_and_scripts(GetIndexPageAndScriptsArgs {
|
||||
cookie: args.cookies.as_str(),
|
||||
}).await?;
|
||||
|
||||
let baggage = parse_index_baggage(&payloads.index_body_html)
|
||||
.ok_or_else(|| GrokGenericApiError::IndexHtmlDidNotIncludeExpectedData {
|
||||
message: "Index did not include baggage.".to_string()
|
||||
})?;
|
||||
|
||||
let sentry_trace = parse_index_sentry_trace(&payloads.index_body_html)
|
||||
.ok_or_else(|| GrokGenericApiError::IndexHtmlDidNotIncludeExpectedData {
|
||||
message: "Index did not include sentry trace.".to_string()
|
||||
})?;
|
||||
|
||||
let verification_token = parse_index_verification_token(&payloads.index_body_html)
|
||||
.ok_or_else(|| GrokGenericApiError::IndexHtmlDidNotIncludeExpectedData {
|
||||
message: "Index did not include verification token.".to_string()
|
||||
})?;
|
||||
|
||||
let user_id = parse_index_user_id(&payloads.index_body_html)
|
||||
.ok_or_else(|| GrokGenericApiError::IndexHtmlDidNotIncludeExpectedData {
|
||||
message: "Index did not include user id.".to_string()
|
||||
})?;
|
||||
|
||||
// NB: Optional.
|
||||
let maybe_user_email = parse_index_user_email(&payloads.index_body_html);
|
||||
|
||||
// NB: We'll only use one of the several SVG paths
|
||||
let svg_paths = parse_svg_paths_from_index_html(&payloads.index_body_html);
|
||||
|
||||
if svg_paths.is_empty() {
|
||||
return Err(GrokGenericApiError::IndexHtmlDidNotIncludeExpectedData {
|
||||
message: "Index did not include any SVG paths.".to_string(),
|
||||
}.into());
|
||||
}
|
||||
|
||||
let loading_anim = convert_verification_token_to_loading_anim(&verification_token)?;
|
||||
|
||||
let svg_path_data = select_svg_path_by_loading_anim(&svg_paths, &loading_anim)?;
|
||||
|
||||
let actions_and_xsid_script = parse_script_actions_and_xsid_script_path(&payloads.scripts)?;
|
||||
|
||||
let xsid_script = get_xsid_script(GetXsidScriptArgs {
|
||||
client: &payloads.client,
|
||||
html: &payloads.index_body_html,
|
||||
cookie: args.cookies.as_str(),
|
||||
xsid_script_id: &actions_and_xsid_script.xsid_script_path,
|
||||
}).await?;
|
||||
|
||||
let numbers = parse_xsid_script_numbers(&xsid_script.xsid_script_body);
|
||||
|
||||
if numbers.numbers.is_empty() {
|
||||
return Err(GrokGenericApiError::IndexHtmlDidNotIncludeExpectedData {
|
||||
message: "Index did not include any SVG paths.".to_string(),
|
||||
}.into());
|
||||
}
|
||||
|
||||
Ok(GrokClientSecrets {
|
||||
baggage,
|
||||
sentry_trace,
|
||||
verification_token,
|
||||
svg_path_data,
|
||||
numbers,
|
||||
user_id,
|
||||
user_email: maybe_user_email,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::recipes::request_client_secrets::{request_client_secrets, RequestClientSecretsArgs};
|
||||
use crate::test_utils::get_test_cookies::get_typed_test_cookies;
|
||||
use errors::AnyhowResult;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Manual test invocation
|
||||
async fn test() -> AnyhowResult<()> {
|
||||
let cookies = get_typed_test_cookies()?;
|
||||
|
||||
let secrets = request_client_secrets(RequestClientSecretsArgs {
|
||||
cookies: &cookies,
|
||||
}).await?;
|
||||
|
||||
println!("Baggage: {:?}", secrets.baggage);
|
||||
println!("Sentry trace: {:?}", secrets.sentry_trace);
|
||||
println!("Verification token: {:?}", secrets.verification_token);
|
||||
println!("SVG path: {:?}", secrets.svg_path_data);
|
||||
println!("Numbers: {:?}", secrets.numbers);
|
||||
println!("User ID: {:?}", secrets.user_id);
|
||||
println!("User Email: {:?}", secrets.user_email);
|
||||
|
||||
assert_eq!(1, 2);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
use crate::credentials::grok_full_credentials::GrokFullCredentials;
|
||||
use crate::datatypes::api::aspect_ratio::AspectRatio;
|
||||
use crate::datatypes::api::file_id::FileId;
|
||||
use crate::datatypes::api::post_id::PostId;
|
||||
use crate::datatypes::api::video_generation_mode::VideoGenerationMode;
|
||||
use crate::datatypes::file_upload_spec::FileUploadSpec;
|
||||
use crate::error::grok_error::GrokError;
|
||||
use crate::error::grok_generic_api_error::GrokGenericApiError;
|
||||
use crate::requests::media_posts::create_media_post::grok_create_media_post::{GrokCreateMediaPost, MediaPostType};
|
||||
use crate::requests::media_posts::like_media_post::grok_like_media::GrokLikeMediaPost;
|
||||
use crate::requests::upload_file::grok_upload_file::GrokUploadFile;
|
||||
use crate::requests::video_chat::grok_video_gen_chat_conversation::{GrokVideoGenChatConversationBuilder, VideoMediaPostType};
|
||||
use crate::utils::user_and_file_id_to_image_url::user_and_file_id_to_image_url;
|
||||
use crate::utils::user_and_file_id_to_video_url::user_and_file_id_to_video_url;
|
||||
use log::{error, info};
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
pub struct UploadImageAndGenerateVideo<'a, P: AsRef<Path>> {
|
||||
pub full_credentials: &'a GrokFullCredentials,
|
||||
|
||||
// NB: Must be owned.
|
||||
pub file: FileUploadSpec<P>,
|
||||
|
||||
/// Video generation prompt
|
||||
pub prompt: Option<&'a str>,
|
||||
|
||||
/// Aspect ratio for the video.
|
||||
pub aspect_ratio: Option<AspectRatio>,
|
||||
|
||||
/// Mode to generate the video: normal, fun, spicy, etc.
|
||||
pub mode: Option<VideoGenerationMode>,
|
||||
|
||||
/// Wait for the full video to be generated before returning
|
||||
/// By default, the endpoint stays open for 30-ish seconds while
|
||||
/// the video generates and streams back JSON progress updates.
|
||||
/// If we set this to false, we'll wait for the video ID and exit
|
||||
/// the request early, before the video is finished asynchronously
|
||||
/// generating.
|
||||
pub wait_for_generation: bool,
|
||||
|
||||
pub individual_request_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ImageUploadAndGenerateVideoResult {
|
||||
/// The ID of the "post"
|
||||
/// This often is equivalent to the upload id / `image_file_id`,
|
||||
/// and this is in the user-facing URL for website navigation to the video.
|
||||
pub post_id: PostId,
|
||||
|
||||
/// The ID of the file we uploaded
|
||||
pub image_file_id: FileId,
|
||||
|
||||
/// The ID of the video we're generating, which may or may not exist yet.
|
||||
pub video_file_id: Option<FileId>,
|
||||
|
||||
/// The URL for the video, which may or may not exist yet.
|
||||
pub video_url: Option<String>,
|
||||
|
||||
/// Whether the video generation has already completed.
|
||||
/// Based on request `wait_for_generation`.
|
||||
pub generation_is_complete: bool,
|
||||
}
|
||||
|
||||
pub async fn upload_image_and_generate_video<P: AsRef<Path>>(args: UploadImageAndGenerateVideo<'_, P>)
|
||||
-> Result<ImageUploadAndGenerateVideoResult, GrokError>
|
||||
{
|
||||
|
||||
info!("Uploading file to Grok...");
|
||||
|
||||
let request = GrokUploadFile {
|
||||
file: args.file,
|
||||
cookie: args.full_credentials.cookies.to_string(),
|
||||
request_timeout: args.individual_request_timeout,
|
||||
};
|
||||
|
||||
let upload_result = request.upload().await?;
|
||||
|
||||
info!("File URI: {:?}", upload_result.file_uri);
|
||||
info!("File Metadata ID: {:?}", upload_result.file_id);
|
||||
|
||||
let upload_file_id = match upload_result.file_id {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
error!("Previous file upload failed. Cannot continue.");
|
||||
return Err(GrokGenericApiError::UploadFailed.into());
|
||||
}
|
||||
};
|
||||
|
||||
let url = user_and_file_id_to_image_url(
|
||||
&args.full_credentials.client_secrets.user_id,
|
||||
&upload_file_id
|
||||
);
|
||||
|
||||
info!("Uploaded URI: {:?}", url);
|
||||
|
||||
info!("Creating media post...");
|
||||
|
||||
let request = GrokCreateMediaPost {
|
||||
user_id: &args.full_credentials.client_secrets.user_id,
|
||||
file_id: &upload_file_id,
|
||||
media_type: MediaPostType::UserUploadedImage,
|
||||
cookie: args.full_credentials.cookies.as_str(),
|
||||
request_timeout: args.individual_request_timeout,
|
||||
};
|
||||
|
||||
let post_result = request.send().await?;
|
||||
|
||||
info!("Posted Image.");
|
||||
|
||||
info!("Video prompt: {:?}", args.prompt);
|
||||
info!("Video aspect ratio: {:?}", args.aspect_ratio);
|
||||
|
||||
info!("Generate video...");
|
||||
|
||||
let request = GrokVideoGenChatConversationBuilder {
|
||||
user_id: &args.full_credentials.client_secrets.user_id,
|
||||
file_id: &upload_file_id,
|
||||
media_type: VideoMediaPostType::UserUploadedImage,
|
||||
cookie: args.full_credentials.cookies.as_str(),
|
||||
prompt: args.prompt,
|
||||
mode: args.mode,
|
||||
aspect_ratio: args.aspect_ratio,
|
||||
request_timeout: args.individual_request_timeout,
|
||||
baggage: &args.full_credentials.client_secrets.baggage,
|
||||
sentry_trace: &args.full_credentials.client_secrets.sentry_trace,
|
||||
verification_token: &args.full_credentials.client_secrets.verification_token,
|
||||
svg_data: &args.full_credentials.client_secrets.svg_path_data,
|
||||
numbers: &args.full_credentials.client_secrets.numbers,
|
||||
};
|
||||
|
||||
let maybe_video_file_id;
|
||||
let generation_is_complete;
|
||||
|
||||
if args.wait_for_generation {
|
||||
info!("Waiting for video generation...");
|
||||
let video_gen_result = request.wait_for_video().await?;
|
||||
maybe_video_file_id = video_gen_result.video_file_id;
|
||||
generation_is_complete = true;
|
||||
} else {
|
||||
info!("Streaming response partially...");
|
||||
let video_gen_result = request.stream_only_video_id().await?;
|
||||
maybe_video_file_id = video_gen_result.video_file_id;
|
||||
generation_is_complete = false;
|
||||
}
|
||||
|
||||
let maybe_video_url = maybe_video_file_id
|
||||
.as_ref()
|
||||
.map(|file_id| {
|
||||
user_and_file_id_to_video_url(&args.full_credentials.client_secrets.user_id, file_id, false)
|
||||
});
|
||||
|
||||
info!("Video Generation Enqueued");
|
||||
|
||||
info!("Liking media...");
|
||||
|
||||
let request = GrokLikeMediaPost {
|
||||
file_id: &upload_file_id,
|
||||
cookie: args.full_credentials.cookies.as_str(),
|
||||
request_timeout: args.individual_request_timeout,
|
||||
baggage: &args.full_credentials.client_secrets.baggage,
|
||||
sentry_trace: &args.full_credentials.client_secrets.sentry_trace,
|
||||
verification_token: &args.full_credentials.client_secrets.verification_token,
|
||||
svg_data: &args.full_credentials.client_secrets.svg_path_data,
|
||||
numbers: &args.full_credentials.client_secrets.numbers,
|
||||
};
|
||||
|
||||
let _like_result = request.send().await?;
|
||||
|
||||
info!("Media Liked");
|
||||
|
||||
Ok(ImageUploadAndGenerateVideoResult {
|
||||
post_id: post_result.post_id,
|
||||
image_file_id: upload_file_id,
|
||||
video_file_id: maybe_video_file_id,
|
||||
video_url: maybe_video_url,
|
||||
generation_is_complete,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::credentials::grok_full_credentials::GrokFullCredentials;
|
||||
use crate::datatypes::api::aspect_ratio::AspectRatio;
|
||||
use crate::datatypes::file_upload_spec::FileUploadSpec;
|
||||
use crate::recipes::request_client_secrets::{request_client_secrets, RequestClientSecretsArgs};
|
||||
use crate::recipes::upload_image_and_generate_video::{upload_image_and_generate_video, UploadImageAndGenerateVideo};
|
||||
use crate::test_utils::get_test_cookies::get_typed_test_cookies;
|
||||
use crate::test_utils::setup_test_logging::setup_test_logging;
|
||||
use errors::AnyhowResult;
|
||||
use log::LevelFilter;
|
||||
// Result: GrokUploadFileResponse { file_metadata_id:
|
||||
// Some("acdee48f-9d6f-4bc6-9d06-fcc97dd4418a"), file_uri:
|
||||
// Some("users/85980643-ffab-4984-a3de-59a608c47d7f/acdee48f-9d6f-4bc6-9d06-fcc97dd4418a/content") }
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Client side tests only
|
||||
async fn test_upload_image_and_generate_video() -> AnyhowResult<()> {
|
||||
setup_test_logging(LevelFilter::Info);
|
||||
|
||||
//let image_path = "/Users/bt/Pictures/Zelda 64 Art/FCgYX6tWEAEhpsy.jpg";
|
||||
//let prompt = "our hero link plunges the sword into the pedestal, the temple is glowing with a blue aura";
|
||||
|
||||
let image_path = "/Users/bt/Pictures/Midjourney/hipster_girl.png";
|
||||
let maybe_prompt = Some("an anime girl runs away from a giant t-rex");
|
||||
|
||||
let cookies = get_typed_test_cookies()?;
|
||||
|
||||
let secrets = request_client_secrets(RequestClientSecretsArgs {
|
||||
cookies: &cookies,
|
||||
}).await?;
|
||||
|
||||
println!("[test] Verification Token: {:?}", secrets.verification_token);
|
||||
println!("[test] Sentry Trace: {:?}", secrets.sentry_trace);
|
||||
println!("[test] Numbers: {:?}", secrets.numbers);
|
||||
println!("[test] Svg Path: {:?}", secrets.svg_path_data);
|
||||
println!("[test] Baggage: {:?}", secrets.baggage);
|
||||
|
||||
let credentials = GrokFullCredentials::from_cookies_and_client_secrets(cookies, secrets);
|
||||
|
||||
let video_result = upload_image_and_generate_video(UploadImageAndGenerateVideo {
|
||||
full_credentials: &credentials,
|
||||
file: FileUploadSpec::Path(image_path),
|
||||
prompt: maybe_prompt,
|
||||
aspect_ratio: Some(AspectRatio::Square),
|
||||
mode: None,
|
||||
individual_request_timeout: None,
|
||||
wait_for_generation: false,
|
||||
}).await?;
|
||||
|
||||
println!("[test] Post ID: {:?}", video_result.post_id);
|
||||
println!("[test] Image File ID: {:?}", video_result.image_file_id);
|
||||
println!("[test] Video File ID: {:?}", video_result.video_file_id);
|
||||
println!("[test] Video URL: {:?}", video_result.video_url);
|
||||
|
||||
assert_eq!(1, 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+348
@@ -0,0 +1,348 @@
|
||||
use crate::credentials::grok_client_secrets::GrokClientSecrets;
|
||||
use crate::credentials::grok_full_credentials::GrokFullCredentials;
|
||||
use crate::datatypes::api::aspect_ratio::AspectRatio;
|
||||
use crate::datatypes::api::video_generation_mode::VideoGenerationMode;
|
||||
use crate::datatypes::file_upload_spec::FileUploadSpec;
|
||||
use crate::error::grok_client_error::GrokClientError;
|
||||
use crate::error::grok_error::GrokError;
|
||||
use crate::error::grok_generic_api_error::GrokGenericApiError;
|
||||
use crate::error::grok_specific_api_error::GrokSpecificApiError;
|
||||
use crate::recipes::request_client_secrets::{request_client_secrets, RequestClientSecretsArgs};
|
||||
use crate::recipes::upload_image_and_generate_video::{ImageUploadAndGenerateVideoResult, UploadImageAndGenerateVideo};
|
||||
use crate::requests::media_posts::create_media_post::grok_create_media_post::{GrokCreateMediaPost, MediaPostType};
|
||||
use crate::requests::media_posts::like_media_post::grok_like_media::GrokLikeMediaPost;
|
||||
use crate::requests::upload_file::grok_upload_file::GrokUploadFile;
|
||||
use crate::requests::video_chat::grok_video_gen_chat_conversation::{GrokVideoGenChatConversationBuilder, VideoMediaPostType};
|
||||
use crate::utils::user_and_file_id_to_image_url::user_and_file_id_to_image_url;
|
||||
use crate::utils::user_and_file_id_to_video_url::user_and_file_id_to_video_url;
|
||||
use log::{error, info};
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
pub struct UploadImageAndGenerateVideoWithRetry<'a, P: AsRef<Path>> {
|
||||
pub credentials: &'a GrokFullCredentials,
|
||||
|
||||
// NB: Must be owned.
|
||||
pub file: FileUploadSpec<P>,
|
||||
|
||||
/// Video generation prompt
|
||||
pub prompt: Option<&'a str>,
|
||||
|
||||
/// Aspect ratio for the video.
|
||||
pub aspect_ratio: Option<AspectRatio>,
|
||||
|
||||
/// Mode to generate the video: normal, fun, spicy, etc.
|
||||
pub mode: Option<VideoGenerationMode>,
|
||||
|
||||
/// Wait for the full video to be generated before returning
|
||||
/// By default, the endpoint stays open for 30-ish seconds while
|
||||
/// the video generates and streams back JSON progress updates.
|
||||
/// If we set this to false, we'll wait for the video ID and exit
|
||||
/// the request early, before the video is finished asynchronously
|
||||
/// generating.
|
||||
pub wait_for_generation: bool,
|
||||
|
||||
pub individual_request_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ImageUploadAndGenerateVideoWithRetryResult {
|
||||
/// If new secrets were generated, they're stored here.
|
||||
pub maybe_new_client_secrets: Option<GrokClientSecrets>,
|
||||
|
||||
/// The video generation result.
|
||||
pub upload_result: ImageUploadAndGenerateVideoResult,
|
||||
}
|
||||
|
||||
pub async fn upload_image_and_generate_video_with_retry<P: AsRef<Path>>(
|
||||
args: UploadImageAndGenerateVideoWithRetry<'_, P>
|
||||
) -> Result<ImageUploadAndGenerateVideoWithRetryResult, GrokError> {
|
||||
|
||||
let mut current_full_credentials_ref = args.credentials;
|
||||
let mut maybe_new_credentials = None;
|
||||
|
||||
info!("Uploading file to Grok...");
|
||||
|
||||
let request = GrokUploadFile {
|
||||
file: args.file,
|
||||
cookie: current_full_credentials_ref.cookies.to_string(),
|
||||
request_timeout: args.individual_request_timeout,
|
||||
};
|
||||
|
||||
let upload_result = request.upload().await?;
|
||||
|
||||
info!("File URI: {:?}", upload_result.file_uri);
|
||||
info!("File Metadata ID: {:?}", upload_result.file_id);
|
||||
|
||||
let upload_file_id = match upload_result.file_id {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
error!("Previous file upload failed. Cannot continue.");
|
||||
return Err(GrokGenericApiError::UploadFailed.into());
|
||||
}
|
||||
};
|
||||
|
||||
let url = user_and_file_id_to_image_url(
|
||||
¤t_full_credentials_ref.client_secrets.user_id,
|
||||
&upload_file_id
|
||||
);
|
||||
|
||||
info!("Uploaded URI: {:?}", url);
|
||||
|
||||
info!("Creating media post...");
|
||||
|
||||
let request = GrokCreateMediaPost {
|
||||
user_id: ¤t_full_credentials_ref.client_secrets.user_id,
|
||||
file_id: &upload_file_id,
|
||||
media_type: MediaPostType::UserUploadedImage,
|
||||
cookie: current_full_credentials_ref.cookies.as_str(),
|
||||
request_timeout: args.individual_request_timeout,
|
||||
};
|
||||
|
||||
let post_result = request.send().await?;
|
||||
|
||||
info!("Posted Image.");
|
||||
|
||||
info!("Video prompt: {:?}", args.prompt);
|
||||
info!("Video aspect ratio: {:?}", args.aspect_ratio);
|
||||
|
||||
let mut video_enqueued_successfully = false;
|
||||
let mut generation_is_complete= false;
|
||||
let mut maybe_video_file_id = None;
|
||||
let mut last_error = None;
|
||||
|
||||
for i in 0..3 {
|
||||
info!("Generate video... attempt {}", (i+1));
|
||||
|
||||
let request = GrokVideoGenChatConversationBuilder {
|
||||
user_id: ¤t_full_credentials_ref.client_secrets.user_id,
|
||||
file_id: &upload_file_id,
|
||||
media_type: VideoMediaPostType::UserUploadedImage,
|
||||
cookie: current_full_credentials_ref.cookies.as_str(),
|
||||
prompt: args.prompt,
|
||||
mode: args.mode,
|
||||
aspect_ratio: args.aspect_ratio,
|
||||
request_timeout: args.individual_request_timeout,
|
||||
baggage: ¤t_full_credentials_ref.client_secrets.baggage,
|
||||
sentry_trace: ¤t_full_credentials_ref.client_secrets.sentry_trace,
|
||||
verification_token: ¤t_full_credentials_ref.client_secrets.verification_token,
|
||||
svg_data: ¤t_full_credentials_ref.client_secrets.svg_path_data,
|
||||
numbers: ¤t_full_credentials_ref.client_secrets.numbers,
|
||||
};
|
||||
|
||||
let video_gen_result =
|
||||
if args.wait_for_generation {
|
||||
request.wait_for_video()
|
||||
.await
|
||||
.map(|res| res.video_file_id)
|
||||
} else {
|
||||
request.stream_only_video_id()
|
||||
.await
|
||||
.map(|res| res.video_file_id)
|
||||
};
|
||||
|
||||
match video_gen_result {
|
||||
Ok(res) => {
|
||||
info!("Video Generation Enqueued!");
|
||||
video_enqueued_successfully = true;
|
||||
generation_is_complete = args.wait_for_generation; // If we synchronously waited, it'll be complete.
|
||||
maybe_video_file_id = res;
|
||||
break;
|
||||
}
|
||||
Err(GrokError::ApiSpecific(GrokSpecificApiError::AutomationBlocked)) => {
|
||||
info!("Grok automation blocked; renewing credentials.");
|
||||
last_error = Some(GrokError::ApiSpecific(GrokSpecificApiError::AutomationBlocked));
|
||||
}
|
||||
Err(err) => {
|
||||
info!("Grok error; renewing credentials. Error = {:?}", err);
|
||||
last_error = Some(err);
|
||||
}
|
||||
}
|
||||
|
||||
info!("Refreshing Grok client secrets...");
|
||||
|
||||
let secrets = request_client_secrets(RequestClientSecretsArgs {
|
||||
cookies: ¤t_full_credentials_ref.cookies,
|
||||
}).await?;
|
||||
|
||||
// NB: This is just to appease the borrow checker. It doesn't like that we're borrowing `maybe_new_credentials`
|
||||
// in a loop and replacing it while borrowed, hence this hack.
|
||||
current_full_credentials_ref = args.credentials;
|
||||
|
||||
maybe_new_credentials = Some(GrokFullCredentials::from_cookies_and_client_secrets(
|
||||
current_full_credentials_ref.cookies.clone(),
|
||||
secrets,
|
||||
));
|
||||
|
||||
if let Some(creds) = maybe_new_credentials.as_ref() {
|
||||
current_full_credentials_ref = creds;
|
||||
}
|
||||
}
|
||||
|
||||
if !video_enqueued_successfully {
|
||||
if let Some(err) = last_error {
|
||||
return Err(err);
|
||||
} else {
|
||||
// NB: This branch should be impossible as we should always set last_error.
|
||||
return Err(GrokError::Client(GrokClientError::ErrorGeneratingVideo))
|
||||
}
|
||||
}
|
||||
|
||||
let mut video_liked_successfully = false;
|
||||
let mut last_error = None;
|
||||
|
||||
for i in 0..3 {
|
||||
info!("Liking media ... attempt {}", (i+1));
|
||||
|
||||
let request = GrokLikeMediaPost {
|
||||
file_id: &upload_file_id,
|
||||
cookie: current_full_credentials_ref.cookies.as_str(),
|
||||
request_timeout: args.individual_request_timeout,
|
||||
baggage: ¤t_full_credentials_ref.client_secrets.baggage,
|
||||
sentry_trace: ¤t_full_credentials_ref.client_secrets.sentry_trace,
|
||||
verification_token: ¤t_full_credentials_ref.client_secrets.verification_token,
|
||||
svg_data: ¤t_full_credentials_ref.client_secrets.svg_path_data,
|
||||
numbers: ¤t_full_credentials_ref.client_secrets.numbers,
|
||||
};
|
||||
|
||||
let like_result = request.send().await;
|
||||
|
||||
match like_result {
|
||||
Ok(_) => {
|
||||
info!("Media Liked");
|
||||
video_liked_successfully = true;
|
||||
break;
|
||||
}
|
||||
Err(GrokError::ApiSpecific(GrokSpecificApiError::AutomationBlocked)) => {
|
||||
info!("Grok automation blocked liking media; renewing credentials.");
|
||||
last_error = Some(GrokError::ApiSpecific(GrokSpecificApiError::AutomationBlocked));
|
||||
}
|
||||
Err(err) => {
|
||||
info!("Grok error liking media; renewing credentials. Error = {:?}", err);
|
||||
last_error = Some(err);
|
||||
}
|
||||
}
|
||||
|
||||
info!("Refreshing Grok client secrets...");
|
||||
|
||||
let secrets = request_client_secrets(RequestClientSecretsArgs {
|
||||
cookies: ¤t_full_credentials_ref.cookies,
|
||||
}).await?;
|
||||
|
||||
// NB: This is just to appease the borrow checker. It doesn't like that we're borrowing `maybe_new_credentials`
|
||||
// in a loop and replacing it while borrowed, hence this hack.
|
||||
current_full_credentials_ref = args.credentials;
|
||||
|
||||
maybe_new_credentials = Some(GrokFullCredentials::from_cookies_and_client_secrets(
|
||||
current_full_credentials_ref.cookies.clone(),
|
||||
secrets,
|
||||
));
|
||||
|
||||
if let Some(creds) = maybe_new_credentials.as_ref() {
|
||||
current_full_credentials_ref = creds;
|
||||
}
|
||||
}
|
||||
|
||||
if !video_liked_successfully {
|
||||
if let Some(err) = last_error {
|
||||
return Err(err);
|
||||
} else {
|
||||
// NB: This branch should be impossible as we should always set last_error.
|
||||
return Err(GrokError::Client(GrokClientError::ErrorGeneratingVideo))
|
||||
}
|
||||
}
|
||||
|
||||
let maybe_video_url = maybe_video_file_id
|
||||
.as_ref()
|
||||
.map(|file_id| {
|
||||
user_and_file_id_to_video_url(¤t_full_credentials_ref.client_secrets.user_id, file_id, false)
|
||||
});
|
||||
|
||||
Ok(ImageUploadAndGenerateVideoWithRetryResult {
|
||||
maybe_new_client_secrets: maybe_new_credentials
|
||||
.map(|secrets| secrets.client_secrets),
|
||||
upload_result: ImageUploadAndGenerateVideoResult {
|
||||
post_id: post_result.post_id,
|
||||
image_file_id: upload_file_id,
|
||||
video_file_id: maybe_video_file_id,
|
||||
video_url: maybe_video_url,
|
||||
generation_is_complete,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::credentials::grok_full_credentials::GrokFullCredentials;
|
||||
use crate::datatypes::api::aspect_ratio::AspectRatio;
|
||||
use crate::datatypes::api::video_generation_mode::VideoGenerationMode;
|
||||
use crate::datatypes::file_upload_spec::FileUploadSpec;
|
||||
use crate::recipes::request_client_secrets::{request_client_secrets, RequestClientSecretsArgs};
|
||||
use crate::recipes::upload_image_and_generate_video_with_retry::{upload_image_and_generate_video_with_retry, UploadImageAndGenerateVideoWithRetry};
|
||||
use crate::test_utils::get_test_cookies::get_typed_test_cookies;
|
||||
use crate::test_utils::setup_test_logging::setup_test_logging;
|
||||
use errors::AnyhowResult;
|
||||
use log::LevelFilter;
|
||||
|
||||
// Result: GrokUploadFileResponse { file_metadata_id:
|
||||
// Some("acdee48f-9d6f-4bc6-9d06-fcc97dd4418a"), file_uri:
|
||||
// Some("users/85980643-ffab-4984-a3de-59a608c47d7f/acdee48f-9d6f-4bc6-9d06-fcc97dd4418a/content") }
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Client side tests only
|
||||
async fn test_upload_image_and_generate_video() -> AnyhowResult<()> {
|
||||
setup_test_logging(LevelFilter::Info);
|
||||
|
||||
//let image_path = "/Users/bt/Pictures/Zelda 64 Art/FCgYX6tWEAEhpsy.jpg";
|
||||
//let prompt = "our hero link plunges the sword into the pedestal, the temple is glowing with a blue aura";
|
||||
|
||||
let image_path = "/Users/bt/Pictures/Midjourney/hipster_girl.png";
|
||||
let maybe_prompt = Some("an anime girl turns and high fives a giant t-rex");
|
||||
|
||||
let cookies = get_typed_test_cookies()?;
|
||||
|
||||
let mut bad_secrets = request_client_secrets(RequestClientSecretsArgs {
|
||||
cookies: &cookies,
|
||||
}).await?;
|
||||
|
||||
// NB: We're purposely messing these up to trigger "retry".
|
||||
bad_secrets.numbers.numbers = Vec::new();
|
||||
bad_secrets.sentry_trace.0 = "".to_string();
|
||||
|
||||
let credentials = GrokFullCredentials::from_cookies_and_client_secrets(cookies, bad_secrets);
|
||||
|
||||
let result = upload_image_and_generate_video_with_retry(UploadImageAndGenerateVideoWithRetry {
|
||||
credentials: &credentials,
|
||||
file: FileUploadSpec::Path(image_path),
|
||||
prompt: maybe_prompt,
|
||||
mode: Some(VideoGenerationMode::Custom),
|
||||
aspect_ratio: Some(AspectRatio::TallTwoByThree),
|
||||
individual_request_timeout: None,
|
||||
wait_for_generation: false,
|
||||
}).await?;
|
||||
|
||||
let new_secrets = result.maybe_new_client_secrets;
|
||||
|
||||
println!("[test] New secrets ? : {:?}", new_secrets.is_some());
|
||||
|
||||
if let Some(secrets) = new_secrets {
|
||||
println!("[test] Verification Token: {:?}", secrets.verification_token);
|
||||
println!("[test] Sentry Trace: {:?}", secrets.sentry_trace);
|
||||
println!("[test] Numbers: {:?}", secrets.numbers);
|
||||
println!("[test] Svg Path: {:?}", secrets.svg_path_data);
|
||||
println!("[test] Baggage: {:?}", secrets.baggage);
|
||||
}
|
||||
|
||||
let video_result = result.upload_result;
|
||||
|
||||
println!("[test] Post ID: {:?}", video_result.post_id);
|
||||
println!("[test] Image File ID: {:?}", video_result.image_file_id);
|
||||
println!("[test] Video File ID: {:?}", video_result.video_file_id);
|
||||
println!("[test] Video URL: {:?}", video_result.video_url);
|
||||
|
||||
assert_eq!(1, 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
// Note: this was accessing the file outside of the page (right click-> open in new tab)
|
||||
|
||||
curl 'https://assets.grok.com/users/85980643-ffab-4984-a3de-59a608c47d7f/generated/34fa0313-3cde-4693-b046-319093374dbe/generated_video.mp4?cache=1' \
|
||||
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:144.0) Gecko/20100101 Firefox/144.0' \
|
||||
-H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' \
|
||||
-H 'Accept-Language: en-US,en;q=0.5' \
|
||||
-H 'Accept-Encoding: gzip, deflate, br, zstd' \
|
||||
-H 'Referer: https://grok.com/' \
|
||||
-H 'Sec-GPC: 1' \
|
||||
-H 'Connection: keep-alive' \
|
||||
-H 'Cookie: cf_clearance=exXZNJVPTGE_jw9bkiCb3mhyiRhx4m9KU3dOAIg4jQQ-1761851143-1.2.1.1-ylhkr3kBkOwGLyhesgDutGoC515v.kuElqWRvO63KLtVpPOGiOJ8oXNZpCKHjFfB.jS6ucdE1QVGpW06pr_dKs1QWI5vLEhDc7elgHiaGGAFOrNb..raDuk0UCvlQtXS9Iqy5k9eUq.LJ9QptwsjbAMm2K1y8tACePxF.qcDUXeoRWvlAOvWU4dTIS5ZsU6c1BD5ujgWYujpANhcE93qosdLJ6ho7f5elwI0aOLbx0k; x-anonuserid=41f3614a-1a08-4c9b-81b4-0c2085f6b8d8; x-challenge=tmsr%2B%2BbUg2pjTWyAOGqqFe%2BW38g66vDImJiVuiLtmSmNpaQm%2B46WajkdQoflNC1CyRKRcOLOc1ylzYI%2BerCVbWLrHr022ZWcTJqcN0o1499%2FcjPAAspeAKPV%2F6OAaHmYGu8eSXR4dj27qXSWxLixSctJLIQZVpKXX7njuuSkvKDQCwBgt%2BY%3D; x-signature=GqtNLJQw2%2BOk1rO6sby9vQT3N5%2BiX8g%2B8Bqrndiv51dSYQFC4v2%2FlrkKmH1utjqExbfvVjtks0%2Bn6RC3q61%2FRA%3D%3D; sso=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiMmM1OGEyMjQtMDViZi00NjllLWFkYjUtNmY3ODdmZDU1MTc2In0.rnUG2UvKv32YBGSyYWrWh_1KaLiTix-6H4TZe4ZywfU; sso-rw=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiMmM1OGEyMjQtMDViZi00NjllLWFkYjUtNmY3ODdmZDU1MTc2In0.rnUG2UvKv32YBGSyYWrWh_1KaLiTix-6H4TZe4ZywfU; mp_ea93da913ddb66b6372b89d97b1029ac_mixpanel=%7B%22distinct_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%2C%22%24device_id%22%3A%22c02bd50a-db0e-4f35-903e-b03f32f28b5f%22%2C%22%24initial_referrer%22%3A%22%24direct%22%2C%22%24initial_referring_domain%22%3A%22%24direct%22%2C%22__mps%22%3A%7B%7D%2C%22__mpso%22%3A%7B%7D%2C%22__mpus%22%3A%7B%7D%2C%22__mpa%22%3A%7B%7D%2C%22__mpu%22%3A%7B%7D%2C%22__mpr%22%3A%5B%5D%2C%22__mpap%22%3A%5B%5D%2C%22%24user_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%7D' \
|
||||
-H 'Upgrade-Insecure-Requests: 1' \
|
||||
-H 'Sec-Fetch-Dest: document' \
|
||||
-H 'Sec-Fetch-Mode: navigate' \
|
||||
-H 'Sec-Fetch-Site: same-site' \
|
||||
-H 'Sec-Fetch-User: ?1' \
|
||||
-H 'Priority: u=0, i'
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
use crate::client::browser_user_agents::FIREFOX_143_MAC_USER_AGENT;
|
||||
use crate::datatypes::api::file_id::FileId;
|
||||
use crate::datatypes::api::user_id::UserId;
|
||||
use crate::error::grok_client_error::GrokClientError;
|
||||
use crate::error::grok_error::GrokError;
|
||||
use crate::error::grok_generic_api_error::GrokGenericApiError;
|
||||
use crate::utils::user_and_file_id_to_video_url::user_and_file_id_to_video_url;
|
||||
use log::{error, info, warn};
|
||||
use std::time::Duration;
|
||||
use wreq::header::{ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, CACHE_CONTROL, CONNECTION, CONTENT_TYPE, COOKIE, ORIGIN, REFERER, TE, UPGRADE_INSECURE_REQUESTS, USER_AGENT};
|
||||
use wreq::Client;
|
||||
use wreq_util::Emulation;
|
||||
|
||||
pub struct DownloadVideoFileArgs<'a> {
|
||||
pub cookies: &'a str,
|
||||
pub user_id: &'a UserId,
|
||||
pub file_id: &'a FileId,
|
||||
pub request_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
pub struct VideoFile {
|
||||
pub bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
pub async fn download_video_file(args: DownloadVideoFileArgs<'_>) -> Result<VideoFile, GrokError> {
|
||||
|
||||
let client = Client::builder()
|
||||
.emulation(Emulation::Firefox143)
|
||||
.build()
|
||||
.map_err(|err| GrokClientError::WreqClientError(err))?;
|
||||
|
||||
let video_url = user_and_file_id_to_video_url(&args.user_id, &args.file_id, false);
|
||||
|
||||
info!("Video file URL: {}", video_url);
|
||||
|
||||
let mut request_builder = client.get(&video_url)
|
||||
.header(USER_AGENT, FIREFOX_143_MAC_USER_AGENT)
|
||||
.header(ACCEPT, "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
|
||||
.header(ACCEPT_LANGUAGE, "en-US,en;q=0.5")
|
||||
.header(ACCEPT_ENCODING, "gzip, deflate, br, zstd")
|
||||
.header(REFERER, "https://grok.com/")
|
||||
.header("Sec-GPC", "1")
|
||||
.header(CONNECTION, "keep-alive")
|
||||
.header(COOKIE, args.cookies)
|
||||
.header(UPGRADE_INSECURE_REQUESTS, "1")
|
||||
.header("sec-fetch-dest", "document")
|
||||
.header("sec-fetch-mode", "navigate")
|
||||
.header("sec-fetch-site", "same-site")
|
||||
.header("sec-fetch-user", "?1")
|
||||
.header("priority", "u=0, i");
|
||||
|
||||
|
||||
if let Some(timeout) = args.request_timeout {
|
||||
request_builder = request_builder.timeout(timeout);
|
||||
}
|
||||
|
||||
let response = request_builder.send()
|
||||
.await
|
||||
.map_err(|err| GrokGenericApiError::WreqError(err))?;
|
||||
|
||||
let status = response.status();
|
||||
|
||||
info!("Download video status: {:?}", status);
|
||||
|
||||
let download_bytes = &response.bytes()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error reading Grok create media response body: {:?}", err);
|
||||
GrokGenericApiError::WreqError(err)
|
||||
})?;
|
||||
|
||||
// TODO: Classify errors
|
||||
if !status.is_success() {
|
||||
warn!("Not successful liking media (status: {:?})", status);
|
||||
// error!("Upload file request returned an error (code {}) : {:?}", status.as_u16(), response_body);
|
||||
// return Err(classify_general_http_status_code_and_body(status, response_body));
|
||||
}
|
||||
|
||||
Ok(VideoFile {
|
||||
bytes: download_bytes.to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use crate::datatypes::api::file_id::FileId;
|
||||
use crate::recipes::request_client_secrets::{request_client_secrets, RequestClientSecretsArgs};
|
||||
use crate::requests::download_video_file::download_video_file::{download_video_file, DownloadVideoFileArgs};
|
||||
use crate::test_utils::get_test_cookies::get_typed_test_cookies;
|
||||
use errors::AnyhowResult;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Client side tests only
|
||||
async fn test_download() -> AnyhowResult<()> {
|
||||
//setup_test_logging(LevelFilter::Trace);
|
||||
|
||||
let cookies = get_typed_test_cookies()?;
|
||||
|
||||
let file_id = FileId("d9b300c6-9562-4e24-a87a-3ede2a53f0bc".to_string()); // Ernest
|
||||
|
||||
let secrets = request_client_secrets(RequestClientSecretsArgs {
|
||||
cookies: &cookies,
|
||||
}).await?;
|
||||
|
||||
println!("Verification Token: {:?}", secrets.verification_token);
|
||||
println!("Sentry Trace: {:?}", secrets.sentry_trace);
|
||||
println!("Numbers: {:?}", secrets.numbers);
|
||||
println!("Svg Path: {:?}", secrets.svg_path_data);
|
||||
println!("Baggage: {:?}", secrets.baggage);
|
||||
|
||||
let download = download_video_file(DownloadVideoFileArgs {
|
||||
cookies: cookies.as_str(),
|
||||
user_id: &secrets.user_id,
|
||||
file_id: &file_id,
|
||||
request_timeout: None,
|
||||
}).await?;
|
||||
|
||||
println!("Video byte length: {:?}", download.bytes.len());
|
||||
|
||||
let mut file = File::create("download_video_file.test.mp4")?;
|
||||
file.write_all(&download.bytes)?;
|
||||
|
||||
assert_eq!(1, 2);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
use crate::client::browser_user_agents::FIREFOX_143_MAC_USER_AGENT;
|
||||
use crate::datatypes::api::file_id::FileId;
|
||||
use crate::datatypes::api::user_id::UserId;
|
||||
use crate::error::grok_client_error::GrokClientError;
|
||||
use crate::error::grok_error::GrokError;
|
||||
use crate::error::grok_generic_api_error::GrokGenericApiError;
|
||||
use crate::utils::user_and_file_id_to_video_url::user_and_file_id_to_video_url;
|
||||
use futures::{StreamExt, TryStreamExt};
|
||||
use log::{error, info, warn};
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
use tokio::fs::File;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use wreq::header::{ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, CACHE_CONTROL, CONNECTION, CONTENT_TYPE, COOKIE, ORIGIN, REFERER, TE, UPGRADE_INSECURE_REQUESTS, USER_AGENT};
|
||||
use wreq::{Client, Request, Response};
|
||||
use wreq_util::Emulation;
|
||||
|
||||
pub struct GrokDownloadVideo<'a> {
|
||||
pub cookies: &'a str,
|
||||
pub user_id: &'a UserId,
|
||||
pub file_id: &'a FileId,
|
||||
pub request_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
pub struct DownloadedVideoBytes {
|
||||
pub bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
impl <'a> GrokDownloadVideo<'a> {
|
||||
|
||||
pub async fn download_to_path<P: AsRef<Path>>(&self, path: P) -> Result<(), GrokError> {
|
||||
let response = self.send_request().await?;
|
||||
|
||||
let mut stream = response.bytes_stream();
|
||||
|
||||
let mut file = File::create(&path)
|
||||
.await
|
||||
.map_err(|err| GrokClientError::CannotOpenLocalFileForWriting(err))?;
|
||||
|
||||
//let _r = stream.for_each_concurrent(None, |chunk_result| async {
|
||||
// match chunk_result {
|
||||
// Ok(chunk) => {
|
||||
// file;
|
||||
// //if let Err(e) = file.write_all(&chunk).await {
|
||||
// // //error!("Error writing to file: {}", e);
|
||||
// //}
|
||||
// }
|
||||
// Err(e) => {
|
||||
// //error!("Error in stream: {}", e);
|
||||
// }
|
||||
// }
|
||||
//}).await;
|
||||
|
||||
while let Some(chunk) = stream.try_next()
|
||||
.await
|
||||
.map_err(|err| GrokGenericApiError::WreqError(err))?
|
||||
{
|
||||
file.write_all(&chunk)
|
||||
.await
|
||||
.map_err(|err| GrokClientError::CannotOpenLocalFileForWriting(err))?
|
||||
}
|
||||
|
||||
file.flush().await.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn download_bytes(&self) -> Result<DownloadedVideoBytes, GrokError> {
|
||||
let response = self.send_request().await?;
|
||||
|
||||
let download_bytes = response.bytes()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error reading Grok create media response body: {:?}", err);
|
||||
GrokGenericApiError::WreqError(err)
|
||||
})?;
|
||||
|
||||
Ok(DownloadedVideoBytes {
|
||||
bytes: download_bytes.to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn send_request(&self) -> Result<Response, GrokError> {
|
||||
let client = Client::builder()
|
||||
.emulation(Emulation::Firefox143)
|
||||
.build()
|
||||
.map_err(|err| GrokClientError::WreqClientError(err))?;
|
||||
|
||||
let video_url = user_and_file_id_to_video_url(&self.user_id, &self.file_id, false);
|
||||
|
||||
info!("Video file URL: {}", video_url);
|
||||
|
||||
let mut request_builder = client.get(&video_url)
|
||||
.header(USER_AGENT, FIREFOX_143_MAC_USER_AGENT)
|
||||
.header(ACCEPT, "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
|
||||
.header(ACCEPT_LANGUAGE, "en-US,en;q=0.5")
|
||||
.header(ACCEPT_ENCODING, "gzip, deflate, br, zstd")
|
||||
.header(REFERER, "https://grok.com/")
|
||||
.header("Sec-GPC", "1")
|
||||
.header(CONNECTION, "keep-alive")
|
||||
.header(COOKIE, self.cookies.to_string())
|
||||
.header(UPGRADE_INSECURE_REQUESTS, "1")
|
||||
.header("sec-fetch-dest", "document")
|
||||
.header("sec-fetch-mode", "navigate")
|
||||
.header("sec-fetch-site", "same-site")
|
||||
.header("sec-fetch-user", "?1")
|
||||
.header("priority", "u=0, i");
|
||||
|
||||
if let Some(timeout) = self.request_timeout {
|
||||
request_builder = request_builder.timeout(timeout);
|
||||
}
|
||||
|
||||
let response = request_builder.send()
|
||||
.await
|
||||
.map_err(|err| GrokGenericApiError::WreqError(err))?;
|
||||
|
||||
let status = response.status();
|
||||
|
||||
// TODO: Classify errors
|
||||
if !status.is_success() {
|
||||
warn!("Not successful liking media (status: {:?})", status);
|
||||
// error!("Upload file request returned an error (code {}) : {:?}", status.as_u16(), response_body);
|
||||
// return Err(classify_general_http_status_code_and_body(status, response_body));
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::datatypes::api::file_id::FileId;
|
||||
use crate::recipes::request_client_secrets::{request_client_secrets, RequestClientSecretsArgs};
|
||||
use crate::requests::download_video_file::download_video_file::{download_video_file, DownloadVideoFileArgs};
|
||||
use crate::test_utils::get_test_cookies::get_typed_test_cookies;
|
||||
use errors::AnyhowResult;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Client side tests only
|
||||
async fn test_download() -> AnyhowResult<()> {
|
||||
//setup_test_logging(LevelFilter::Trace);
|
||||
|
||||
let cookies = get_typed_test_cookies()?;
|
||||
|
||||
let file_id = FileId("d9b300c6-9562-4e24-a87a-3ede2a53f0bc".to_string()); // Ernest
|
||||
|
||||
let secrets = request_client_secrets(RequestClientSecretsArgs {
|
||||
cookies: &cookies,
|
||||
}).await?;
|
||||
|
||||
println!("Verification Token: {:?}", secrets.verification_token);
|
||||
println!("Sentry Trace: {:?}", secrets.sentry_trace);
|
||||
println!("Numbers: {:?}", secrets.numbers);
|
||||
println!("Svg Path: {:?}", secrets.svg_path_data);
|
||||
println!("Baggage: {:?}", secrets.baggage);
|
||||
|
||||
// TDOO: This implementation is untested.
|
||||
//let download = download_video_file(DownloadVideoFileArgs {
|
||||
// cookies: cookies.as_str(),
|
||||
// user_id: &secrets.user_id,
|
||||
// file_id: &file_id,
|
||||
// request_timeout: None,
|
||||
//}).await?;
|
||||
|
||||
//println!("Video byte length: {:?}", download.bytes.len());
|
||||
|
||||
assert_eq!(1, 2);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod download_video_file;
|
||||
pub mod grok_download_video;
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
curl 'wss://grok.com/ws/imagine/listen' \
|
||||
-H 'Upgrade: websocket' \
|
||||
-H 'Origin: https://grok.com' \
|
||||
-H 'Cache-Control: no-cache' \
|
||||
-H 'Accept-Language: en-US,en;q=0.9' \
|
||||
-H 'Pragma: no-cache' \
|
||||
-b 'cf_clearance=o10zUU.x20fFOkj6tPIzI73QFT.kG3rLwdtt5P9KNzI-1760710012-1.2.1.1-q0vgGudZ42zQLs1EEEdVDkqeJGaJxkL0m60w05izUQ4NVtFNawTmElVUNYS5gOXSRnCjQkTmcMu0bI4yw3gkf.0EyfR9fE3McXK9zjwnMDZLC5MDsYazzaMA87sU4seMjd3G8oKfdi.r2pZ2rUzkoLSAJ11.q7IpHUvWsk8zcpxm1hLg9LcvfX0c.Sbcf.8mwY_32NVpIZT_0rzdp71FJUOZGabSOK4isjC409u7GOg; _ga=GA1.1.1232202746.1760710013; i18nextLng=en; x-anonuserid=621371fd-a877-4245-900c-1aa8db6039f2; x-challenge=2CHUrpA5J8%2FW2%2F1H8glEE3WKIXTKrbU7mMARWLT5DHzp5MS6nUk1aTHt7gqzv1vCxfnIdHTdeq4ZLYwcBj1DL%2FBptSSVT9OKi9DQDsN%2BKk0Ur3jg1uS%2BuPtwYSNZ%2B6CtuLCrDZqlDh%2FfFTzYr2sQ9nT3R72HfOyS%2FFXw0qvCgoROCtZCiGY%3D; x-signature=qPh93ojC8uCXOUvq7t0SzcwsyXKOE%2BRXH6dH26oL8lxwRIabYha6eniSt329QvpjBEeUqlekeStaek44mDpEYg%3D%3D; sso=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; sso-rw=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; stblid=b3331fc1-45d7-466b-83df-67427c0b2367; mp_ea93da913ddb66b6372b89d97b1029ac_mixpanel=%7B%22distinct_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%2C%22%24device_id%22%3A%2279ce237a-a0f3-4913-bf4b-519ac8a98263%22%2C%22%24initial_referrer%22%3A%22%24direct%22%2C%22%24initial_referring_domain%22%3A%22%24direct%22%2C%22__mps%22%3A%7B%7D%2C%22__mpso%22%3A%7B%7D%2C%22__mpus%22%3A%7B%7D%2C%22__mpa%22%3A%7B%7D%2C%22__mpu%22%3A%7B%7D%2C%22__mpr%22%3A%5B%5D%2C%22__mpap%22%3A%5B%5D%2C%22%24user_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%7D; _ga_8FEWB057YH=GS2.1.s1760710013$o1$g1$t1760710085$j60$l0$h0' \
|
||||
-H 'Connection: Upgrade' \
|
||||
-H 'Sec-WebSocket-Key: 3cx4uqFUq7dJfGvZsbsVEg==' \
|
||||
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36' \
|
||||
-H 'Sec-WebSocket-Version: 13' \
|
||||
-H 'Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits'
|
||||
|
||||
|
||||
--------------------------------------
|
||||
|
||||
https://stackoverflow.com/questions/47860689/how-to-read-a-websocket-response-with-curl
|
||||
|
||||
curl --include \
|
||||
--no-buffer \
|
||||
-b 'cf_clearance=o10zUU.x20fFOkj6tPIzI73QFT.kG3rLwdtt5P9KNzI-1760710012-1.2.1.1-q0vgGudZ42zQLs1EEEdVDkqeJGaJxkL0m60w05izUQ4NVtFNawTmElVUNYS5gOXSRnCjQkTmcMu0bI4yw3gkf.0EyfR9fE3McXK9zjwnMDZLC5MDsYazzaMA87sU4seMjd3G8oKfdi.r2pZ2rUzkoLSAJ11.q7IpHUvWsk8zcpxm1hLg9LcvfX0c.Sbcf.8mwY_32NVpIZT_0rzdp71FJUOZGabSOK4isjC409u7GOg; _ga=GA1.1.1232202746.1760710013; i18nextLng=en; x-anonuserid=621371fd-a877-4245-900c-1aa8db6039f2; x-challenge=2CHUrpA5J8%2FW2%2F1H8glEE3WKIXTKrbU7mMARWLT5DHzp5MS6nUk1aTHt7gqzv1vCxfnIdHTdeq4ZLYwcBj1DL%2FBptSSVT9OKi9DQDsN%2BKk0Ur3jg1uS%2BuPtwYSNZ%2B6CtuLCrDZqlDh%2FfFTzYr2sQ9nT3R72HfOyS%2FFXw0qvCgoROCtZCiGY%3D; x-signature=qPh93ojC8uCXOUvq7t0SzcwsyXKOE%2BRXH6dH26oL8lxwRIabYha6eniSt329QvpjBEeUqlekeStaek44mDpEYg%3D%3D; sso=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; sso-rw=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; stblid=b3331fc1-45d7-466b-83df-67427c0b2367; mp_ea93da913ddb66b6372b89d97b1029ac_mixpanel=%7B%22distinct_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%2C%22%24device_id%22%3A%2279ce237a-a0f3-4913-bf4b-519ac8a98263%22%2C%22%24initial_referrer%22%3A%22%24direct%22%2C%22%24initial_referring_domain%22%3A%22%24direct%22%2C%22__mps%22%3A%7B%7D%2C%22__mpso%22%3A%7B%7D%2C%22__mpus%22%3A%7B%7D%2C%22__mpa%22%3A%7B%7D%2C%22__mpu%22%3A%7B%7D%2C%22__mpr%22%3A%5B%5D%2C%22__mpap%22%3A%5B%5D%2C%22%24user_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%7D; _ga_8FEWB057YH=GS2.1.s1760710013$o1$g1$t1760710085$j60$l0$h0' \
|
||||
--header "Connection: Upgrade" \
|
||||
--header "Upgrade: websocket" \
|
||||
--header "Host: grok.com" \
|
||||
--header "Origin: https://grok.com" \
|
||||
--header "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \
|
||||
--header "Sec-WebSocket-Version: 13" \
|
||||
"https://grok.com/ws/imagine/listen"
|
||||
|
||||
-------------------------------------------
|
||||
|
||||
This turns a 401 into a 400 - it's a mix of:
|
||||
|
||||
- the above SO answer
|
||||
- "copy as curl" from Chrome"
|
||||
- curl-impersonate:
|
||||
|
||||
curl-impersonate-chrome -vvv 'wss://grok.com/ws/imagine/listen' \
|
||||
--include \
|
||||
--no-buffer \
|
||||
-H 'Connection: Upgrade' \
|
||||
-H 'Upgrade: websocket' \
|
||||
-H 'Host: grok.com' \
|
||||
-H 'Origin: https://grok.com' \
|
||||
-H 'Sec-WebSocket-Key: XjPVg9OQe7CJOH6U7lq5vw==' \
|
||||
-H 'Sec-WebSocket-Version: 13' \
|
||||
-b '_ga=GA1.1.1232202746.1760710013; i18nextLng=en; sso=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; sso-rw=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; stblid=b3331fc1-45d7-466b-83df-67427c0b2367; mp_ea93da913ddb66b6372b89d97b1029ac_mixpanel=%7B%22distinct_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%2C%22%24device_id%22%3A%2279ce237a-a0f3-4913-bf4b-519ac8a98263%22%2C%22%24initial_referrer%22%3A%22%24direct%22%2C%22%24initial_referring_domain%22%3A%22%24direct%22%2C%22__mps%22%3A%7B%7D%2C%22__mpso%22%3A%7B%7D%2C%22__mpus%22%3A%7B%7D%2C%22__mpa%22%3A%7B%7D%2C%22__mpu%22%3A%7B%7D%2C%22__mpr%22%3A%5B%5D%2C%22__mpap%22%3A%5B%5D%2C%22%24user_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%7D; cf_clearance=e7.LXFbmc.U81rZIqxuBfBe88yuBZMXEP.zwxNkbOxw-1760724394-1.2.1.1-JMHrapBGxcZriUw852.NCqwFRpZJFvoYxq.mV0jDpKCkiHBZwpdv09XJi6eFLJUYJJ6UDZ3c1eAsucVYLdWN.SvA9M6qzujj8nY4ym03PxQlMEd2OmXJogtDDJbPhA5AZEGxA39_6QQlvaBIUBnYPALYrTl9XJN_V4q3n4BXtpoBzrIJURMIn0mW3esUCGv0NukuVQrkrNtMgqT5SUmTGi0idaBYoR_2_wv4P09lsug; _ga_8FEWB057YH=GS2.1.s1760724394$o3$g1$t1760724401$j53$l0$h0' \
|
||||
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36'
|
||||
|
||||
> Sec-WebSocket-Version: 13
|
||||
> User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36
|
||||
>
|
||||
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
|
||||
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
|
||||
* old SSL session ID is stale, removing
|
||||
< HTTP/1.1 400 Bad Request
|
||||
< Date: Fri, 17 Oct 2025 18:15:29 GMT
|
||||
< Content-Length: 0
|
||||
< Connection: keep-alive
|
||||
< speculation-rules: "/cdn-cgi/speculation"
|
||||
< Server: cloudflare
|
||||
< CF-RAY: 9901c814add7452d-ATL
|
||||
* Refused WebSockets upgrade: 400
|
||||
* Closing connection 0
|
||||
* TLSv1.3 (OUT), TLS alert, close notify (256):
|
||||
curl: (22) Refused WebSockets upgrade: 400
|
||||
|
||||
Removing the cookie turns it into a Cloudflare 401.
|
||||
|
||||
--------------------------------------------
|
||||
|
||||
--include --no-buffer --header "Connection: Upgrade" \
|
||||
--header "Upgrade: websocket" \
|
||||
--header "Host: example.com" \
|
||||
--header "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \
|
||||
--header "Sec-WebSocket-Version: 13" \
|
||||
|
||||
---------------------------------------------
|
||||
|
||||
Curl script works. We needed to remove headers that curl was automatically adding (and thus we were duplicating) :
|
||||
|
||||
curl-impersonate-chrome -vvv 'wss://grok.com/ws/imagine/listen' \
|
||||
--include \
|
||||
--no-buffer \
|
||||
-H 'Connection: Upgrade' \
|
||||
-H 'Upgrade: websocket' \
|
||||
-b '_ga=GA1.1.1232202746.1760710013; i18nextLng=en; sso=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; sso-rw=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; stblid=b3331fc1-45d7-466b-83df-67427c0b2367; mp_ea93da913ddb66b6372b89d97b1029ac_mixpanel=%7B%22distinct_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%2C%22%24device_id%22%3A%2279ce237a-a0f3-4913-bf4b-519ac8a98263%22%2C%22%24initial_referrer%22%3A%22%24direct%22%2C%22%24initial_referring_domain%22%3A%22%24direct%22%2C%22__mps%22%3A%7B%7D%2C%22__mpso%22%3A%7B%7D%2C%22__mpus%22%3A%7B%7D%2C%22__mpa%22%3A%7B%7D%2C%22__mpu%22%3A%7B%7D%2C%22__mpr%22%3A%5B%5D%2C%22__mpap%22%3A%5B%5D%2C%22%24user_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%7D; cf_clearance=e7.LXFbmc.U81rZIqxuBfBe88yuBZMXEP.zwxNkbOxw-1760724394-1.2.1.1-JMHrapBGxcZriUw852.NCqwFRpZJFvoYxq.mV0jDpKCkiHBZwpdv09XJi6eFLJUYJJ6UDZ3c1eAsucVYLdWN.SvA9M6qzujj8nY4ym03PxQlMEd2OmXJogtDDJbPhA5AZEGxA39_6QQlvaBIUBnYPALYrTl9XJN_V4q3n4BXtpoBzrIJURMIn0mW3esUCGv0NukuVQrkrNtMgqT5SUmTGi0idaBYoR_2_wv4P09lsug; _ga_8FEWB057YH=GS2.1.s1760724394$o3$g1$t1760724401$j53$l0$h0' \
|
||||
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36'
|
||||
|
||||
> GET /ws/imagine/listen HTTP/1.1
|
||||
> Host: grok.com
|
||||
> Accept: */*
|
||||
> Cookie: _ga=GA1.1.1232202746.1760710013; i18nextLng=en; sso=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; sso-rw=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; stblid=b3331fc1-45d7-466b-83df-67427c0b2367; mp_ea93da913ddb66b6372b89d97b1029ac_mixpanel=%7B%22distinct_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%2C%22%24device_id%22%3A%2279ce237a-a0f3-4913-bf4b-519ac8a98263%22%2C%22%24initial_referrer%22%3A%22%24direct%22%2C%22%24initial_referring_domain%22%3A%22%24direct%22%2C%22__mps%22%3A%7B%7D%2C%22__mpso%22%3A%7B%7D%2C%22__mpus%22%3A%7B%7D%2C%22__mpa%22%3A%7B%7D%2C%22__mpu%22%3A%7B%7D%2C%22__mpr%22%3A%5B%5D%2C%22__mpap%22%3A%5B%5D%2C%22%24user_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%7D; cf_clearance=e7.LXFbmc.U81rZIqxuBfBe88yuBZMXEP.zwxNkbOxw-1760724394-1.2.1.1-JMHrapBGxcZriUw852.NCqwFRpZJFvoYxq.mV0jDpKCkiHBZwpdv09XJi6eFLJUYJJ6UDZ3c1eAsucVYLdWN.SvA9M6qzujj8nY4ym03PxQlMEd2OmXJogtDDJbPhA5AZEGxA39_6QQlvaBIUBnYPALYrTl9XJN_V4q3n4BXtpoBzrIJURMIn0mW3esUCGv0NukuVQrkrNtMgqT5SUmTGi0idaBYoR_2_wv4P09lsug; _ga_8FEWB057YH=GS2.1.s1760724394$o3$g1$t1760724401$j53$l0$h0
|
||||
> Connection: Upgrade
|
||||
> Sec-WebSocket-Version: 13
|
||||
> Sec-WebSocket-Key: JpMsHjXLPEmOsa89FQBeHA==
|
||||
> Connection: Upgrade
|
||||
> Upgrade: websocket
|
||||
> User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36
|
||||
>
|
||||
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
|
||||
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
|
||||
* old SSL session ID is stale, removing
|
||||
< HTTP/1.1 101 Switching Protocols
|
||||
< Date: Fri, 17 Oct 2025 18:56:23 GMT
|
||||
< Connection: upgrade
|
||||
< Upgrade: websocket
|
||||
< Sec-WebSocket-Accept: 67ERWy4vrrAcKi3+zna7zswEyOA=
|
||||
< CF-Ray: 990203fe38bd78ce-ATL
|
||||
< CF-Cache-Status: DYNAMIC
|
||||
< Strict-Transport-Security: max-age=31536000; includeSubDomains
|
||||
< x-trace-id: 638d9aa7a99ee6510e0fc833caecd273
|
||||
< speculation-rules: "/cdn-cgi/speculation"
|
||||
< Server: cloudflare
|
||||
* Received 101, switch to WebSocket; mask af11ea2e
|
||||
<
|
||||
|
||||
------------------------------------------------------------
|
||||
|
||||
Here are the internal headers from Wreq:
|
||||
|
||||
Execute... print headers...
|
||||
|
||||
WS Request Header: host: "grok.com"
|
||||
|
||||
WS Request Header: cookie: "_ga=GA1.1.1232202746.1760710013; i18nextLng=en; sso=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; sso-rw=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; stblid=b3331fc1-45d7-466b-83df-67427c0b2367; mp_ea93da913ddb66b6372b89d97b1029ac_mixpanel=%7B%22distinct_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%2C%22%24device_id%22%3A%2279ce237a-a0f3-4913-bf4b-519ac8a98263%22%2C%22%24initial_referrer%22%3A%22%24direct%22%2C%22%24initial_referring_domain%22%3A%22%24direct%22%2C%22__mps%22%3A%7B%7D%2C%22__mpso%22%3A%7B%7D%2C%22__mpus%22%3A%7B%7D%2C%22__mpa%22%3A%7B%7D%2C%22__mpu%22%3A%7B%7D%2C%22__mpr%22%3A%5B%5D%2C%22__mpap%22%3A%5B%5D%2C%22%24user_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%7D; cf_clearance=e7.LXFbmc.U81rZIqxuBfBe88yuBZMXEP.zwxNkbOxw-1760724394-1.2.1.1-JMHrapBGxcZriUw852.NCqwFRpZJFvoYxq.mV0jDpKCkiHBZwpdv09XJi6eFLJUYJJ6UDZ3c1eAsucVYLdWN.SvA9M6qzujj8nY4ym03PxQlMEd2OmXJogtDDJbPhA5AZEGxA39_6QQlvaBIUBnYPALYrTl9XJN_V4q3n4BXtpoBzrIJURMIn0mW3esUCGv0NukuVQrkrNtMgqT5SUmTGi0idaBYoR_2_wv4P09lsug; _ga_8FEWB057YH=GS2.1.s1760724394$o3$g1$t1760724401$j53$l0$h0"
|
||||
WS Request Header: connection: "upgrade"
|
||||
WS Request Header: upgrade: "websocket"
|
||||
WS Request Header: sec-websocket-version: "13"
|
||||
WS Request Header: sec-websocket-key: "Dh44njJrey+qIso3X1IdhA=="
|
||||
|
||||
|
||||
WS Request Header: user-agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36"
|
||||
Response version: HTTP/1.1
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
Here are some potential ways we might be caught up in a WAF rule.
|
||||
|
||||
The other alternative is that it's some heuristic, potentially related to the handshake.
|
||||
|
||||
Dropdown
|
||||
-----------
|
||||
|
||||
cookie - string matching tools
|
||||
Cookie value of [name] [operator - string matching tools] [value]
|
||||
Referer - string matching tools
|
||||
Request method - equals / does not equal / in - strict list of [GET, POST, PURGE, PUT, HEAD, OPTIONS, DELETE, PATCH]
|
||||
Hostname - string matching tools
|
||||
SSL/HTTPS - checkbox
|
||||
HTTP Version - equals / does not equal / in - strict list of [HTTP/1.0, HTTP/1.1, HTTP/1.2, HTTP/2, HTTP/3, SPDY/3.1]
|
||||
User agent - string matching tools
|
||||
Client certificate verified - checkbox
|
||||
Header - map for any header name + string matching tools
|
||||
Fallthrough detected - checkbox
|
||||
X-Forwarded-For - string matching tools
|
||||
|
||||
URI, Path, Query, etc.
|
||||
|
||||
WAF Attack Score Class - Attack, Likely Attack, Likely Clean, Clean
|
||||
Disposable email check - bool
|
||||
User and password leaked - bool
|
||||
Password Leaked - bool
|
||||
|
||||
|
||||
Expressions
|
||||
--------------
|
||||
|
||||
cookie:
|
||||
(http.cookie wildcard r"asdf=foo*")
|
||||
|
||||
Cookie value of:
|
||||
(any(http.request.cookies[""][*] matches ".*"))
|
||||
|
||||
Referer:
|
||||
(http.referer eq "https://example.com")
|
||||
|
||||
Hostname:
|
||||
(http.host eq "foo.com")
|
||||
|
||||
SSL/HTTPS:
|
||||
(ssl) - strictly on
|
||||
(not ssl) - strictly off
|
||||
|
||||
HTTP Version:
|
||||
(http.request.version eq "HTTP/1.0")
|
||||
|
||||
User Agent:
|
||||
(http.user_agent wildcard r"Mozilla/*")
|
||||
|
||||
Client Certificate Verified:
|
||||
(cf.tls_client_auth.cert_verified) - check on
|
||||
|
||||
Header:
|
||||
(any(http.request.headers["header-name"][*] eq "foo"))
|
||||
|
||||
Fallthrough detected:
|
||||
(cf.api_gateway.fallthrough_detected)
|
||||
|
||||
X-Forwarded-For:
|
||||
(http.x_forwarded_for wildcard r"192.0.*")
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
Chrome Raw Request Headers
|
||||
==========================
|
||||
|
||||
GET wss://grok.com/ws/imagine/listen HTTP/1.1
|
||||
Host: grok.com
|
||||
Connection: Upgrade
|
||||
Pragma: no-cache
|
||||
Cache-Control: no-cache
|
||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36
|
||||
Upgrade: websocket
|
||||
Origin: https://grok.com
|
||||
Sec-WebSocket-Version: 13
|
||||
Accept-Encoding: gzip, deflate, br, zstd
|
||||
Accept-Language: en-US,en;q=0.9
|
||||
Cookie: cf_clearance=o10zUU.x20fFOkj6tPIzI73QFT.kG3rLwdtt5P9KNzI-1760710012-1.2.1.1-q0vgGudZ42zQLs1EEEdVDkqeJGaJxkL0m60w05izUQ4NVtFNawTmElVUNYS5gOXSRnCjQkTmcMu0bI4yw3gkf.0EyfR9fE3McXK9zjwnMDZLC5MDsYazzaMA87sU4seMjd3G8oKfdi.r2pZ2rUzkoLSAJ11.q7IpHUvWsk8zcpxm1hLg9LcvfX0c.Sbcf.8mwY_32NVpIZT_0rzdp71FJUOZGabSOK4isjC409u7GOg; _ga=GA1.1.1232202746.1760710013; i18nextLng=en; x-anonuserid=621371fd-a877-4245-900c-1aa8db6039f2; x-challenge=2CHUrpA5J8%2FW2%2F1H8glEE3WKIXTKrbU7mMARWLT5DHzp5MS6nUk1aTHt7gqzv1vCxfnIdHTdeq4ZLYwcBj1DL%2FBptSSVT9OKi9DQDsN%2BKk0Ur3jg1uS%2BuPtwYSNZ%2B6CtuLCrDZqlDh%2FfFTzYr2sQ9nT3R72HfOyS%2FFXw0qvCgoROCtZCiGY%3D; x-signature=qPh93ojC8uCXOUvq7t0SzcwsyXKOE%2BRXH6dH26oL8lxwRIabYha6eniSt329QvpjBEeUqlekeStaek44mDpEYg%3D%3D; sso=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; sso-rw=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; stblid=b3331fc1-45d7-466b-83df-67427c0b2367; mp_ea93da913ddb66b6372b89d97b1029ac_mixpanel=%7B%22distinct_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%2C%22%24device_id%22%3A%2279ce237a-a0f3-4913-bf4b-519ac8a98263%22%2C%22%24initial_referrer%22%3A%22%24direct%22%2C%22%24initial_referring_domain%22%3A%22%24direct%22%2C%22__mps%22%3A%7B%7D%2C%22__mpso%22%3A%7B%7D%2C%22__mpus%22%3A%7B%7D%2C%22__mpa%22%3A%7B%7D%2C%22__mpu%22%3A%7B%7D%2C%22__mpr%22%3A%5B%5D%2C%22__mpap%22%3A%5B%5D%2C%22%24user_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%7D; _ga_8FEWB057YH=GS2.1.s1760710013$o1$g1$t1760710085$j60$l0$h0
|
||||
Sec-WebSocket-Key: 3cx4uqFUq7dJfGvZsbsVEg==
|
||||
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
|
||||
|
||||
|
||||
Firefox Raw Request Headers
|
||||
===========================
|
||||
|
||||
GET /ws/imagine/listen HTTP/1.1
|
||||
Host: grok.com
|
||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:144.0) Gecko/20100101 Firefox/144.0
|
||||
Accept: */*
|
||||
Accept-Language: en-US,en;q=0.5
|
||||
Accept-Encoding: gzip, deflate, br, zstd
|
||||
Sec-WebSocket-Version: 13
|
||||
Origin: https://grok.com
|
||||
Sec-WebSocket-Extensions: permessage-deflate
|
||||
Sec-WebSocket-Key: v901qtQaweRjQflydUViHw==
|
||||
Sec-GPC: 1
|
||||
Connection: keep-alive, Upgrade
|
||||
Cookie: stblid=dea257e3-e71c-4214-88b8-11b0cac7152e; cf_clearance=4.JXCkTU2iJUEoDH1QieG_4ZyUsFLDHUdbAniQePfbk-1760685075-1.2.1.1-sPbWPHzZDlOVbMNiUrLrH9pmBI6te.Zwvy2IBz08XRe4FN8n7RaMsM6tnFJxV7bU4EhC53RPF4zvaGJTJeNicR.ETBJa9IWTEvnIYNLofI778oq.hNEn7mTi4Jh8x.vnQvdxdhmbzCase_MwGVMbDLe3YiHuP5NhCEvML1s61yt_0l00XTmo89le_7UGvLd2xUYaE2iYwSGwakyrJvNUp2Bs7GlLEbe2Agugi4TH_7w; i18nextLng=en; x-anonuserid=636cd954-e8e0-4592-aa72-9c8337b0b481; x-challenge=GZHDBobMfJjwZAjHkPRIUjkptE4EhMI%2FtZQCIUlbQ24JpOZNB80jAFPUVuqH1V33VQqf%2FI8E%2Bf0zvIgnkshHQbqMbRz1cidQa4fhmjCHVceIGQ%2BBFXME6z2JL6e%2FNJUFfa1gdzy6OC%2Bn%2FPVFY06KuZ5O445m2u5JTNnwJ8HyqRnNXUOhncA%3D; x-signature=ALNyXb5BnhvGpXzo2lSb64bZEjWrJRwDpWQYxacDa5wRp34HDP6tPsWkEk2Oe2DZSQtzkZSAwZmihrSf%2BnO9Vw%3D%3D; sso=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiNjllNjhlMzQtM2RhNS00NTg0LTljMWMtODQ2OGYwZDMzMzM4In0.9DoSVTs4BEfPbO1oX72qMoRcM8vVUZ9Y461T2i5l_DQ; sso-rw=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiNjllNjhlMzQtM2RhNS00NTg0LTljMWMtODQ2OGYwZDMzMzM4In0.9DoSVTs4BEfPbO1oX72qMoRcM8vVUZ9Y461T2i5l_DQ; mp_ea93da913ddb66b6372b89d97b1029ac_mixpanel=%7B%22distinct_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%2C%22%24device_id%22%3A%22b231bb76-612b-402a-896b-8d53fe0654e1%22%2C%22%24initial_referrer%22%3A%22%24direct%22%2C%22%24initial_referring_domain%22%3A%22%24direct%22%2C%22__mps%22%3A%7B%7D%2C%22__mpso%22%3A%7B%7D%2C%22__mpus%22%3A%7B%7D%2C%22__mpa%22%3A%7B%7D%2C%22__mpu%22%3A%7B%7D%2C%22__mpr%22%3A%5B%5D%2C%22__mpap%22%3A%5B%5D%2C%22%24user_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%7D
|
||||
Sec-Fetch-Dest: empty
|
||||
Sec-Fetch-Mode: websocket
|
||||
Sec-Fetch-Site: same-origin
|
||||
Pragma: no-cache
|
||||
Cache-Control: no-cache
|
||||
Upgrade: websocket
|
||||
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
Server Connection:
|
||||
Address grok.com:443
|
||||
Resolved Address 104.18.28.234:443
|
||||
HTTP Version HTTP/1.1
|
||||
ALPN http/1.1
|
||||
Server Certificate:
|
||||
Type EC (secp256r1), 256 bits
|
||||
SHA256 digest bb 6e e7 1f 26 4c ea 10 5c a4 26 c1 93 3d f3 99 c5 a3 c9 e8 a6 e3 d3 03 e4 44 66 44 65 c1 3f 47
|
||||
Valid from 2025-09-25 19:14:19+00:00
|
||||
Valid to 2025-12-24 20:14:16+00:00
|
||||
Serial 291992553975709925923951507305721322045
|
||||
Subject CN grok.com
|
||||
Issuer C US
|
||||
O Google Trust Services
|
||||
CN WE1
|
||||
Alt names grok.com, typeahead.grok.com
|
||||
Client Connection:
|
||||
Address 127.0.0.1:63171
|
||||
HTTP Version HTTP/1.1
|
||||
TLS Version TLSv1.3
|
||||
Server Name Indication grok.com
|
||||
Cipher Name TLS_AES_256_GCM_SHA384
|
||||
ALPN http/1.1
|
||||
Timing:
|
||||
Client conn. established 2025-10-18 01:02:06.884
|
||||
Server conn. initiated 2025-10-18 01:02:06.888
|
||||
Server conn. TCP handshake 2025-10-18 01:02:06.899
|
||||
Server conn. TLS handshake 2025-10-18 01:02:06.923
|
||||
Client conn. TLS handshake 2025-10-18 01:02:06.925
|
||||
First request byte 2025-10-18 01:02:06.925
|
||||
Request complete 2025-10-18 01:02:06.926
|
||||
First response byte 2025-10-18 01:02:07.256
|
||||
Response complete 2025-10-18 01:02:07.263
|
||||
Client conn. closed –
|
||||
Server conn. closed –
|
||||
|
||||
TODO: Now try this with wreq and see what the differences are.
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
Host: grok.com
|
||||
Connection: Upgrade
|
||||
Pragma: no-cache
|
||||
Cache-Control: no-cache
|
||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36
|
||||
Upgrade: websocket
|
||||
Origin: https://grok.com
|
||||
Sec-WebSocket-Version: 13
|
||||
Accept-Encoding: gzip, deflate, br, zstd
|
||||
Accept-Language: en-US,en;q=0.9
|
||||
Cookie: _ga=GA1.1.1232202746.1760710013; i18nextLng=en; sso=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTct
|
||||
OWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; sso-rw=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzct
|
||||
OTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; stblid=b3331fc1-45d7-466b-83df-67427c0b2367; cf_clearanc
|
||||
e=pHyVLkkYV0wHKuAHOqhI3paeuJoM5VqHFXksRO.5poo-1760763724-1.2.1.1-uNM2i.EbSArwG0hMIz04Y3yMvxMnHMeuXgGbK_qdNGRB1ElLRJhO8pO5xs9zd1ZiR9NYLQmk5Z8Z
|
||||
BZazkIbsd0bHowJ06XjzcvWTDREB_enubIV82nhnkH_wVm2Cb9lQisSp0nAEeG7UubY4202XcmS3cDAiK_ihEXOI1zCiZdMC0JDgVThOU6HWunJrAiVkAQ880dhb.MmiJ2Lrs.8T1UdJQ
|
||||
WmE.DXmWja3dyUxKmU; mp_ea93da913ddb66b6372b89d97b1029ac_mixpanel=%7B%22distinct_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%2C%22%24de
|
||||
vice_id%22%3A%2279ce237a-a0f3-4913-bf4b-519ac8a98263%22%2C%22%24initial_referrer%22%3A%22%24direct%22%2C%22%24initial_referring_domain%22%3A%
|
||||
22%24direct%22%2C%22__mps%22%3A%7B%7D%2C%22__mpso%22%3A%7B%7D%2C%22__mpus%22%3A%7B%7D%2C%22__mpa%22%3A%7B%7D%2C%22__mpu%22%3A%7B%7D%2C%22__mp
|
||||
r%22%3A%5B%5D%2C%22__mpap%22%3A%5B%5D%2C%22%24user_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%7D;
|
||||
_ga_8FEWB057YH=GS2.1.s1760763724$o4$g1$t1760763725$j59$l0$h0
|
||||
Sec-WebSocket-Key: RB4Awj8U5qcXxoQfKQPVtQ==
|
||||
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user