Compare commits

...
12 Commits
61 changed files with 1327 additions and 254 deletions
+107
View File
@@ -0,0 +1,107 @@
name: "Artcraft Publish"
on:
push:
branches:
- release
- artcraft-release
# This workflow creates Universal macOS builds with signing certificate support.
# On each push to the release branches it will create or update a GitHub release, build your app, and upload the artifacts to the release.
jobs:
publish-artcraft:
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
# Universal macOS builds (arm64 and x86_64)
- platform: 'macos-latest'
args: '--target universal-apple-darwin'
# Uncomment below for additional platforms
# - platform: 'ubuntu-22.04'
# args: ''
# - platform: 'windows-latest'
# args: ''
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- name: setup node
uses: actions/setup-node@v4
with:
node-version: lts/*
- name: install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
- name: install frontend dependencies
run: |
cd ./crates/desktop/artcraft
yarn install
- name: import Apple Developer Certificate
if: matrix.platform == 'macos-latest'
# Prevents keychain from locking automatically for 3600 seconds.
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security set-keychain-settings -t 3600 -u build.keychain
security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
security find-identity -v -p codesigning build.keychain
- name: verify certificate
if: matrix.platform == 'macos-latest'
run: |
echo "Available certificates:"
security find-identity -v -p codesigning build.keychain
# Try multiple certificate patterns
CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep -E "(Developer ID Application|Apple Development|Storyteller Developer)" | head -1)
if [ -z "$CERT_INFO" ]; then
echo "No matching certificate found. Using first available certificate."
CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep -v "Policy" | head -1)
fi
CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV
echo "Using certificate: $CERT_ID"
- name: build and publish
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }}
with:
projectPath: ./crates/desktop/artcraft
tagName: artcraft-v__VERSION__ # the action automatically replaces __VERSION__ with the app version.
releaseName: "Artcraft v__VERSION__"
releaseBody: "See the assets to download this version and install."
releaseDraft: true
prerelease: false
args: ${{ matrix.args }}
@@ -16,6 +16,9 @@
"allow": [
{
"url": "https://*"
},
{
"url": "http://localhost:12345"
}
]
},
@@ -1,10 +1,13 @@
use crate::core::commands::enqueue::image::handle_image_artcraft::handle_image_artcraft;
use crate::core::commands::enqueue::image::handle_image_fal::handle_image_fal;
use crate::core::commands::enqueue::image::handle_image_sora::handle_image_sora;
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
use crate::core::commands::enqueue::object::handle_object_artcraft::handle_object_artcraft;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use crate::core::commands::response::shorthand::Response;
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
use crate::core::events::sendable_event_trait::SendableEvent;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use crate::core::utils::download_media_file_to_temp_dir::download_media_file_to_temp_dir;
@@ -55,8 +58,6 @@ use storyteller_client::media_files::upload_image_media_file_from_file::upload_i
use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, Emitter, Manager, State};
use tempfile::NamedTempFile;
use crate::core::commands::enqueue::image::handle_image_artcraft::handle_image_artcraft;
use crate::core::commands::enqueue::object::handle_object_artcraft::handle_object_artcraft;
#[derive(Deserialize)]
pub struct EnqueueTextToImageRequest {
@@ -70,8 +71,17 @@ pub struct EnqueueTextToImageRequest {
#[derive(Deserialize, Debug, Copy, Clone)]
#[serde(rename_all = "snake_case")]
pub enum EnqueueTextToImageModel {
#[serde(rename = "flux_1_dev")]
Flux1Dev,
#[serde(rename = "flux_1_schnell")]
Flux1Schnell,
#[deprecated(note="use `flux_pro_11_ultra` instead")]
#[serde(rename = "flux_pro_ultra")]
FluxProUltra,
#[serde(rename = "flux_pro_11")]
FluxPro11,
#[serde(rename = "flux_pro_11_ultra")]
FluxPro11Ultra,
#[serde(rename = "gpt_image_1")]
GptImage1,
#[serde(rename = "recraft_3")]
@@ -105,6 +115,7 @@ pub async fn enqueue_text_to_image_command(
app: AppHandle,
request: EnqueueTextToImageRequest,
app_data_root: State<'_, AppDataRoot>,
app_env_configs: State<'_, AppEnvConfigs>,
fal_creds_manager: State<'_, FalCredentialManager>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
fal_task_queue: State<'_, FalTaskQueue>,
@@ -120,6 +131,7 @@ pub async fn enqueue_text_to_image_command(
&app_data_root,
&fal_creds_manager,
&storyteller_creds_manager,
&app_env_configs,
&fal_task_queue,
&sora_creds_manager,
&sora_task_queue,
@@ -167,6 +179,7 @@ pub async fn handle_request(
app_data_root: &AppDataRoot,
fal_creds_manager: &FalCredentialManager,
storyteller_creds_manager: &StorytellerCredentialManager,
app_env_configs: &AppEnvConfigs,
fal_task_queue: &FalTaskQueue,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
@@ -188,7 +201,7 @@ pub async fn handle_request(
if fal_creds_manager.has_apparent_api_token()? {
handle_image_fal(&app, request, fal_creds_manager, fal_task_queue).await?;
} else {
handle_image_artcraft(request, &app, app_data_root, storyteller_creds_manager).await?;
handle_image_artcraft(request, &app, app_env_configs, app_data_root, storyteller_creds_manager).await?;
}
Ok(())
@@ -4,6 +4,7 @@ use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::utils::download_media_file_to_temp_dir::download_media_file_to_temp_dir;
use crate::core::utils::save_base64_image_to_temp_dir::save_base64_image_to_temp_dir;
@@ -13,8 +14,11 @@ use crate::services::sora::state::sora_credential_manager::SoraCredentialManager
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use anyhow::anyhow;
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageRequest;
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageRequest;
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageRequest;
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageRequest;
use artcraft_api_defs::generate::object::generate_hunyuan_2_image_to_3d::GenerateHunyuan2ImageTo3dRequest;
use artcraft_api_defs::generate::object::generate_hunyuan_2_0_image_to_3d::GenerateHunyuan20ImageTo3dRequest;
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProImageToVideoRequest;
use fal_client::creds::fal_api_key::FalApiKey;
use fal_client::requests::queue::enqueue_hunyuan2_image_to_3d::{enqueue_hunyuan2_image_to_3d, Hunyuan2Args};
@@ -22,8 +26,11 @@ use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_im
use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs};
use idempotency::uuid::generate_random_uuid;
use log::{error, info, warn};
use storyteller_client::generate::image::generate_flux_1_dev_text_to_image::generate_flux_1_dev_text_to_image;
use storyteller_client::generate::image::generate_flux_1_schnell_text_to_image::generate_flux_1_schnell_text_to_image;
use storyteller_client::generate::image::generate_flux_pro_11_text_to_image::generate_flux_pro_11_text_to_image;
use storyteller_client::generate::image::generate_flux_pro_11_ultra_text_to_image::generate_flux_pro_11_ultra_text_to_image;
use storyteller_client::generate::object::generate_hunyuan2_image_to_3d::generate_hunyuan2_image_to_3d;
use storyteller_client::generate::object::generate_hunyuan_3d_2_0_image_to_3d::generate_hunyuan3d_2_0_image_to_3d;
use storyteller_client::generate::video::generate_kling_16_pro_image_to_video::generate_kling_16_pro_image_to_video;
use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, State};
@@ -31,6 +38,7 @@ use tauri::{AppHandle, State};
pub async fn handle_image_artcraft(
request: EnqueueTextToImageRequest,
app: &AppHandle,
app_env_configs: &AppEnvConfigs,
app_data_root: &AppDataRoot,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<(), InternalImageError> {
@@ -66,8 +74,86 @@ pub async fn handle_image_artcraft(
Some(EnqueueTextToImageModel::Recraft3) => {
return Err(InternalImageError::AnyhowError(anyhow!("not yet implemented in Artcraft")));
}
Some(EnqueueTextToImageModel::FluxProUltra) => {
info!("enqueue Flux Pro Ultra");
Some(EnqueueTextToImageModel::Flux1Dev) => {
info!("enqueue Flux 1 Dev");
selected_model = Some(GenerationModel::Flux1Dev);
let request = GenerateFlux1DevTextToImageRequest {
uuid_idempotency_token,
prompt: request.prompt,
aspect_ratio: None,
num_images: None,
};
let result = generate_flux_1_dev_text_to_image(
&app_env_configs.storyteller_host,
Some(&creds),
request,
).await;
match result {
Ok(enqueued) => {
info!("Successfully enqueued Artcraft Flux 1 Dev text to image generation");
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft Flux 1 Dev text to image generation: {:?}", err);
return Err(InternalImageError::StorytellerError(err));
}
}
}
Some(EnqueueTextToImageModel::Flux1Schnell) => {
info!("enqueue Flux 1 Schnell");
selected_model = Some(GenerationModel::Flux1Schnell);
let request = GenerateFlux1SchnellTextToImageRequest {
uuid_idempotency_token,
prompt: request.prompt,
aspect_ratio: None,
num_images: None,
};
let result = generate_flux_1_schnell_text_to_image(
&app_env_configs.storyteller_host,
Some(&creds),
request,
).await;
match result {
Ok(enqueued) => {
info!("Successfully enqueued Artcraft Flux 1 Schnell text to image generation");
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft Flux 1 Schnell text to image generation: {:?}", err);
return Err(InternalImageError::StorytellerError(err));
}
}
}
Some(EnqueueTextToImageModel::FluxPro11) => {
info!("enqueue Flux Pro 1.1");
selected_model = Some(GenerationModel::FluxPro11);
let request = GenerateFluxPro11TextToImageRequest {
uuid_idempotency_token,
prompt: request.prompt,
aspect_ratio: None,
num_images: None,
};
let result = generate_flux_pro_11_text_to_image(
&app_env_configs.storyteller_host,
Some(&creds),
request,
).await;
match result {
Ok(enqueued) => {
info!("Successfully enqueued Artcraft Flux Pro 1.1 text to image generation");
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft Flux Pro 1.1 text to image generation: {:?}", err);
return Err(InternalImageError::StorytellerError(err));
}
}
}
Some(
EnqueueTextToImageModel::FluxProUltra |
EnqueueTextToImageModel::FluxPro11Ultra
) => {
info!("enqueue Flux Pro 1.1 Ultra");
selected_model = Some(GenerationModel::FluxPro11Ultra);
let request = GenerateFluxPro11UltraTextToImageRequest {
uuid_idempotency_token,
@@ -76,17 +162,17 @@ pub async fn handle_image_artcraft(
num_images: None,
};
let result = generate_flux_pro_11_ultra_text_to_image(
&ApiHost::Storyteller,
&app_env_configs.storyteller_host,
Some(&creds),
request,
).await;
match result {
Ok(enqueued) => {
info!("Successfully enqueued Artcraft flux pro ultra text to image generation");
info!("Successfully enqueued Artcraft Flux Pro 1.1 Ultra text to image generation");
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft flux pro ultra text to image generation: {:?}", err);
error!("Failed to use Artcraft Flux Pro 1.1 Ultra text to image generation: {:?}", err);
return Err(InternalImageError::StorytellerError(err));
}
}
@@ -54,8 +54,18 @@ pub async fn handle_image_fal(
Some(EnqueueTextToImageModel::GptImage1) => {
return Err(InternalImageError::AnyhowError(anyhow!("wrong logic: fal is handling sora images")));
}
Some(EnqueueTextToImageModel::FluxProUltra) => {
info!("enqueue Flux Pro Ultra text-to-image with prompt: {}", prompt);
Some(
EnqueueTextToImageModel::Flux1Dev |
EnqueueTextToImageModel::Flux1Schnell |
EnqueueTextToImageModel::FluxPro11
) => {
return Err(InternalImageError::AnyhowError(anyhow!("not yet implemented: {:?}", request.model)));
}
Some(
EnqueueTextToImageModel::FluxProUltra |
EnqueueTextToImageModel::FluxPro11Ultra
) => {
info!("enqueue Flux Pro 1.1 Ultra text-to-image with prompt: {}", prompt);
enqueue_flux_pro_11_ultra_text_to_image(FluxPro11UltraTextToImageArgs {
prompt,
api_key: &api_key,
@@ -70,8 +70,13 @@ pub struct EnqueueImageTo3dObjectRequest {
#[derive(Deserialize, Debug, Copy, Clone)]
#[serde(rename_all = "snake_case")]
pub enum EnqueueImageTo3dObjectModel {
#[deprecated(note="Use `hunyuan_3d_2_0` instead")]
#[serde(rename = "hunyuan_3d_2")]
Hunyuan3d2,
#[serde(rename = "hunyuan_3d_2_0")]
Hunyuan3d2_0,
#[serde(rename = "hunyuan_3d_2_1")]
Hunyuan3d2_1,
}
#[derive(Serialize)]
@@ -14,16 +14,18 @@ use crate::services::sora::state::sora_credential_manager::SoraCredentialManager
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use anyhow::anyhow;
use artcraft_api_defs::generate::object::generate_hunyuan_2_image_to_3d::GenerateHunyuan2ImageTo3dRequest;
use artcraft_api_defs::generate::object::generate_hunyuan_2_0_image_to_3d::GenerateHunyuan20ImageTo3dRequest;
use fal_client::creds::fal_api_key::FalApiKey;
use fal_client::requests::queue::enqueue_hunyuan2_image_to_3d::{enqueue_hunyuan2_image_to_3d, Hunyuan2Args};
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs};
use idempotency::uuid::generate_random_uuid;
use log::{error, info, warn};
use storyteller_client::generate::object::generate_hunyuan2_image_to_3d::generate_hunyuan2_image_to_3d;
use storyteller_client::generate::object::generate_hunyuan_3d_2_0_image_to_3d::generate_hunyuan3d_2_0_image_to_3d;
use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, State};
use artcraft_api_defs::generate::object::generate_hunyuan_2_1_image_to_3d::GenerateHunyuan21ImageTo3dRequest;
use storyteller_client::generate::object::generate_hunyuan_3d_2_1_image_to_3d::generate_hunyuan3d_2_1_image_to_3d;
pub async fn handle_object_artcraft(
request: EnqueueImageTo3dObjectRequest,
@@ -63,13 +65,16 @@ pub async fn handle_object_artcraft(
None => {
return Err(InternalObjectError::NoModelSpecified);
}
Some(EnqueueImageTo3dObjectModel::Hunyuan3d2) => {
info!("enqueue Hunyuan 3D 2.0");
let request = GenerateHunyuan2ImageTo3dRequest {
Some(
EnqueueImageTo3dObjectModel::Hunyuan3d2 |
EnqueueImageTo3dObjectModel::Hunyuan3d2_0
) => {
info!("enqueue Artcraft Hunyuan 3D 2.0");
let request = GenerateHunyuan20ImageTo3dRequest {
uuid_idempotency_token,
media_file_token: request.image_media_token,
};
let result = generate_hunyuan2_image_to_3d(
let result = generate_hunyuan3d_2_0_image_to_3d(
&ApiHost::Storyteller,
Some(&creds),
request,
@@ -85,6 +90,28 @@ pub async fn handle_object_artcraft(
}
}
}
Some(EnqueueImageTo3dObjectModel::Hunyuan3d2_1) => {
info!("enqueue Artcraft Hunyuan 3D 2.1");
let request = GenerateHunyuan21ImageTo3dRequest {
uuid_idempotency_token,
media_file_token: request.image_media_token,
};
let result = generate_hunyuan3d_2_1_image_to_3d(
&ApiHost::Storyteller,
Some(&creds),
request,
).await;
match result {
Ok(enqueued) => {
info!("Successfully enqueued Artcraft Hunyuan 3D 2.1");
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft Hunyuan 3D 2.1: {:?}", err);
return Err(InternalObjectError::StorytellerError(err));
}
}
}
};
info!("Successfully enqueued 3D generation");
@@ -67,13 +67,19 @@ pub async fn handle_object_fal(
None => {
return Err(InternalObjectError::NoModelSpecified);
}
Some(EnqueueImageTo3dObjectModel::Hunyuan3d2) => {
Some(
EnqueueImageTo3dObjectModel::Hunyuan3d2_0 |
EnqueueImageTo3dObjectModel::Hunyuan3d2
) => {
info!("enqueue Hunyuan 3D 2.0");
enqueue_hunyuan2_image_to_3d(Hunyuan2Args {
image_path: filename,
api_key: &api_key,
}).await
}
_ => {
return Err(InternalObjectError::AnyhowError(anyhow!("Wrong model specified: {:?}", request.model)));
}
};
match result {
@@ -7,6 +7,7 @@ use crate::core::commands::response::failure_response_wrapper::{CommandErrorResp
use crate::core::commands::response::shorthand::Response;
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
use crate::core::events::sendable_event_trait::SendableEvent;
use crate::core::model::video_models::VideoModel;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use crate::core::utils::download_media_file_to_temp_dir::download_media_file_to_temp_dir;
@@ -66,14 +67,7 @@ pub struct EnqueueImageToVideoRequest {
pub image_media_token: Option<MediaFileToken>,
/// The model to use.
pub model: Option<EnqueueImageToVideoModel>,
}
#[derive(Deserialize, Debug, Copy, Clone)]
#[serde(rename_all = "snake_case")]
pub enum EnqueueImageToVideoModel {
#[serde(rename = "kling1_6")]
Kling16,
pub model: Option<VideoModel>,
}
#[derive(Serialize)]
@@ -1,11 +1,12 @@
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageModel, EnqueueTextToImageRequest};
use crate::core::commands::enqueue::object::enqueue_image_to_3d_object_command::{EnqueueImageTo3dObjectModel, EnqueueImageTo3dObjectRequest};
use crate::core::commands::enqueue::video::enqueue_image_to_video_command::{EnqueueImageToVideoModel, EnqueueImageToVideoRequest};
use crate::core::commands::enqueue::video::enqueue_image_to_video_command::EnqueueImageToVideoRequest;
use crate::core::commands::enqueue::video::internal_video_error::InternalVideoError;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::core::model::video_models::VideoModel;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::utils::download_media_file_to_temp_dir::download_media_file_to_temp_dir;
use crate::core::utils::save_base64_image_to_temp_dir::save_base64_image_to_temp_dir;
@@ -15,17 +16,26 @@ use crate::services::sora::state::sora_credential_manager::SoraCredentialManager
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use anyhow::anyhow;
use artcraft_api_defs::generate::object::generate_hunyuan_2_image_to_3d::GenerateHunyuan2ImageTo3dRequest;
use artcraft_api_defs::generate::object::generate_hunyuan_2_0_image_to_3d::GenerateHunyuan20ImageTo3dRequest;
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::{GenerateKling16ProAspectRatio, GenerateKling16ProImageToVideoRequest};
use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::{GenerateKling21MasterAspectRatio, GenerateKling21MasterImageToVideoRequest};
use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::{GenerateKling21ProAspectRatio, GenerateKling21ProImageToVideoRequest};
use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteImageToVideoRequest;
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::{GenerateVeo2AspectRatio, GenerateVeo2ImageToVideoRequest};
use fal_client::creds::fal_api_key::FalApiKey;
use fal_client::requests::queue::enqueue_hunyuan2_image_to_3d::{enqueue_hunyuan2_image_to_3d, Hunyuan2Args};
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs};
use fal_client::requests::queue::video_gen::enqueue_kling_16_pro_image_to_video::Kling16ProAspectRatio;
use fal_client::requests::webhook::video::enqueue_veo_2_image_to_video_webhook::Veo2AspectRatio;
use idempotency::uuid::generate_random_uuid;
use log::{error, info, warn};
use storyteller_client::generate::object::generate_hunyuan2_image_to_3d::generate_hunyuan2_image_to_3d;
use storyteller_client::generate::object::generate_hunyuan_3d_2_0_image_to_3d::generate_hunyuan3d_2_0_image_to_3d;
use storyteller_client::generate::video::generate_kling_16_pro_image_to_video::generate_kling_16_pro_image_to_video;
use storyteller_client::generate::video::generate_kling_21_master_image_to_video::generate_kling_21_master_image_to_video;
use storyteller_client::generate::video::generate_kling_21_pro_image_to_video::generate_kling_21_pro_image_to_video;
use storyteller_client::generate::video::generate_seedance_1_0_lite_image_to_video::generate_seedance_1_0_lite_image_to_video;
use storyteller_client::generate::video::generate_veo_2_image_to_video::generate_veo_2_image_to_video;
use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, State};
@@ -69,8 +79,8 @@ pub async fn handle_video_artcraft(
None => {
return Err(InternalVideoError::NoModelSpecified);
}
Some(EnqueueImageToVideoModel::Kling16) => {
info!("enqueue Kling 1.6");
Some(VideoModel::Kling16Pro) => {
info!("enqueue Kling 1.6 Pro with Artcraft API");
selected_model = Some(GenerationModel::Kling1_6);
let request = GenerateKling16ProImageToVideoRequest {
uuid_idempotency_token,
@@ -95,6 +105,111 @@ pub async fn handle_video_artcraft(
}
}
}
Some(VideoModel::Kling21Master) => {
info!("enqueue Kling 2.1 Master with Artcraft API");
selected_model = Some(GenerationModel::Kling21Master);
let request = GenerateKling21MasterImageToVideoRequest {
uuid_idempotency_token,
media_file_token: request.image_media_token,
aspect_ratio: Some(GenerateKling21MasterAspectRatio::WideSixteenNine),
prompt: None,
duration: None,
};
let result = generate_kling_21_master_image_to_video(
&ApiHost::Storyteller,
Some(&creds),
request,
).await;
match result {
Ok(enqueued) => {
info!("Successfully enqueued Artcraft Kling 2.1 Master video generation");
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft Kling 2.1 Master video generation: {:?}", err);
return Err(InternalVideoError::StorytellerError(err));
}
}
}
Some(VideoModel::Kling21Pro) => {
info!("enqueue Kling 2.1 Pro with Artcraft API");
selected_model = Some(GenerationModel::Kling21Pro);
let request = GenerateKling21ProImageToVideoRequest {
uuid_idempotency_token,
media_file_token: request.image_media_token,
aspect_ratio: Some(GenerateKling21ProAspectRatio::WideSixteenNine),
prompt: None,
duration: None,
};
let result = generate_kling_21_pro_image_to_video(
&ApiHost::Storyteller,
Some(&creds),
request,
).await;
match result {
Ok(enqueued) => {
info!("Successfully enqueued Artcraft Kling 2.1 Pro video generation");
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft Kling 2.1 Pro video generation: {:?}", err);
return Err(InternalVideoError::StorytellerError(err));
}
}
}
Some(VideoModel::Seedance10Lite) => {
info!("enqueue Seedance 1.0 Lite with Artcraft API");
selected_model = Some(GenerationModel::Seedance10Lite);
let request = GenerateSeedance10LiteImageToVideoRequest {
uuid_idempotency_token,
media_file_token: request.image_media_token,
prompt: None,
resolution: None,
duration: None,
};
let result = generate_seedance_1_0_lite_image_to_video(
&ApiHost::Storyteller,
Some(&creds),
request,
).await;
match result {
Ok(enqueued) => {
info!("Successfully enqueued Artcraft Seedance 1.0 Lite video generation");
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft Seedance 1.0 Lite video generation: {:?}", err);
return Err(InternalVideoError::StorytellerError(err));
}
}
}
Some(VideoModel::Veo2) => {
info!("enqueue Veo 2 with Artcraft API");
selected_model = Some(GenerationModel::Veo2);
let request = GenerateVeo2ImageToVideoRequest {
uuid_idempotency_token,
media_file_token: request.image_media_token,
aspect_ratio: Some(GenerateVeo2AspectRatio::WideSixteenNine),
prompt: None,
duration: None,
};
let result = generate_veo_2_image_to_video(
&ApiHost::Storyteller,
Some(&creds),
request,
).await;
match result {
Ok(enqueued) => {
info!("Successfully enqueued Artcraft Veo 2 video generation");
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft Veo 2 video generation: {:?}", err);
return Err(InternalVideoError::StorytellerError(err));
}
}
}
};
info!("Successfully enqueued video generation");
@@ -1,10 +1,11 @@
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageModel, EnqueueTextToImageRequest};
use crate::core::commands::enqueue::video::enqueue_image_to_video_command::{EnqueueImageToVideoModel, EnqueueImageToVideoRequest};
use crate::core::commands::enqueue::video::enqueue_image_to_video_command::EnqueueImageToVideoRequest;
use crate::core::commands::enqueue::video::internal_video_error::InternalVideoError;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::core::model::video_models::VideoModel;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::utils::download_media_file_to_temp_dir::download_media_file_to_temp_dir;
use crate::core::utils::save_base64_image_to_temp_dir::save_base64_image_to_temp_dir;
@@ -60,8 +61,6 @@ pub async fn handle_video_fal(
return Err(InternalVideoError::AnyhowError(anyhow!("No image media token provided")));
}
info!("Calling FAL image to 3d ...");
let filename = temp_download.path().to_path_buf();
let mut selected_model = None;
@@ -70,8 +69,8 @@ pub async fn handle_video_fal(
None => {
return Err(InternalVideoError::NoModelSpecified);
}
Some(EnqueueImageToVideoModel::Kling16) => {
info!("enqueue Kling 1.6 image to video");
Some(VideoModel::Kling16Pro) => {
info!("enqueue Kling 1.6 image to video with Kling API");
selected_model = Some(GenerationModel::Kling1_6);
enqueue_kling_16_pro_image_to_video(Kling16ProArgs {
image_path: filename,
@@ -81,6 +80,9 @@ pub async fn handle_video_fal(
prompt: "",
}).await
}
_ => {
return Err(InternalVideoError::AnyhowError(anyhow!("Unsupported model: {:?}", request.model)));
}
};
match result {
@@ -11,13 +11,27 @@ pub enum GenerationServiceProvider {
#[derive(Debug, Copy, Clone, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum GenerationModel {
#[serde(rename = "flux_1_dev")]
Flux1Dev,
#[serde(rename = "flux_1_schnell")]
Flux1Schnell,
#[serde(rename = "flux_pro_1.1")]
FluxPro11,
#[serde(rename = "flux_pro_1.1_ultra")]
FluxPro11Ultra,
#[serde(rename = "kling_1.6")]
Kling1_6,
#[serde(rename = "kling_2.0")]
Kling2_0,
#[serde(rename = "kling_2.1_master")]
Kling21Master,
#[serde(rename = "kling_2.1_pro")]
Kling21Pro,
Sora,
#[serde(rename = "seedance_1.0_lite")]
Seedance10Lite,
#[serde(rename = "veo_2")]
Veo2,
}
#[derive(Debug, Copy, Clone, Serialize)]
+1
View File
@@ -1,6 +1,7 @@
pub mod artcraft_error;
pub mod commands;
pub mod events;
pub mod model;
pub mod state;
pub mod threads;
pub mod utils;
@@ -0,0 +1,2 @@
pub mod image_models;
pub mod video_models;
@@ -0,0 +1,22 @@
use serde_derive::Deserialize;
/// This is used in the Tauri command bridge.
/// Don't change the serializations without coordinating with the frontend.
#[derive(Deserialize, Debug, Copy, Clone)]
#[serde(rename_all = "snake_case")]
pub enum VideoModel {
#[serde(rename = "kling_1.6_pro")]
Kling16Pro,
#[serde(rename = "kling_2.1_pro")]
Kling21Pro,
#[serde(rename = "kling_2.1_master")]
Kling21Master,
#[serde(rename = "seedance_1.0_lite")]
Seedance10Lite,
#[serde(rename = "veo_2")]
Veo2,
}
@@ -0,0 +1,38 @@
use log::info;
use errors::AnyhowResult;
use storyteller_client::utils::api_host::ApiHost;
use crate::core::state::app_env_configs::app_env_configs_serializeable::{AppEnvConfigsSerializable, StorytellerApiHost};
use crate::core::state::data_dir::app_data_root::AppDataRoot;
#[derive(Clone)]
pub struct AppEnvConfigs {
pub storyteller_host: ApiHost,
}
impl AppEnvConfigs {
pub fn load_from_filesystem(root: &AppDataRoot) -> AnyhowResult<Self> {
println!("Loading app environmental configs from filesystem...");
let input = AppEnvConfigsSerializable::load_from_filesystem(root)?;
let storyteller = input.as_ref()
.map(|i| i.storyteller_host)
.flatten();
let storyteller_api = match storyteller {
Some(StorytellerApiHost::Localhost) => ApiHost::Localhost {
port: input.as_ref()
.and_then(|i| i.storyteller_port)
.unwrap_or(12345)
},
Some(StorytellerApiHost::Production) => ApiHost::Storyteller,
_ => ApiHost::Storyteller,
};
println!("Using storyteller API host: {:?}", storyteller_api);
Ok(Self {
storyteller_host: storyteller_api,
})
}
}
@@ -0,0 +1,34 @@
use serde_derive::{Deserialize, Serialize};
use errors::AnyhowResult;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
const CURRENT_VERSION: &str = "1";
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StorytellerApiHost {
Production,
Localhost,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct AppEnvConfigsSerializable {
/// Versioning string.
pub version: Option<String>,
pub storyteller_port: Option<u32>,
pub storyteller_host: Option<StorytellerApiHost>,
}
impl AppEnvConfigsSerializable {
pub fn load_from_filesystem(app_data_root: &AppDataRoot) -> AnyhowResult<Option<Self>> {
let filename = app_data_root.settings_dir().get_app_env_configs_path();
if !filename.exists() {
return Ok(None);
}
let contents = std::fs::read_to_string(filename)?;
let data: Self = serde_json::from_str(&contents)?;
Ok(Some(data))
}
}
@@ -0,0 +1,2 @@
pub mod app_env_configs;
pub mod app_env_configs_serializeable;
@@ -31,7 +31,11 @@ impl AppSettingsDir {
pub fn get_app_preferences_path(&self) -> PathBuf {
self.path.join("app_preferences.json")
}
pub fn get_app_env_configs_path(&self) -> PathBuf {
self.path.join("env_configs.json")
}
pub fn write_app_preferences(&self, prefs: &AppPreferences) -> AnyhowResult<()> {
let serializable = prefs.to_serializable();
@@ -1,3 +1,4 @@
pub mod app_env_configs;
pub mod app_preferences;
pub mod app_startup_time;
pub mod data_dir;
+5
View File
@@ -11,6 +11,7 @@ use crate::core::commands::enqueue::video::enqueue_image_to_video_command::enque
use crate::core::commands::flip_image::flip_image;
use crate::core::commands::get_build_info_command::get_build_info_command;
use crate::core::commands::platform_info_command::platform_info_command;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::app_preferences::app_preferences_manager::load_app_preferences_or_default;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::main_window_position::MainWindowPosition;
@@ -71,6 +72,9 @@ pub fn run() {
let fal_task_queue = FalTaskQueue::new();
let fal_task_queue_2 = fal_task_queue.clone();
let app_env_configs = AppEnvConfigs::load_from_filesystem(&app_data_root)
.expect("AppEnvConfigs should be loaded from disk");
println!("Initializing backend runtime...");
@@ -155,6 +159,7 @@ pub fn run() {
Ok(())
})
.manage(app_data_root)
.manage(app_env_configs)
.manage(app_preferences)
.manage(fal_creds_manager)
.manage(fal_task_queue)
@@ -2,4 +2,6 @@ pub mod fal_background_removal_command;
pub mod fal_hunyuan_image_to_3d_command;
pub mod fal_kling_image_to_video_command;
pub mod get_fal_api_key_command;
pub mod set_fal_api_key_command;
pub mod set_fal_api_key_command;
// pub mod get_open_ai_api_key_command;
// pub mod set_open_ai_api_key_command;
@@ -0,0 +1,19 @@
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::generate_flux_1_dev_text_to_image::{GenerateFlux1DevTextToImageRequest, GenerateFlux1DevTextToImageResponse, GENERATE_FLUX_1_DEV_TEXT_TO_IMAGE_PATH};
pub async fn generate_flux_1_dev_text_to_image(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: GenerateFlux1DevTextToImageRequest,
) -> Result<GenerateFlux1DevTextToImageResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
GENERATE_FLUX_1_DEV_TEXT_TO_IMAGE_PATH,
maybe_creds,
request,
).await?)
}
@@ -0,0 +1,19 @@
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::generate_flux_1_schnell_text_to_image::{GenerateFlux1SchnellTextToImageRequest, GenerateFlux1SchnellTextToImageResponse, GENERATE_FLUX_1_SCHNELL_TEXT_TO_IMAGE_PATH};
pub async fn generate_flux_1_schnell_text_to_image(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: GenerateFlux1SchnellTextToImageRequest,
) -> Result<GenerateFlux1SchnellTextToImageResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
GENERATE_FLUX_1_SCHNELL_TEXT_TO_IMAGE_PATH,
maybe_creds,
request,
).await?)
}
@@ -0,0 +1,19 @@
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::generate_flux_pro_11_text_to_image::{GenerateFluxPro11TextToImageRequest, GenerateFluxPro11TextToImageResponse, GENERATE_FLUX_PRO_11_TEXT_TO_IMAGE_PATH};
pub async fn generate_flux_pro_11_text_to_image(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: GenerateFluxPro11TextToImageRequest,
) -> Result<GenerateFluxPro11TextToImageResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
GENERATE_FLUX_PRO_11_TEXT_TO_IMAGE_PATH,
maybe_creds,
request,
).await?)
}
@@ -1,68 +1,18 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::api_error::ApiError;
use crate::error::client_error::ClientError;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
use crate::utils::filter_bad_response::filter_bad_response;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::{GenerateFluxPro11UltraTextToImageRequest, GenerateFluxPro11UltraTextToImageResponse, GENERATE_FLUX_PRO_11_ULTRA_TEXT_TO_IMAGE_PATH};
use log::{debug, info};
use reqwest::Client;
pub async fn generate_flux_pro_11_ultra_text_to_image(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: GenerateFluxPro11UltraTextToImageRequest,
) -> Result<GenerateFluxPro11UltraTextToImageResponse, StorytellerError> {
let url = get_route(api_host);
info!("Requesting {:?}", &url);
let client = Client::builder()
.gzip(true)
.build()
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
let mut request_builder = client.post(url)
.header("User-Agent", USER_AGENT)
.header("Accept", APPLICATION_JSON)
.header("Content-Type", APPLICATION_JSON);
if let Some(creds) = maybe_creds {
if let Some(header) = &creds.maybe_as_cookie_header() {
request_builder = request_builder.header("Cookie", header);
}
}
let request_body = serde_json::to_string(&request)
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
debug!("Request body: {:?}", request_body);
let response = request_builder
.body(request_body)
.send()
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let response = filter_bad_response(response)
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let response_body = &response.text()
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
debug!("Response body: {:?}", response_body);
let response = serde_json::from_str(&response_body)
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
Ok(response)
}
fn get_route(api_host: &ApiHost) -> String {
let api_hostname = api_host.to_api_hostname();
format!("https://{}{}", api_hostname, GENERATE_FLUX_PRO_11_ULTRA_TEXT_TO_IMAGE_PATH)
Ok(basic_json_post_request(
api_host,
GENERATE_FLUX_PRO_11_ULTRA_TEXT_TO_IMAGE_PATH,
maybe_creds,
request,
).await?)
}
@@ -1 +1,4 @@
pub mod generate_flux_1_dev_text_to_image;
pub mod generate_flux_1_schnell_text_to_image;
pub mod generate_flux_pro_11_text_to_image;
pub mod generate_flux_pro_11_ultra_text_to_image;
@@ -0,0 +1,68 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::api_error::ApiError;
use crate::error::client_error::ClientError;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
use crate::utils::filter_bad_response::filter_bad_response;
use artcraft_api_defs::generate::object::generate_hunyuan_2_0_image_to_3d::{GenerateHunyuan20ImageTo3dRequest, GenerateHunyuan20ImageTo3dResponse, GENERATE_HUNYUAN_2_0_IMAGE_TO_3D_URL_PATH};
use log::{debug, info};
use reqwest::Client;
pub async fn generate_hunyuan3d_2_0_image_to_3d(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: GenerateHunyuan20ImageTo3dRequest,
) -> Result<GenerateHunyuan20ImageTo3dResponse, StorytellerError> {
let url = get_route(api_host);
info!("Requesting {:?}", &url);
let client = Client::builder()
.gzip(true)
.build()
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
let mut request_builder = client.post(url)
.header("User-Agent", USER_AGENT)
.header("Accept", APPLICATION_JSON)
.header("Content-Type", APPLICATION_JSON);
if let Some(creds) = maybe_creds {
if let Some(header) = &creds.maybe_as_cookie_header() {
request_builder = request_builder.header("Cookie", header);
}
}
let request_body = serde_json::to_string(&request)
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
debug!("Request body: {:?}", request_body);
let response = request_builder
.body(request_body)
.send()
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let response = filter_bad_response(response)
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let response_body = &response.text()
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
debug!("Response body: {:?}", response_body);
let response = serde_json::from_str(&response_body)
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
Ok(response)
}
fn get_route(api_host: &ApiHost) -> String {
let api_hostname = api_host.to_api_hostname();
format!("https://{}{}", api_hostname, GENERATE_HUNYUAN_2_0_IMAGE_TO_3D_URL_PATH)
}
@@ -0,0 +1,68 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::api_error::ApiError;
use crate::error::client_error::ClientError;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
use crate::utils::filter_bad_response::filter_bad_response;
use artcraft_api_defs::generate::object::generate_hunyuan_2_1_image_to_3d::{GenerateHunyuan21ImageTo3dRequest, GenerateHunyuan21ImageTo3dResponse, GENERATE_HUNYUAN_2_1_IMAGE_TO_3D_URL_PATH};
use log::{debug, info};
use reqwest::Client;
pub async fn generate_hunyuan3d_2_1_image_to_3d(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: GenerateHunyuan21ImageTo3dRequest,
) -> Result<GenerateHunyuan21ImageTo3dResponse, StorytellerError> {
let url = get_route(api_host);
info!("Requesting {:?}", &url);
let client = Client::builder()
.gzip(true)
.build()
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
let mut request_builder = client.post(url)
.header("User-Agent", USER_AGENT)
.header("Accept", APPLICATION_JSON)
.header("Content-Type", APPLICATION_JSON);
if let Some(creds) = maybe_creds {
if let Some(header) = &creds.maybe_as_cookie_header() {
request_builder = request_builder.header("Cookie", header);
}
}
let request_body = serde_json::to_string(&request)
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
debug!("Request body: {:?}", request_body);
let response = request_builder
.body(request_body)
.send()
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let response = filter_bad_response(response)
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let response_body = &response.text()
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
debug!("Response body: {:?}", response_body);
let response = serde_json::from_str(&response_body)
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
Ok(response)
}
fn get_route(api_host: &ApiHost) -> String {
let api_hostname = api_host.to_api_hostname();
format!("https://{}{}", api_hostname, GENERATE_HUNYUAN_2_1_IMAGE_TO_3D_URL_PATH)
}
@@ -1 +1,2 @@
pub mod generate_hunyuan2_image_to_3d;
pub mod generate_hunyuan_3d_2_0_image_to_3d;
pub mod generate_hunyuan_3d_2_1_image_to_3d;
@@ -0,0 +1,68 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::api_error::ApiError;
use crate::error::client_error::ClientError;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
use crate::utils::filter_bad_response::filter_bad_response;
use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::{GenerateKling21MasterImageToVideoRequest, GenerateKling21MasterImageToVideoResponse, GENERATE_KLING_2_1_MASTER_IMAGE_TO_VIDEO_URL_PATH};
use log::{debug, info};
use reqwest::Client;
pub async fn generate_kling_21_master_image_to_video(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: GenerateKling21MasterImageToVideoRequest,
) -> Result<GenerateKling21MasterImageToVideoResponse, StorytellerError> {
let url = get_route(api_host);
info!("Requesting {:?}", &url);
let client = Client::builder()
.gzip(true)
.build()
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
let mut request_builder = client.post(url)
.header("User-Agent", USER_AGENT)
.header("Accept", APPLICATION_JSON)
.header("Content-Type", APPLICATION_JSON);
if let Some(creds) = maybe_creds {
if let Some(header) = &creds.maybe_as_cookie_header() {
request_builder = request_builder.header("Cookie", header);
}
}
let request_body = serde_json::to_string(&request)
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
debug!("Request body: {:?}", request_body);
let response = request_builder
.body(request_body)
.send()
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let response = filter_bad_response(response)
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let response_body = &response.text()
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
debug!("Response body: {:?}", response_body);
let response = serde_json::from_str(&response_body)
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
Ok(response)
}
fn get_route(api_host: &ApiHost) -> String {
let api_hostname = api_host.to_api_hostname();
format!("https://{}{}", api_hostname, GENERATE_KLING_2_1_MASTER_IMAGE_TO_VIDEO_URL_PATH)
}
@@ -0,0 +1,68 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::api_error::ApiError;
use crate::error::client_error::ClientError;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
use crate::utils::filter_bad_response::filter_bad_response;
use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::{GenerateKling21ProImageToVideoRequest, GenerateKling21ProImageToVideoResponse, GENERATE_KLING_2_1_PRO_IMAGE_TO_VIDEO_URL_PATH};
use log::{debug, info};
use reqwest::Client;
pub async fn generate_kling_21_pro_image_to_video(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: GenerateKling21ProImageToVideoRequest,
) -> Result<GenerateKling21ProImageToVideoResponse, StorytellerError> {
let url = get_route(api_host);
info!("Requesting {:?}", &url);
let client = Client::builder()
.gzip(true)
.build()
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
let mut request_builder = client.post(url)
.header("User-Agent", USER_AGENT)
.header("Accept", APPLICATION_JSON)
.header("Content-Type", APPLICATION_JSON);
if let Some(creds) = maybe_creds {
if let Some(header) = &creds.maybe_as_cookie_header() {
request_builder = request_builder.header("Cookie", header);
}
}
let request_body = serde_json::to_string(&request)
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
debug!("Request body: {:?}", request_body);
let response = request_builder
.body(request_body)
.send()
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let response = filter_bad_response(response)
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let response_body = &response.text()
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
debug!("Response body: {:?}", response_body);
let response = serde_json::from_str(&response_body)
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
Ok(response)
}
fn get_route(api_host: &ApiHost) -> String {
let api_hostname = api_host.to_api_hostname();
format!("https://{}{}", api_hostname, GENERATE_KLING_2_1_PRO_IMAGE_TO_VIDEO_URL_PATH)
}
@@ -0,0 +1,68 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::api_error::ApiError;
use crate::error::client_error::ClientError;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
use crate::utils::filter_bad_response::filter_bad_response;
use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::{GenerateSeedance10LiteImageToVideoRequest, GenerateSeedance10LiteImageToVideoResponse, GENERATE_SEEDANCE_1_0_LITE_IMAGE_TO_VIDEO_URL_PATH};
use log::{debug, info};
use reqwest::Client;
pub async fn generate_seedance_1_0_lite_image_to_video(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: GenerateSeedance10LiteImageToVideoRequest,
) -> Result<GenerateSeedance10LiteImageToVideoResponse, StorytellerError> {
let url = get_route(api_host);
info!("Requesting {:?}", &url);
let client = Client::builder()
.gzip(true)
.build()
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
let mut request_builder = client.post(url)
.header("User-Agent", USER_AGENT)
.header("Accept", APPLICATION_JSON)
.header("Content-Type", APPLICATION_JSON);
if let Some(creds) = maybe_creds {
if let Some(header) = &creds.maybe_as_cookie_header() {
request_builder = request_builder.header("Cookie", header);
}
}
let request_body = serde_json::to_string(&request)
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
debug!("Request body: {:?}", request_body);
let response = request_builder
.body(request_body)
.send()
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let response = filter_bad_response(response)
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let response_body = &response.text()
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
debug!("Response body: {:?}", response_body);
let response = serde_json::from_str(&response_body)
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
Ok(response)
}
fn get_route(api_host: &ApiHost) -> String {
let api_hostname = api_host.to_api_hostname();
format!("https://{}{}", api_hostname, GENERATE_SEEDANCE_1_0_LITE_IMAGE_TO_VIDEO_URL_PATH)
}
@@ -5,15 +5,15 @@ use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
use crate::utils::filter_bad_response::filter_bad_response;
use artcraft_api_defs::generate::object::generate_hunyuan_2_image_to_3d::{GenerateHunyuan2ImageTo3dRequest, GenerateHunyuan2ImageTo3dResponse, GENERATE_HUNYUAN_2_IMAGE_TO_3D_URL_PATH};
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::{GenerateVeo2ImageToVideoRequest, GenerateVeo2ImageToVideoResponse, GENERATE_VEO_2_IMAGE_TO_VIDEO_URL_PATH};
use log::{debug, info};
use reqwest::Client;
pub async fn generate_hunyuan2_image_to_3d(
pub async fn generate_veo_2_image_to_video(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: GenerateHunyuan2ImageTo3dRequest,
) -> Result<GenerateHunyuan2ImageTo3dResponse, StorytellerError> {
request: GenerateVeo2ImageToVideoRequest,
) -> Result<GenerateVeo2ImageToVideoResponse, StorytellerError> {
let url = get_route(api_host);
@@ -64,5 +64,5 @@ pub async fn generate_hunyuan2_image_to_3d(
fn get_route(api_host: &ApiHost) -> String {
let api_hostname = api_host.to_api_hostname();
format!("https://{}{}", api_hostname, GENERATE_HUNYUAN_2_IMAGE_TO_3D_URL_PATH)
format!("https://{}{}", api_hostname, GENERATE_VEO_2_IMAGE_TO_VIDEO_URL_PATH)
}
@@ -1 +1,5 @@
pub mod generate_kling_16_pro_image_to_video;
pub mod generate_kling_16_pro_image_to_video;
pub mod generate_kling_21_master_image_to_video;
pub mod generate_kling_21_pro_image_to_video;
pub mod generate_seedance_1_0_lite_image_to_video;
pub mod generate_veo_2_image_to_video;
@@ -1,5 +1,5 @@
#[derive(Clone)]
#[derive(Clone, Debug)]
pub enum ApiHost {
Storyteller,
FakeYou,
@@ -14,4 +14,12 @@ impl ApiHost {
ApiHost::Localhost { port } => format!("localhost:{}", port),
}
}
pub fn to_api_hostname_and_scheme(&self) -> String {
match self {
ApiHost::Storyteller => "https://api.storyteller.ai".to_string(),
ApiHost::FakeYou => "https://api.fakeyou.com".to_string(),
ApiHost::Localhost { port } => format!("http://localhost:{}", port),
}
}
}
@@ -0,0 +1,71 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::api_error::ApiError;
use crate::error::client_error::ClientError;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
use crate::utils::filter_bad_response::filter_bad_response;
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::{GenerateFlux1SchnellTextToImageRequest, GenerateFlux1SchnellTextToImageResponse, GENERATE_FLUX_1_SCHNELL_TEXT_TO_IMAGE_PATH};
use log::{debug, info};
use reqwest::Client;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
pub async fn basic_json_post_request<Req: Serialize, Res: DeserializeOwned>(
api_host: &ApiHost,
route_path: &str,
maybe_creds: Option<&StorytellerCredentialSet>,
request: Req,
) -> Result<Res, StorytellerError> {
let url = get_route(api_host, route_path);
info!("Requesting {:?}", &url);
let client = Client::builder()
.gzip(true)
.build()
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
let mut request_builder = client.post(url)
.header("User-Agent", USER_AGENT)
.header("Accept", APPLICATION_JSON)
.header("Content-Type", APPLICATION_JSON);
if let Some(creds) = maybe_creds {
if let Some(header) = &creds.maybe_as_cookie_header() {
request_builder = request_builder.header("Cookie", header);
}
}
let request_body = serde_json::to_string(&request)
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
debug!("Request body: {:?}", request_body);
let response = request_builder
.body(request_body)
.send()
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let response = filter_bad_response(response)
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let response_body = &response.text()
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
debug!("Response body: {:?}", response_body);
let response = serde_json::from_str(&response_body)
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
Ok(response)
}
fn get_route(api_host: &ApiHost, route_path: &str) -> String {
let api_hostname = api_host.to_api_hostname_and_scheme();
format!("{}{}", api_hostname, route_path)
}
@@ -1,4 +1,5 @@
pub mod api_host;
pub mod basic_json_post_request;
pub mod constants;
pub mod filter_bad_response;
pub mod http_get_anonymous;
@@ -3,10 +3,10 @@ use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const GENERATE_HUNYUAN_2_IMAGE_TO_3D_URL_PATH: &str = "/v1/generate/object/hunyuan_2_image_to_3d";
pub const GENERATE_HUNYUAN_2_0_IMAGE_TO_3D_URL_PATH: &str = "/v1/generate/object/hunyuan_2.0_image_to_3d";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateHunyuan2ImageTo3dRequest {
pub struct GenerateHunyuan20ImageTo3dRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
@@ -16,7 +16,7 @@ pub struct GenerateHunyuan2ImageTo3dRequest {
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateHunyuan2ImageTo3dResponse {
pub struct GenerateHunyuan20ImageTo3dResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -1,2 +1,2 @@
pub mod generate_hunyuan_21_image_to_3d;
pub mod generate_hunyuan_2_image_to_3d;
pub mod generate_hunyuan_2_0_image_to_3d;
pub mod generate_hunyuan_2_1_image_to_3d;
@@ -18,10 +18,10 @@ use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageResponse;
use artcraft_api_defs::generate::image::remove_image_background::RemoveImageBackgroundRequest;
use artcraft_api_defs::generate::image::remove_image_background::RemoveImageBackgroundResponse;
use artcraft_api_defs::generate::object::generate_hunyuan_21_image_to_3d::GenerateHunyuan21ImageTo3dRequest;
use artcraft_api_defs::generate::object::generate_hunyuan_21_image_to_3d::GenerateHunyuan21ImageTo3dResponse;
use artcraft_api_defs::generate::object::generate_hunyuan_2_image_to_3d::GenerateHunyuan2ImageTo3dRequest;
use artcraft_api_defs::generate::object::generate_hunyuan_2_image_to_3d::GenerateHunyuan2ImageTo3dResponse;
use artcraft_api_defs::generate::object::generate_hunyuan_2_1_image_to_3d::GenerateHunyuan21ImageTo3dRequest;
use artcraft_api_defs::generate::object::generate_hunyuan_2_1_image_to_3d::GenerateHunyuan21ImageTo3dResponse;
use artcraft_api_defs::generate::object::generate_hunyuan_2_0_image_to_3d::GenerateHunyuan20ImageTo3dRequest;
use artcraft_api_defs::generate::object::generate_hunyuan_2_0_image_to_3d::GenerateHunyuan20ImageTo3dResponse;
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProAspectRatio;
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProDuration;
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProImageToVideoRequest;
@@ -527,8 +527,8 @@ use crate::http_server::web_utils::response_success_helpers::*;
GenerateFluxPro11UltraTextToImageResponse,
GenerateHunyuan21ImageTo3dRequest,
GenerateHunyuan21ImageTo3dResponse,
GenerateHunyuan2ImageTo3dRequest,
GenerateHunyuan2ImageTo3dResponse,
GenerateHunyuan20ImageTo3dRequest,
GenerateHunyuan20ImageTo3dResponse,
GenerateKling16ProAspectRatio,
GenerateKling16ProDuration,
GenerateKling16ProImageToVideoRequest,
@@ -16,8 +16,8 @@ use actix_web::http::StatusCode;
use actix_web::web::Json;
use actix_web::web::Path;
use actix_web::{web, HttpRequest, HttpResponse};
use artcraft_api_defs::generate::object::generate_hunyuan_2_image_to_3d::GenerateHunyuan2ImageTo3dRequest;
use artcraft_api_defs::generate::object::generate_hunyuan_2_image_to_3d::GenerateHunyuan2ImageTo3dResponse;
use artcraft_api_defs::generate::object::generate_hunyuan_2_0_image_to_3d::GenerateHunyuan20ImageTo3dRequest;
use artcraft_api_defs::generate::object::generate_hunyuan_2_0_image_to_3d::GenerateHunyuan20ImageTo3dResponse;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use enums::common::visibility::Visibility;
use fal_client::requests::webhook::object::enqueue_hunyuan_3d_2_image_to_3d_webhook::enqueue_hunyuan_3d_2_image_to_3d_webhook;
@@ -38,7 +38,7 @@ use utoipa::ToSchema;
#[utoipa::path(
post,
tag = "Generate Objects",
path = "/v1/generate/object/hunyuan_2_image_to_3d",
path = "/v1/generate/object/hunyuan_2.0_image_to_3d",
responses(
(status = 200, description = "Success", body = GenerateHunyuan2ImageTo3dResponse),
),
@@ -46,11 +46,11 @@ use utoipa::ToSchema;
("request" = GenerateHunyuan2ImageTo3dRequest, description = "Payload for Request"),
)
)]
pub async fn generate_hunyuan_2_image_to_3d_handler(
pub async fn generate_hunyuan_2_0_image_to_3d_handler(
http_request: HttpRequest,
request: Json<GenerateHunyuan2ImageTo3dRequest>,
request: Json<GenerateHunyuan20ImageTo3dRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<GenerateHunyuan2ImageTo3dResponse>, CommonWebError> {
) -> Result<Json<GenerateHunyuan20ImageTo3dResponse>, CommonWebError> {
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
@@ -173,7 +173,7 @@ pub async fn generate_hunyuan_2_image_to_3d_handler(
}
};
Ok(Json(GenerateHunyuan2ImageTo3dResponse {
Ok(Json(GenerateHunyuan20ImageTo3dResponse {
success: true,
inference_job_token: job_token,
}))
@@ -16,8 +16,8 @@ use actix_web::http::StatusCode;
use actix_web::web::Json;
use actix_web::web::Path;
use actix_web::{web, HttpRequest, HttpResponse};
use artcraft_api_defs::generate::object::generate_hunyuan_21_image_to_3d::GenerateHunyuan21ImageTo3dRequest;
use artcraft_api_defs::generate::object::generate_hunyuan_21_image_to_3d::GenerateHunyuan21ImageTo3dResponse;
use artcraft_api_defs::generate::object::generate_hunyuan_2_1_image_to_3d::GenerateHunyuan21ImageTo3dRequest;
use artcraft_api_defs::generate::object::generate_hunyuan_2_1_image_to_3d::GenerateHunyuan21ImageTo3dResponse;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use enums::common::visibility::Visibility;
use fal_client::requests::webhook::object::enqueue_hunyuan_3d_21_image_to_3d_webhook::enqueue_hunyuan_3d_2_1_image_to_3d_webhook;
@@ -46,7 +46,7 @@ use utoipa::ToSchema;
("request" = GenerateHunyuan21ImageTo3dRequest, description = "Payload for Request"),
)
)]
pub async fn generate_hunyuan_21_image_to_3d_handler(
pub async fn generate_hunyuan_2_1_image_to_3d_handler(
http_request: HttpRequest,
request: Json<GenerateHunyuan21ImageTo3dRequest>,
server_state: web::Data<Arc<ServerState>>
@@ -1,2 +1,2 @@
pub mod generate_hunyuan_21_image_to_3d_handler;
pub mod generate_hunyuan_2_image_to_3d_handler;
pub mod generate_hunyuan_2_1_image_to_3d_handler;
pub mod generate_hunyuan_2_0_image_to_3d_handler;
@@ -2,6 +2,7 @@ use std::fmt;
use std::sync::Arc;
use crate::http_server::endpoints::webhooks::handle_image_payload::handle_image_payload;
use crate::http_server::endpoints::webhooks::handle_images_payload::handle_images_payload;
use crate::http_server::endpoints::webhooks::handle_model_mesh_payload::handle_model_mesh_payload;
use crate::http_server::endpoints::webhooks::handle_video_payload::handle_video_payload;
use crate::state::server_state::ServerState;
@@ -18,7 +19,6 @@ use mysql_queries::queries::generic_inference::fal::get_inference_job_by_fal_id:
use mysql_queries::queries::generic_inference::fal::mark_fal_generic_inference_job_successfully_done::mark_fal_generic_inference_job_successfully_done;
use serde_json::Value;
use utoipa::ToSchema;
// 1. tauri --> hit endpoint to enqueue
//
// 2. webhook
@@ -140,17 +140,24 @@ pub async fn fal_webhook_handler(
return Err(FalWebhookError::ServerError);
}
};
let mut maybe_media_token = None;
info!("Fal webhook job record: {:?}", job);
let mut maybe_media_token = None;
if let Some(payload_obj) = payload.as_object() {
if payload_obj.contains_key("image") {
info!("Handling image payload for job: {:?}", job.job_token);
let token = handle_image_payload(payload_obj, &job, &server_state).await?;
maybe_media_token = Some(token);
} else if payload_obj.contains_key("images") {
maybe_media_token = handle_images_payload(payload_obj, &job, &server_state).await?;
} else if payload_obj.contains_key("video") {
info!("Handling video payload for job: {:?}", job.job_token);
let token = handle_video_payload(payload_obj, &job, &server_state).await?;
maybe_media_token = Some(token);
} else if payload_obj.contains_key("model_mesh") {
info!("Handling model_mesh payload for job: {:?}", job.job_token);
let token = handle_model_mesh_payload(payload_obj, &job, &server_state).await?;
maybe_media_token = Some(token);
}
@@ -167,6 +174,8 @@ pub async fn fal_webhook_handler(
warn!("Error marking job as successfully done: {:?}", err);
FalWebhookError::ServerError
})?;
} else {
warn!("No media token found in payload for job: {:?}", job.job_token);
}
Ok(SimpleGenericJsonSuccess::wrapped(true))
@@ -0,0 +1,124 @@
use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError;
use crate::state::server_state::ServerState;
use crate::util::http_download_url_to_bytes::http_download_url_to_bytes;
use crate::util::http_download_url_to_tempfile::http_download_url_to_tempfile;
use actix_web::web;
use anyhow::anyhow;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use enums::by_table::media_files::media_file_class::MediaFileClass;
use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory;
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
use enums::by_table::media_files::media_file_type::MediaFileType;
use errors::AnyhowResult;
use hashing::sha256::sha256_hash_bytes::sha256_hash_bytes;
use images::image_info::image_info::ImageInfo;
use log::{info, warn};
use mimetypes::mimetype_info::mimetype_info::MimetypeInfo;
use mysql_queries::queries::generic_inference::fal::get_inference_job_by_fal_id::FalJobDetails;
use mysql_queries::queries::media_files::create::insert_builder::media_file_insert_builder::MediaFileInsertBuilder;
use serde_json::{Map, Value};
use std::sync::Arc;
use tokens::tokens::media_files::MediaFileToken;
const PREFIX : Option<&str> = Some("artcraft_");
#[derive(Deserialize, Debug)]
pub struct FalWebhookImage {
pub content_type: Option<String>,
pub file_name: Option<String>,
pub file_size: Option<usize>,
pub height: Option<usize>,
pub width: Option<usize>,
pub url: Option<String>,
}
pub async fn handle_images_payload(
payload: &Map<String, Value>,
job: &FalJobDetails,
server_state: &ServerState,
) -> AnyhowResult<Option<MediaFileToken>> {
let images_value = payload.get("images")
.ok_or_else(|| anyhow!("no `images` key in payload"))?;
info!("Fal Images Payload: {:?}", images_value);
let images: Vec<FalWebhookImage> = serde_json::from_value(images_value.clone())?;
let mut maybe_media_token = None;
for image in images {
let media_token = upload_image(job, server_state, &image).await?;
if maybe_media_token.is_none() {
maybe_media_token = Some(media_token); // Set the first media token
}
}
Ok(maybe_media_token)
}
pub async fn upload_image(
job: &FalJobDetails,
server_state: &ServerState,
image: &FalWebhookImage,
) -> AnyhowResult<MediaFileToken> {
let image_url = image.url
.as_deref()
.ok_or_else(|| anyhow!("no `url` in image payload"))?;
//let mime_type = image.content_type
// .as_deref()
// .ok_or_else(|| anyhow!("no `content_type` in image payload"))?;
let file_bytes = http_download_url_to_bytes(image_url)
.await
.map_err(|e| anyhow!("Failed to download image: {:?}", e))?;
let mimetype_info = MimetypeInfo::get_for_bytes(&file_bytes)
.ok_or_else(|| anyhow!("Failed to get mimetype info"))?;
let mime_type = mimetype_info.mime_type();
let media_file_type = MediaFileType::try_from_mime_type(mime_type)
.ok_or_else(|| anyhow!("Unsupported media file type: {}", mime_type))?;
let extension_with_period = mimetype_info.file_extension()
.map(|ext| ext.extension_with_period())
.ok_or_else(|| anyhow!("Failed to get file extension from mimetype info"))?;
let file_size_bytes = file_bytes.len();
let file_hash = sha256_hash_bytes(&file_bytes)?;
let image_info = ImageInfo::decode_image_from_bytes(&file_bytes)?;
let public_upload_path = MediaFileBucketPath::generate_new(PREFIX, Some(&extension_with_period));
info!("Uploading media to bucket path: {}", public_upload_path.get_full_object_path_str());
server_state.public_bucket_client.upload_file_with_content_type_process(
public_upload_path.get_full_object_path_str(),
file_bytes.as_ref(),
&mime_type)
.await?;
let media_token = MediaFileInsertBuilder::new()
.maybe_creator_user(job.maybe_creator_user_token.as_ref())
.maybe_creator_anonymous_visitor(job.maybe_creator_anonymous_visitor_token.as_ref())
.creator_ip_address(&job.creator_ip_address)
.public_bucket_directory_hash(&public_upload_path)
.media_file_class(MediaFileClass::Image)
.media_file_type(media_file_type)
.media_file_origin_category(MediaFileOriginCategory::Inference)
//.media_file_origin_product_category(MediaFileOriginProductCategory::Unknown)
.mime_type(mime_type)
.file_size_bytes(file_size_bytes as u64)
.frame_width(image_info.width())
.frame_height(image_info.height())
.checksum_sha2(&file_hash)
.insert_pool(&server_state.mysql_pool)
.await?;
info!("Image media file uploaded with token: {}", media_token);
Ok(media_token)
}
@@ -1,4 +1,5 @@
pub mod fal_webhook_handler;
pub mod handle_image_payload;
pub mod handle_images_payload;
pub mod handle_model_mesh_payload;
pub mod handle_video_payload;
@@ -3,8 +3,8 @@ use crate::http_server::endpoints::generate::image::generate_flux_1_schnell_text
use crate::http_server::endpoints::generate::image::generate_flux_pro_11_text_to_image_handler::generate_flux_pro_11_text_to_image_handler;
use crate::http_server::endpoints::generate::image::generate_flux_pro_11_ultra_text_to_image_handler::generate_flux_pro_11_ultra_text_to_image_handler;
use crate::http_server::endpoints::generate::image::remove_image_background_handler::remove_image_background_handler;
use crate::http_server::endpoints::generate::object::generate_hunyuan_21_image_to_3d_handler::generate_hunyuan_21_image_to_3d_handler;
use crate::http_server::endpoints::generate::object::generate_hunyuan_2_image_to_3d_handler::generate_hunyuan_2_image_to_3d_handler;
use crate::http_server::endpoints::generate::object::generate_hunyuan_2_0_image_to_3d_handler::generate_hunyuan_2_0_image_to_3d_handler;
use crate::http_server::endpoints::generate::object::generate_hunyuan_2_1_image_to_3d_handler::generate_hunyuan_2_1_image_to_3d_handler;
use crate::http_server::endpoints::generate::video::generate_kling_1_6_pro_video_handler::generate_kling_1_6_pro_video_handler;
use crate::http_server::endpoints::generate::video::generate_kling_2_1_master_video_handler::generate_kling_2_1_master_video_handler;
use crate::http_server::endpoints::generate::video::generate_kling_2_1_pro_video_handler::generate_kling_2_1_pro_video_handler;
@@ -72,12 +72,12 @@ where
)
)
.service(web::scope("/object")
.service(web::resource("/hunyuan_2_image_to_3d")
.route(web::post().to(generate_hunyuan_2_image_to_3d_handler))
.service(web::resource("/hunyuan_2.0_image_to_3d")
.route(web::post().to(generate_hunyuan_2_0_image_to_3d_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
.service(web::resource("/hunyuan_2.1_image_to_3d")
.route(web::post().to(generate_hunyuan_21_image_to_3d_handler))
.route(web::post().to(generate_hunyuan_2_1_image_to_3d_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
)
@@ -3,18 +3,9 @@ import { AUTH_STATUS } from "~/enums";
import { authentication } from "~/signals";
import { Button } from "@storyteller/ui-button";
import ProfileDropdown from "./ProfileDropdown";
import { LoginModal } from "@storyteller/ui-login-modal";
import { useState } from "react";
import {
disableHotkeyInput,
DomLevels,
enableHotkeyInput,
} from "~/pages/PageEnigma/signals";
export const AuthButtons = () => {
export const AuthButtons = ({ loginSignUpPressed }: { loginSignUpPressed: () => void }) => {
useSignals();
const [showLoginModal, setShowLoginModal] = useState(false);
const [isSignUp, setIsSignUp] = useState(false);
const { status, userInfo } = authentication;
@@ -31,35 +22,13 @@ export const AuthButtons = () => {
<Button
className="h-[38px]"
onClick={() => {
setIsSignUp(true);
setShowLoginModal(true);
loginSignUpPressed()
}}
>
Login / Sign Up
</Button>
</div>
{showLoginModal && (
<LoginModal
onClose={() => setShowLoginModal(false)}
isSignUp={isSignUp}
videoSrc2D="/resources/videos/artcraft-canvas-demo.mp4"
videoSrc3D="/resources/videos/artcraft-3d-demo.mp4"
onArtCraftAuthSuccess={(userInfo_value) => {
userInfo.value = userInfo_value;
status.value = AUTH_STATUS.LOGGED_IN;
setShowLoginModal(false);
}}
onOpenChange={(isOpen: boolean) => {
if (isOpen) {
disableHotkeyInput(DomLevels.DIALOGUE);
} else {
enableHotkeyInput(DomLevels.DIALOGUE);
}
}}
/>
)}
</>
);
}
};
};
@@ -36,6 +36,7 @@ import {
} from "~/pages/PageEnigma/Editor/editor";
interface Props {
pageName: string;
loginSignUpPressed: () => void;
}
const appMenuTabs: MenuIconItem[] = [
@@ -70,7 +71,7 @@ const appMenuTabs: MenuIconItem[] = [
export const topNavMediaId = signal<string>("");
export const topNavMediaUrl = signal<string>("");
export const TopBar = ({ pageName }: Props) => {
export const TopBar = ({ pageName,loginSignUpPressed }: Props) => {
useSignals();
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
@@ -160,7 +161,7 @@ export const TopBar = ({ pageName }: Props) => {
<Activity />
</div>
<div className="flex justify-end gap-2">
<AuthButtons />
<AuthButtons loginSignUpPressed={loginSignUpPressed}/>
</div>
</div>
</nav>
@@ -133,7 +133,7 @@ interface SceneState {
sendToBack: (nodeIds: string[]) => void;
bringForward: (nodeIds: string[]) => void;
sendBackward: (nodeIds: string[]) => void;
removeBackground: (nodeIds: string[], operation: (success: boolean,base64_image:string, message: string) => Promise<{ success: boolean; file?: File }>) => Promise<void>;
removeBackground: (nodeIds: string[], operation: (success: boolean, base64_image: string, message: string) => Promise<{ success: boolean; file?: File }>) => Promise<void>;
// Add new lock action
toggleLock: (nodeIds: string[]) => void;
@@ -1162,58 +1162,46 @@ export const useSceneStore = create<SceneState>((set, get) => ({
});
get().saveState();
},
removeBackground: async (nodeIds: string[], operation: (success: boolean, base64_image:string,message: string) => Promise<{ success: boolean; file?: File }>) => {
removeBackground: async (nodeIds: string[], operation: (success: boolean, base64_image: string, message: string) => Promise<{ success: boolean; file?: File }>) => {
const hasImageNodes = nodeIds.some(id => {
const node = get().nodes.find(n => n.id === id);
return node ? node.type === 'image': false;
const node = get().nodes.find(n => n.id === id);
return node ? node.type === 'image' : false;
});
let callbackSuccess = false
if (hasImageNodes) {
console.log("Selected nodes include image types.");
const firstNode = get().nodes.find(node => nodeIds.includes(node.id) && node.type === 'image');
if (firstNode) {
const imageFile = firstNode.imageFile; // Assuming the file is stored in the node
if (imageFile !== undefined) {
try {
const base64Image = await convertFileToBase64(imageFile);
console.log("Base64 Image:", base64Image);
// Additional logic to handle the base64 image can be added here
const { success, file } = await operation(true, base64Image, "Removing Background."); // Indicate success in removing background
callbackSuccess = success;
if (callbackSuccess) {
// save the state of the image.
set((state: SceneState) => ({
nodes: state.nodes.map((node: Node) => {
if (node.id === firstNode.id) {
// Create an object URL for immediate preview if needed
console.log("Updated Node!")
const updatedImageUrl: string | undefined = file ? URL.createObjectURL(file) : node.imageUrl;
return new Node({
...node,
imageFile: file,
imageUrl: updatedImageUrl,
});
}
return node;
}),
}));
const firstNode = get().nodes.find(node => nodeIds.includes(node.id) && node.type === 'image');
if (firstNode && firstNode.imageFile) {
try {
const base64Image = await convertFileToBase64(firstNode.imageFile);
const { success, file } = await operation(true, base64Image, "Removing Background.");
if (success && file) {
// Create a new node instance
const updatedNode = new Node({
...firstNode,
imageFile: file
});
// Load the new image
await updatedNode.setImageFromFile(file);
// Update the store with the fully loaded node
set((state: SceneState) => ({
nodes: state.nodes.map((node: Node) => {
if (node.id === firstNode.id) {
return updatedNode;
}
} catch (error) {
console.error("Error converting file to base64:", error);
}
}
console.log("Image File:", imageFile);
} else {
console.warn("No image node found or first node is not an image.");
return node;
}),
}));
get().saveState();
}
} catch (error) {
console.error("Error updating image:", error);
}
} else {
console.log("No selected nodes are of image type.");
operation(false,"","Did not select a Images"); // Indicate failure to remove background
}
}
},
// Add toggleLock action
@@ -1,4 +1,4 @@
import React, { useContext, useEffect } from "react";
import React, { useContext, useEffect, useState } from "react";
import { useSignals } from "@preact/signals-react/runtime";
import { TopBar } from "~/components";
import { Controls3D } from "./comps/Controls3D";
@@ -10,6 +10,7 @@ import { authentication, pageHeight, pageWidth } from "~/signals";
import { PoseModeSelector } from "./comps/PoseModeSelector";
import ImageToVideo from "../PageVideo/ImageToVideo";
import TextToImage from "../PageImage/TextToImage";
import {
timelineHeight,
sidePanelWidth,
@@ -78,7 +79,15 @@ import { useTabStore } from "../Stores/TabState";
export const PageEditor = () => {
useSignals();
const [isLoginModalOpen, setIsLoginModalOpen] = useState<boolean>(false);
const { status } = authentication;
useEffect(() => {
if (status.value !== AUTH_STATUS.LOGGED_OUT) {
setIsLoginModalOpen(true);
}
}, [status.value]);
const tabStore = useTabStore();
const handleOverlayClick = (event: React.MouseEvent<HTMLDivElement>) => {
event.stopPropagation();
@@ -406,9 +415,33 @@ export const PageEditor = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tabStore.activeTabId, editorEngine]);
return (
<div className="w-screen">
<TopBar pageName="Edit Scene" />
<TopBar loginSignUpPressed={()=> {
console.log("PRESSED")
setIsLoginModalOpen(true)
}} pageName="Edit Scene" />
{isLoginModalOpen && (
<LoginModal
onClose={() => setIsLoginModalOpen(false)}
videoSrc2D="/resources/videos/artcraft-canvas-demo.mp4"
videoSrc3D="/resources/videos/artcraft-3d-demo.mp4"
onOpenChange={(isOpen: boolean) => {
if (isOpen) {
disableHotkeyInput(DomLevels.DIALOGUE);
} else {
enableHotkeyInput(DomLevels.DIALOGUE);
}
}}
onArtCraftAuthSuccess={(userInfo: any) => {
authentication.status.value = AUTH_STATUS.LOGGED_IN;
authentication.userInfo.value = userInfo;
}}
/>
)}
{tabStore.activeTabId == "3D" && (
<div>
<OnboardingHelper />
@@ -527,7 +560,7 @@ export const PageEditor = () => {
)}
{tabStore.activeTabId == "IMAGE" && (
<div>
<ImageToVideo
<TextToImage
imageMediaId={topNavMediaId.value}
imageUrl={topNavMediaUrl.value}
/>
@@ -535,30 +568,14 @@ export const PageEditor = () => {
)}
{tabStore.activeTabId == "VIDEO" && (
<div>
<TextToImage
<ImageToVideo
imageMediaId={topNavMediaId.value}
imageUrl={topNavMediaUrl.value}
/>
</div>
)}
<LoginModal
onClose={() => {}}
videoSrc2D="/resources/videos/artcraft-canvas-demo.mp4"
videoSrc3D="/resources/videos/artcraft-3d-demo.mp4"
openAiLogo="/resources/images/openai-logo.png"
onOpenChange={(isOpen: boolean) => {
if (isOpen) {
disableHotkeyInput(DomLevels.DIALOGUE);
} else {
enableHotkeyInput(DomLevels.DIALOGUE);
}
}}
onArtCraftAuthSuccess={(userInfo: any) => {
authentication.status.value = AUTH_STATUS.LOGGED_IN;
authentication.userInfo.value = userInfo;
}}
/>
</div>
);
};
@@ -21,6 +21,10 @@ const ImageToVideo = ({ imageMediaId, imageUrl }: ImageToVideoProps) => {
selectedModels[ModelCategory.ImageToVideo] ||
videoGenerationModels[0]?.label;
console.log("Selected model:", selectedModel)
console.log("Image Media Id:", imageMediaId)
console.log("Image URL:", imageUrl)
const jobContext: JobContextType = {
jobTokens: [],
addJobToken: () => {},
+5 -1
View File
@@ -98,6 +98,7 @@ export class UsersApi extends ApiManager {
};
try {
console.log("calling auth fetch")
const response = await this.authFetch<
{ username_or_email: string; password: string },
{
@@ -110,7 +111,8 @@ export class UsersApi extends ApiManager {
method: "POST",
body: body,
});
console.log(response)
console.log("Auth fetch response!!! above")
return {
success: response.success,
data: response.success
@@ -198,3 +200,5 @@ export class UsersApi extends ApiManager {
}
}
}
(window as any).UsersApi = new UsersApi();
+2
View File
@@ -1,4 +1,6 @@
export enum API_TARGETS {
// NB: Comment this out for quick testing in development
//BASE_API = "http://localhost:12345",
BASE_API = "https://api.storyteller.ai",
GOOGLE_API = "https://storage.googleapis.com",
FUNNEL_API = "https://style.storyteller.ai",
@@ -1,26 +1,21 @@
import { Button } from "@storyteller/ui-button";
import { Transition, TransitionChild } from "@headlessui/react";
import { createPortal } from "react-dom";
import { useState, useEffect, useRef } from "react";
import {
faArrowRight,
faRightToBracket,
} from "@fortawesome/pro-solid-svg-icons";
import { faDiscord } from "@fortawesome/free-brands-svg-icons";
import { IsDesktopApp } from "@storyteller/tauri-utils";
import {
CheckSoraSession,
SoraSessionState,
useSoraLoginListener,
} from "@storyteller/tauri-api";
import { invoke } from "@tauri-apps/api/core";
import { ArtCraftSignUp } from "./artcraft-signup";
import { UsersApi } from "@storyteller/api";
interface LoginModalProps {
onClose: () => void;
videoSrc2D?: string;
videoSrc3D?: string;
openAiLogo?: string;
onOpenChange?: (isOpen: boolean) => void;
onArtCraftAuthSuccess?: (userInfo: any) => void;
isSignUp?: boolean;
@@ -30,14 +25,13 @@ export function LoginModal({
onClose,
videoSrc2D,
videoSrc3D,
openAiLogo,
onOpenChange,
onArtCraftAuthSuccess,
isSignUp: initialIsSignUp = true,
}: LoginModalProps) {
const [step, setStep] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [isOpen, setIsOpen] = useState(true);
const [isLoggedInArtCraft, setIsLoggedInArtCraft] = useState(false);
const [isSignUp, setIsSignUp] = useState(initialIsSignUp);
const [errorMessage, setErrorMessage] = useState("");
@@ -257,15 +251,19 @@ export function LoginModal({
setIsLoading(false);
return;
}
console.log("Path 1")
loginResponse = await usersApi.Login({
usernameOrEmail: username || email,
password,
});
console.log(loginResponse)
} else {
console.log("Path 2")
loginResponse = await usersApi.Login({
usernameOrEmail: username || email,
password,
});
console.log(loginResponse)
}
if (!loginResponse.success) {
@@ -336,8 +334,7 @@ export function LoginModal({
);
};
return createPortal(
<Transition appear show={isOpen}>
return <Transition appear show={isOpen}>
<div className="fixed inset-0 z-[100]">
<TransitionChild
enter="ease-out duration-300"
@@ -378,9 +375,7 @@ export function LoginModal({
</TransitionChild>
</div>
</div>
</Transition>,
document.body
);
</Transition>
}
export default LoginModal;
@@ -7,13 +7,36 @@ export type ModelList = Omit<PopoverItem, "selected">[];
export const allModels = {
video: [
{
label: "Kling 2.0",
id: "kling_1.6_pro",
label: "Kling 1.6 Pro",
icon: <FontAwesomeIcon icon={faFilm} className="h-4 w-4" />,
description: "Good quality model",
badges: [{ label: "2 min.", icon: <FontAwesomeIcon icon={faClock} /> }],
},
{
id: "kling_2.1_pro",
label: "Kling 2.1 Pro",
icon: <FontAwesomeIcon icon={faFilm} className="h-4 w-4" />,
description: "High quality model",
badges: [{ label: "2 min.", icon: <FontAwesomeIcon icon={faClock} /> }],
},
{
label: "Runway",
id: "kling_2.1_master",
label: "Kling 2.1 Master",
icon: <FontAwesomeIcon icon={faFilm} className="h-4 w-4" />,
description: "Master quality model ($$)",
badges: [{ label: "2 min.", icon: <FontAwesomeIcon icon={faClock} /> }],
},
{
id: "seedance_1.0_lite",
label: "Seedance 1.0 Lite",
icon: <FontAwesomeIcon icon={faFilm} className="h-4 w-4" />,
description: "Fast and high-quality model",
badges: [{ label: "2 min.", icon: <FontAwesomeIcon icon={faClock} /> }],
},
{
id: "veo_2",
label: "Google Veo 2",
icon: <FontAwesomeIcon icon={faFilm} className="h-4 w-4" />,
description: "Fast and high-quality model",
badges: [{ label: "2 min.", icon: <FontAwesomeIcon icon={faClock} /> }],
@@ -21,24 +44,28 @@ export const allModels = {
],
image: [
{
id: "todo",
label: "GPT Image 1 (GPT-4o)",
icon: <FontAwesomeIcon icon={faImage} className="h-4 w-4" />,
description: "Slow, ultra instructive model",
badges: [{ label: "45 sec.", icon: <FontAwesomeIcon icon={faClock} /> }],
},
{
id: "todo",
label: "Flux Pro Ultra",
icon: <FontAwesomeIcon icon={faImage} className="h-4 w-4" />,
description: "High quality model",
badges: [{ label: "15 sec.", icon: <FontAwesomeIcon icon={faClock} /> }],
},
{
id: "todo",
label: "Recraft 3",
icon: <FontAwesomeIcon icon={faImage} className="h-4 w-4" />,
description: "Fast and high-quality model",
badges: [{ label: "15 sec.", icon: <FontAwesomeIcon icon={faClock} /> }],
},
{
id: "todo",
label: "Flux.1 Kontext",
icon: <FontAwesomeIcon icon={faImage} className="h-4 w-4" />,
description: "New model with advanced context",
@@ -196,6 +196,8 @@ export const PromptBoxImage = ({
const handleEnqueue = async () => {
setIsEnqueueing(true);
console.log("Prompting");
setTimeout(() => {
// TODO(bt,2025-05-08): This is a hack so we don't accidentally wind up with a permanently disabled prompt box if
// the backend hangs on a given request.
@@ -183,6 +183,8 @@ export const PromptBoxVideo = ({
const handleEnqueue = async () => {
setIsEnqueueing(true);
console.log("Handle Enqueue");
setTimeout(() => {
// TODO(bt,2025-05-08): This is a hack so we don't accidentally wind up with a permanently disabled prompt box if
// the backend hangs on a given request.