mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-01 00:05:54 -04:00
Compare commits
30
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16f93cba52 | ||
|
|
045aefcd13 | ||
|
|
5f6bee5a87 | ||
|
|
068ff6ac10 | ||
|
|
2b5bedf131 | ||
|
|
3d06f89477 | ||
|
|
d007cb81d5 | ||
|
|
221d8bb95a | ||
|
|
760639d071 | ||
|
|
4e68169faf | ||
|
|
8090cdb526 | ||
|
|
ada4c77093 | ||
|
|
7bb14e1b17 | ||
|
|
f9c8343165 | ||
|
|
53eb46d2b6 | ||
|
|
f5a3ff67b3 | ||
|
|
6b262d9fcb | ||
|
|
744df6bcb8 | ||
|
|
9a5ab77d3c | ||
|
|
3e42ccc5e5 | ||
|
|
81bf7d712c | ||
|
|
e9fe8bdf0d | ||
|
|
627fd6beeb | ||
|
|
77788a74e9 | ||
|
|
691f27eb23 | ||
|
|
9008e1f4f4 | ||
|
|
c11894692c | ||
|
|
e7d8613d09 | ||
|
|
55832bdfc2 | ||
|
|
49f18d7616 |
@@ -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"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
+57
-23
@@ -1,12 +1,21 @@
|
||||
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::image::success_event::SuccessEvent;
|
||||
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::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
|
||||
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
|
||||
use crate::core::events::sendable_event_trait::SendableEvent;
|
||||
use crate::core::model::image_models::ImageModel;
|
||||
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::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
use crate::core::utils::download_media_file_to_temp_dir::download_media_file_to_temp_dir;
|
||||
use crate::core::utils::get_url_file_extension::get_url_file_extension;
|
||||
use crate::core::utils::save_base64_image_to_temp_dir::save_base64_image_to_temp_dir;
|
||||
@@ -55,8 +64,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 {
|
||||
@@ -64,19 +71,9 @@ pub struct EnqueueTextToImageRequest {
|
||||
pub prompt: Option<String>,
|
||||
|
||||
/// The model to use.
|
||||
pub model: Option<EnqueueTextToImageModel>,
|
||||
pub model: Option<ImageModel>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Copy, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum EnqueueTextToImageModel {
|
||||
#[serde(rename = "flux_pro_ultra")]
|
||||
FluxProUltra,
|
||||
#[serde(rename = "gpt_image_1")]
|
||||
GptImage1,
|
||||
#[serde(rename = "recraft_3")]
|
||||
Recraft3,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct EnqueueTextToImageSuccessResponse {
|
||||
@@ -89,6 +86,8 @@ impl SerializeMarker for EnqueueTextToImageSuccessResponse {}
|
||||
pub enum EnqueueTextToImageErrorType {
|
||||
/// Caller didn't specify a model
|
||||
ModelNotSpecified,
|
||||
/// No model available for image generation
|
||||
NoProviderAvailable,
|
||||
/// Generic server error
|
||||
ServerError,
|
||||
/// No Fal API key available
|
||||
@@ -105,6 +104,8 @@ pub async fn enqueue_text_to_image_command(
|
||||
app: AppHandle,
|
||||
request: EnqueueTextToImageRequest,
|
||||
app_data_root: State<'_, AppDataRoot>,
|
||||
app_env_configs: State<'_, AppEnvConfigs>,
|
||||
provider_priority_store: State<'_, ProviderPriorityStore>,
|
||||
fal_creds_manager: State<'_, FalCredentialManager>,
|
||||
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
|
||||
fal_task_queue: State<'_, FalTaskQueue>,
|
||||
@@ -118,8 +119,10 @@ pub async fn enqueue_text_to_image_command(
|
||||
&app,
|
||||
request,
|
||||
&app_data_root,
|
||||
&provider_priority_store,
|
||||
&fal_creds_manager,
|
||||
&storyteller_creds_manager,
|
||||
&app_env_configs,
|
||||
&fal_task_queue,
|
||||
&sora_creds_manager,
|
||||
&sora_task_queue,
|
||||
@@ -139,6 +142,11 @@ pub async fn enqueue_text_to_image_command(
|
||||
error_type = EnqueueTextToImageErrorType::ModelNotSpecified;
|
||||
error_message = "No model specified for image generation";
|
||||
}
|
||||
InternalImageError::NoProviderAvailable => {
|
||||
status = CommandErrorStatus::ServerError;
|
||||
error_type = EnqueueTextToImageErrorType::NoProviderAvailable;
|
||||
error_message = "No configured provider available for image generation";
|
||||
}
|
||||
InternalImageError::NeedsFalApiKey => {
|
||||
status = CommandErrorStatus::Unauthorized;
|
||||
error_type = EnqueueTextToImageErrorType::NeedsFalApiKey;
|
||||
@@ -154,7 +162,17 @@ pub async fn enqueue_text_to_image_command(
|
||||
error_details: None,
|
||||
})
|
||||
}
|
||||
Ok(()) => {
|
||||
Ok(event) => {
|
||||
let event = GenerationEnqueueSuccessEvent {
|
||||
action: GenerationAction::GenerateImage,
|
||||
service: event.service_provider,
|
||||
model: Some(event.tauri_event_model()),
|
||||
};
|
||||
|
||||
if let Err(err) = event.send(&app) {
|
||||
error!("Failed to emit event: {:?}", err); // Fail open.
|
||||
}
|
||||
|
||||
Ok(EnqueueTextToImageSuccessResponse {}.into())
|
||||
}
|
||||
}
|
||||
@@ -165,31 +183,47 @@ pub async fn handle_request(
|
||||
app: &AppHandle,
|
||||
request: EnqueueTextToImageRequest,
|
||||
app_data_root: &AppDataRoot,
|
||||
provider_priority_store: &ProviderPriorityStore,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
sora_task_queue: &SoraTaskQueue,
|
||||
) -> Result<(), InternalImageError> {
|
||||
) -> Result<SuccessEvent, InternalImageError> {
|
||||
|
||||
match request.model {
|
||||
None => {
|
||||
return Err(InternalImageError::NoModelSpecified);
|
||||
}
|
||||
Some(EnqueueTextToImageModel::GptImage1) => {
|
||||
Some(ImageModel::GptImage1) => {
|
||||
handle_image_sora(&app, request, sora_creds_manager, sora_task_queue).await?;
|
||||
return Ok(());
|
||||
return Ok(SuccessEvent {
|
||||
service_provider: GenerationServiceProvider::Sora,
|
||||
model: ImageModel::GptImage1,
|
||||
});
|
||||
}
|
||||
_ => {
|
||||
// Fall-through
|
||||
}
|
||||
};
|
||||
|
||||
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?;
|
||||
let priority = provider_priority_store.get_priority()?;
|
||||
|
||||
for provider in priority.iter() {
|
||||
match provider {
|
||||
Provider::Sora => {} // Fallthrough
|
||||
Provider::Artcraft => {
|
||||
return handle_image_artcraft(
|
||||
request, &app, app_env_configs, app_data_root, storyteller_creds_manager).await;
|
||||
}
|
||||
Provider::Fal => {
|
||||
if fal_creds_manager.has_apparent_api_token()? {
|
||||
return handle_image_fal(&app, request, fal_creds_manager, fal_task_queue).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
Err(InternalImageError::NoProviderAvailable)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageModel, EnqueueTextToImageRequest};
|
||||
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::EnqueueTextToImageRequest;
|
||||
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
|
||||
use crate::core::commands::enqueue::image::success_event::SuccessEvent;
|
||||
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::image_models::ImageModel;
|
||||
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 +16,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 +28,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,9 +40,10 @@ 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> {
|
||||
) -> Result<SuccessEvent, InternalImageError> {
|
||||
|
||||
let creds = match storyteller_creds_manager.get_credentials()? {
|
||||
Some(creds) => creds,
|
||||
@@ -54,21 +64,96 @@ pub async fn handle_image_artcraft(
|
||||
|
||||
let uuid_idempotency_token = generate_random_uuid();
|
||||
|
||||
let mut selected_model = None;
|
||||
let mut selected_model;
|
||||
|
||||
let job_token = match request.model {
|
||||
None => {
|
||||
return Err(InternalImageError::NoModelSpecified);
|
||||
}
|
||||
Some(EnqueueTextToImageModel::GptImage1) => {
|
||||
Some(ImageModel::GptImage1) => {
|
||||
return Err(InternalImageError::AnyhowError(anyhow!("wrong logic: artcraft is handling sora images")));
|
||||
}
|
||||
Some(EnqueueTextToImageModel::Recraft3) => {
|
||||
Some(ImageModel::Recraft3) => {
|
||||
return Err(InternalImageError::AnyhowError(anyhow!("not yet implemented in Artcraft")));
|
||||
}
|
||||
Some(EnqueueTextToImageModel::FluxProUltra) => {
|
||||
info!("enqueue Flux Pro Ultra");
|
||||
selected_model = Some(GenerationModel::FluxPro11Ultra);
|
||||
Some(ImageModel::Flux1Dev) => {
|
||||
info!("enqueue Flux 1 Dev");
|
||||
selected_model = ImageModel::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(ImageModel::Flux1Schnell) => {
|
||||
info!("enqueue Flux 1 Schnell");
|
||||
selected_model = ImageModel::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(ImageModel::FluxPro11) => {
|
||||
info!("enqueue Flux Pro 1.1");
|
||||
selected_model = ImageModel::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(ImageModel::FluxPro11Ultra) => {
|
||||
info!("enqueue Flux Pro 1.1 Ultra");
|
||||
selected_model = ImageModel::FluxPro11Ultra;
|
||||
let request = GenerateFluxPro11UltraTextToImageRequest {
|
||||
uuid_idempotency_token,
|
||||
prompt: request.prompt,
|
||||
@@ -76,22 +161,25 @@ 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
Ok(SuccessEvent {
|
||||
service_provider: GenerationServiceProvider::Artcraft,
|
||||
model:selected_model,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageModel, EnqueueTextToImageRequest};
|
||||
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::EnqueueTextToImageRequest;
|
||||
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
|
||||
use crate::core::commands::enqueue::image::success_event::SuccessEvent;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, 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::image_models::ImageModel;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
|
||||
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
|
||||
@@ -22,7 +24,7 @@ pub async fn handle_image_fal(
|
||||
request: EnqueueTextToImageRequest,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
) -> Result<(), InternalImageError> {
|
||||
) -> Result<SuccessEvent, InternalImageError> {
|
||||
|
||||
let api_key = match fal_creds_manager.get_key()? {
|
||||
Some(key) => key,
|
||||
@@ -47,21 +49,32 @@ pub async fn handle_image_fal(
|
||||
|
||||
let prompt = request.prompt.as_deref().unwrap_or("");
|
||||
|
||||
let model;
|
||||
|
||||
let result = match request.model {
|
||||
None => {
|
||||
return Err(InternalImageError::NoModelSpecified);
|
||||
}
|
||||
Some(EnqueueTextToImageModel::GptImage1) => {
|
||||
Some(ImageModel::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(
|
||||
ImageModel::Flux1Dev |
|
||||
ImageModel::Flux1Schnell |
|
||||
ImageModel::FluxPro11
|
||||
) => {
|
||||
return Err(InternalImageError::AnyhowError(anyhow!("not yet implemented: {:?}", request.model)));
|
||||
}
|
||||
Some(ImageModel::FluxPro11Ultra) => {
|
||||
model = ImageModel::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,
|
||||
}).await
|
||||
}
|
||||
Some(EnqueueTextToImageModel::Recraft3) => {
|
||||
Some(ImageModel::Recraft3) => {
|
||||
model = ImageModel::Recraft3;
|
||||
info!("enqueue Recraft v3 text-to-image with prompt: {}", prompt);
|
||||
enqueue_recraft3_text_to_image(Recraft3TextToImageArgs {
|
||||
prompt,
|
||||
@@ -74,15 +87,15 @@ pub async fn handle_image_fal(
|
||||
Ok(enqueued) => {
|
||||
info!("Successfully enqueued text to image");
|
||||
|
||||
let event = GenerationEnqueueSuccessEvent {
|
||||
action: GenerationAction::GenerateImage,
|
||||
service: GenerationServiceProvider::Fal,
|
||||
model: None,
|
||||
};
|
||||
//let event = GenerationEnqueueSuccessEvent {
|
||||
// action: GenerationAction::GenerateImage,
|
||||
// service: GenerationServiceProvider::Fal,
|
||||
// model: None,
|
||||
//};
|
||||
|
||||
if let Err(err) = event.send(app) {
|
||||
error!("Failed to emit event: {:?}", err); // Fail open.
|
||||
}
|
||||
//if let Err(err) = event.send(app) {
|
||||
// error!("Failed to emit event: {:?}", err); // Fail open.
|
||||
//}
|
||||
|
||||
if let Err(err) = fal_task_queue.insert(&enqueued) {
|
||||
error!("Failed to enqueue task: {:?}", err);
|
||||
@@ -107,5 +120,8 @@ pub async fn handle_image_fal(
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(SuccessEvent {
|
||||
service_provider: GenerationServiceProvider::Fal,
|
||||
model,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageModel, EnqueueTextToImageRequest};
|
||||
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageRequest};
|
||||
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
|
||||
@@ -90,15 +90,5 @@ pub async fn handle_image_sora(
|
||||
|
||||
sora_task_queue.insert(&response.task_id)?;
|
||||
|
||||
let event = GenerationEnqueueSuccessEvent {
|
||||
action: GenerationAction::GenerateImage,
|
||||
service: GenerationServiceProvider::Sora,
|
||||
model: None,
|
||||
};
|
||||
|
||||
if let Err(err) = event.send(app) {
|
||||
error!("Failed to emit event: {:?}", err); // Fail open.
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use storyteller_client::error::storyteller_error::StorytellerError;
|
||||
#[derive(Debug)]
|
||||
pub enum InternalImageError {
|
||||
NoModelSpecified,
|
||||
NoProviderAvailable,
|
||||
NeedsFalApiKey,
|
||||
NeedsSoraCredentials,
|
||||
NeedsStorytellerCredentials,
|
||||
|
||||
@@ -2,4 +2,5 @@ pub mod enqueue_text_to_image_command;
|
||||
pub mod handle_image_artcraft;
|
||||
pub mod handle_image_fal;
|
||||
pub mod handle_image_sora;
|
||||
pub mod internal_image_error;
|
||||
pub mod internal_image_error;
|
||||
mod success_event;
|
||||
@@ -0,0 +1,20 @@
|
||||
use crate::core::events::generation_events::common::{GenerationModel, GenerationServiceProvider};
|
||||
use crate::core::model::image_models::ImageModel;
|
||||
|
||||
pub struct SuccessEvent {
|
||||
pub service_provider: GenerationServiceProvider,
|
||||
pub model: ImageModel,
|
||||
}
|
||||
|
||||
impl SuccessEvent {
|
||||
pub fn tauri_event_model(&self) -> GenerationModel {
|
||||
match self.model {
|
||||
ImageModel::Flux1Dev => GenerationModel::Flux1Dev,
|
||||
ImageModel::Flux1Schnell => GenerationModel::Flux1Schnell,
|
||||
ImageModel::FluxPro11 => GenerationModel::FluxPro11,
|
||||
ImageModel::FluxPro11Ultra => GenerationModel::FluxPro11Ultra,
|
||||
ImageModel::GptImage1 => GenerationModel::GptImage1,
|
||||
ImageModel::Recraft3 => GenerationModel::Recraft3,
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
-6
@@ -56,6 +56,9 @@ use storyteller_client::utils::api_host::ApiHost;
|
||||
use tauri::{AppHandle, Emitter, Manager, State};
|
||||
use tempfile::NamedTempFile;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::EnqueueTextToImageErrorType;
|
||||
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
|
||||
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct EnqueueImageTo3dObjectRequest {
|
||||
@@ -70,8 +73,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)]
|
||||
@@ -85,6 +93,8 @@ impl SerializeMarker for EnqueueImageTo3dObjectSuccessResponse {}
|
||||
pub enum EnqueueImageTo3dObjectErrorType {
|
||||
/// Caller didn't specify a model
|
||||
ModelNotSpecified,
|
||||
/// No model available for object generation
|
||||
NoProviderAvailable,
|
||||
/// Generic server error
|
||||
ServerError,
|
||||
/// No Fal API key available
|
||||
@@ -100,6 +110,7 @@ pub async fn enqueue_image_to_3d_object_command(
|
||||
app: AppHandle,
|
||||
request: EnqueueImageTo3dObjectRequest,
|
||||
app_data_root: State<'_, AppDataRoot>,
|
||||
provider_priority_store: State<'_, ProviderPriorityStore>,
|
||||
fal_creds_manager: State<'_, FalCredentialManager>,
|
||||
fal_task_queue: State<'_, FalTaskQueue>,
|
||||
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
|
||||
@@ -112,6 +123,7 @@ pub async fn enqueue_image_to_3d_object_command(
|
||||
&app,
|
||||
request,
|
||||
&app_data_root,
|
||||
&provider_priority_store,
|
||||
&fal_creds_manager,
|
||||
&storyteller_creds_manager,
|
||||
&fal_task_queue,
|
||||
@@ -129,7 +141,12 @@ pub async fn enqueue_image_to_3d_object_command(
|
||||
InternalObjectError::NoModelSpecified => {
|
||||
status = CommandErrorStatus::BadRequest;
|
||||
error_type = EnqueueImageTo3dObjectErrorType::ModelNotSpecified;
|
||||
error_message = "No model specified for image generation";
|
||||
error_message = "No model specified for object generation";
|
||||
}
|
||||
InternalObjectError::NoProviderAvailable => {
|
||||
status = CommandErrorStatus::ServerError;
|
||||
error_type = EnqueueImageTo3dObjectErrorType::NoProviderAvailable;
|
||||
error_message = "No configured provider available for object generation";
|
||||
}
|
||||
InternalObjectError::NeedsFalApiKey => {
|
||||
status = CommandErrorStatus::Unauthorized;
|
||||
@@ -162,16 +179,29 @@ pub async fn handle_request(
|
||||
app: &AppHandle,
|
||||
request: EnqueueImageTo3dObjectRequest,
|
||||
app_data_root: &AppDataRoot,
|
||||
provider_priority_store: &ProviderPriorityStore,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
) -> Result<(), InternalObjectError> {
|
||||
|
||||
if fal_creds_manager.has_apparent_api_token()? {
|
||||
handle_object_fal(&app, app_data_root, request, fal_creds_manager, fal_task_queue).await?;
|
||||
} else {
|
||||
handle_object_artcraft(request, &app, app_data_root, storyteller_creds_manager).await?;
|
||||
let priority = provider_priority_store.get_priority()?;
|
||||
|
||||
for provider in priority.iter() {
|
||||
match provider {
|
||||
Provider::Sora => {} // Fallthrough
|
||||
Provider::Artcraft => {
|
||||
return Ok(handle_object_artcraft(
|
||||
request, &app, app_data_root, storyteller_creds_manager).await?);
|
||||
}
|
||||
Provider::Fal => {
|
||||
if fal_creds_manager.has_apparent_api_token()? {
|
||||
return Ok(handle_object_fal(
|
||||
&app, app_data_root, request, fal_creds_manager, fal_task_queue).await?);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Err(InternalObjectError::NoProviderAvailable)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
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::object::internal_object_error::InternalObjectError;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
@@ -14,14 +13,16 @@ 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::object::generate_hunyuan_2_1_image_to_3d::GenerateHunyuan21ImageTo3dRequest;
|
||||
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::generate::object::generate_hunyuan_3d_2_1_image_to_3d::generate_hunyuan3d_2_1_image_to_3d;
|
||||
use storyteller_client::utils::api_host::ApiHost;
|
||||
use tauri::{AppHandle, State};
|
||||
|
||||
@@ -63,13 +64,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 +89,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");
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
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::object::internal_object_error::InternalObjectError;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
@@ -67,13 +66,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 storyteller_client::error::storyteller_error::StorytellerError;
|
||||
#[derive(Debug)]
|
||||
pub enum InternalObjectError {
|
||||
NoModelSpecified,
|
||||
NoProviderAvailable,
|
||||
NeedsFalApiKey,
|
||||
NeedsSoraCredentials,
|
||||
NeedsStorytellerCredentials,
|
||||
|
||||
+33
-17
@@ -1,12 +1,10 @@
|
||||
use crate::core::commands::enqueue::object::handle_object_artcraft::handle_object_artcraft;
|
||||
use crate::core::commands::enqueue::object::handle_object_fal::handle_object_fal;
|
||||
use crate::core::commands::enqueue::video::handle_video_artcraft::handle_video_artcraft;
|
||||
use crate::core::commands::enqueue::video::handle_video_fal::handle_video_fal;
|
||||
use crate::core::commands::enqueue::video::internal_video_error::InternalVideoError;
|
||||
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::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;
|
||||
@@ -58,6 +56,9 @@ use storyteller_client::utils::api_host::ApiHost;
|
||||
use tauri::{AppHandle, Emitter, Manager, State};
|
||||
use tempfile::NamedTempFile;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use crate::core::commands::enqueue::object::enqueue_image_to_3d_object_command::EnqueueImageTo3dObjectErrorType;
|
||||
use crate::core::commands::enqueue::object::internal_object_error::InternalObjectError;
|
||||
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct EnqueueImageToVideoRequest {
|
||||
@@ -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)]
|
||||
@@ -87,6 +81,8 @@ impl SerializeMarker for EnqueueImageToVideoSuccessResponse {}
|
||||
pub enum EnqueueImageToVideoErrorType {
|
||||
/// Caller didn't specify a model
|
||||
ModelNotSpecified,
|
||||
/// No model available for video generation
|
||||
NoProviderAvailable,
|
||||
/// Generic server error
|
||||
ServerError,
|
||||
/// No Fal API key available
|
||||
@@ -102,6 +98,7 @@ pub async fn enqueue_image_to_video_command(
|
||||
app: AppHandle,
|
||||
request: EnqueueImageToVideoRequest,
|
||||
app_data_root: State<'_, AppDataRoot>,
|
||||
provider_priority_store: State<'_, ProviderPriorityStore>,
|
||||
fal_creds_manager: State<'_, FalCredentialManager>,
|
||||
fal_task_queue: State<'_, FalTaskQueue>,
|
||||
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
|
||||
@@ -114,6 +111,7 @@ pub async fn enqueue_image_to_video_command(
|
||||
&app,
|
||||
request,
|
||||
&app_data_root,
|
||||
&provider_priority_store,
|
||||
&fal_creds_manager,
|
||||
&storyteller_creds_manager,
|
||||
&fal_task_queue,
|
||||
@@ -131,7 +129,12 @@ pub async fn enqueue_image_to_video_command(
|
||||
InternalVideoError::NoModelSpecified => {
|
||||
status = CommandErrorStatus::BadRequest;
|
||||
error_type = EnqueueImageToVideoErrorType::ModelNotSpecified;
|
||||
error_message = "No model specified for image generation";
|
||||
error_message = "No model specified for video generation";
|
||||
}
|
||||
InternalVideoError::NoProviderAvailable => {
|
||||
status = CommandErrorStatus::ServerError;
|
||||
error_type = EnqueueImageToVideoErrorType::NoProviderAvailable;
|
||||
error_message = "No configured provider available for video generation";
|
||||
}
|
||||
InternalVideoError::NeedsFalApiKey => {
|
||||
status = CommandErrorStatus::Unauthorized;
|
||||
@@ -164,16 +167,29 @@ pub async fn handle_request(
|
||||
app: &AppHandle,
|
||||
request: EnqueueImageToVideoRequest,
|
||||
app_data_root: &AppDataRoot,
|
||||
provider_priority_store: &ProviderPriorityStore,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
) -> Result<(), InternalVideoError> {
|
||||
|
||||
if fal_creds_manager.has_apparent_api_token()? {
|
||||
handle_video_fal(&app, app_data_root, request, fal_creds_manager, fal_task_queue).await?;
|
||||
} else {
|
||||
handle_video_artcraft(request, &app, app_data_root, storyteller_creds_manager).await?;
|
||||
let priority = provider_priority_store.get_priority()?;
|
||||
|
||||
for provider in priority.iter() {
|
||||
match provider {
|
||||
Provider::Sora => {} // Fallthrough
|
||||
Provider::Artcraft => {
|
||||
return Ok(handle_video_artcraft(
|
||||
request, &app, app_data_root, storyteller_creds_manager).await?);
|
||||
}
|
||||
Provider::Fal => {
|
||||
if fal_creds_manager.has_apparent_api_token()? {
|
||||
return Ok(handle_video_fal(
|
||||
&app, app_data_root, request, fal_creds_manager, fal_task_queue).await?);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Err(InternalVideoError::NoProviderAvailable)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
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 +15,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 +78,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 +104,110 @@ 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,10 @@
|
||||
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 +60,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 +68,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 +79,9 @@ pub async fn handle_video_fal(
|
||||
prompt: "",
|
||||
}).await
|
||||
}
|
||||
_ => {
|
||||
return Err(InternalVideoError::AnyhowError(anyhow!("Unsupported model: {:?}", request.model)));
|
||||
}
|
||||
};
|
||||
|
||||
match result {
|
||||
|
||||
@@ -7,6 +7,7 @@ use storyteller_client::error::storyteller_error::StorytellerError;
|
||||
#[derive(Debug)]
|
||||
pub enum InternalVideoError {
|
||||
NoModelSpecified,
|
||||
NoProviderAvailable,
|
||||
NeedsFalApiKey,
|
||||
NeedsSoraCredentials,
|
||||
NeedsStorytellerCredentials,
|
||||
|
||||
+13
-5
@@ -1,5 +1,6 @@
|
||||
use crate::core::commands::response::shorthand::InfallibleResponse;
|
||||
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::os_platform::OsPlatform;
|
||||
use crate::services::fal::commands::fal_background_removal_command::FalBackgroundRemovalSuccessResponse;
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
@@ -10,31 +11,38 @@ use image::ImageReader;
|
||||
use log::info;
|
||||
use serde_derive::Serialize;
|
||||
use std::io::Cursor;
|
||||
use tauri::State;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct BuildInfoResponse {
|
||||
pub struct AppInfoResponse {
|
||||
pub build_timestamp: DateTime<Utc>,
|
||||
pub git_commit_id: Option<String>,
|
||||
pub git_commit_short_id: Option<String>,
|
||||
pub git_commit_timestamp: Option<DateTime<Utc>>,
|
||||
pub storyteller_host: String,
|
||||
}
|
||||
|
||||
impl SerializeMarker for BuildInfoResponse {}
|
||||
impl SerializeMarker for AppInfoResponse {}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_build_info_command() -> InfallibleResponse<BuildInfoResponse> {
|
||||
info!("get_build_info_command called...");
|
||||
pub fn get_app_info_command(
|
||||
app_env_configs: State<'_, AppEnvConfigs>,
|
||||
) -> InfallibleResponse<AppInfoResponse> {
|
||||
info!("get_app_info_command called...");
|
||||
|
||||
let build_timestamp = build_metadata::build_timestamp();
|
||||
let git_commit_id = build_metadata::git_commit_id();
|
||||
let git_commit_short_id = build_metadata::git_commit_short_id();
|
||||
let git_commit_timestamp = build_metadata::git_commit_timestamp();
|
||||
|
||||
BuildInfoResponse {
|
||||
let storyteller_host = app_env_configs.storyteller_host.to_api_hostname();
|
||||
|
||||
AppInfoResponse {
|
||||
build_timestamp,
|
||||
git_commit_id: git_commit_id.map(|s| s.to_string()),
|
||||
git_commit_short_id: git_commit_short_id.map(|s| s.to_string()),
|
||||
git_commit_timestamp,
|
||||
storyteller_host,
|
||||
}.into()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
pub mod app_preferences;
|
||||
pub mod enqueue;
|
||||
pub mod flip_image;
|
||||
pub mod get_build_info_command;
|
||||
pub mod get_app_info_command;
|
||||
pub mod platform_info_command;
|
||||
pub mod providers;
|
||||
pub mod response;
|
||||
@@ -0,0 +1,33 @@
|
||||
use crate::core::commands::response::shorthand::ResponseOrErrorMessage;
|
||||
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
|
||||
use crate::core::state::app_preferences::app_preferences_manager::AppPreferencesManager;
|
||||
use crate::core::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
|
||||
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
use errors::AnyhowResult;
|
||||
use log::{error, info};
|
||||
use serde_derive::Serialize;
|
||||
use tauri::{AppHandle, State};
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct GetProviderOrderResponse {
|
||||
pub providers: Vec<Provider>,
|
||||
}
|
||||
|
||||
impl SerializeMarker for GetProviderOrderResponse{}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_provider_order_command(
|
||||
provider_priority_store: State<'_, ProviderPriorityStore>,
|
||||
) -> ResponseOrErrorMessage<GetProviderOrderResponse> {
|
||||
info!("get_provider_order_command called");
|
||||
|
||||
let providers = provider_priority_store.get_priority()
|
||||
.map_err(|err| {
|
||||
error!("Failed to get provider order: {:?}", err);
|
||||
"Failed to retrieve provider order"
|
||||
})?;
|
||||
|
||||
Ok(GetProviderOrderResponse {
|
||||
providers,
|
||||
}.into())
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod get_provider_order_command;
|
||||
pub mod set_provider_order_command;
|
||||
@@ -0,0 +1,39 @@
|
||||
use crate::core::commands::response::shorthand::{ResponseOrErrorMessage, SuccessOrErrorMessage};
|
||||
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
|
||||
use crate::core::model::image_models::ImageModel;
|
||||
use crate::core::state::app_preferences::app_preferences_manager::AppPreferencesManager;
|
||||
use crate::core::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
use errors::AnyhowResult;
|
||||
use log::{error, info};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, State};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SetProviderOrderRequest {
|
||||
pub providers: Vec<Provider>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn set_provider_order_command(
|
||||
request: SetProviderOrderRequest,
|
||||
provider_priority_store: State<'_, ProviderPriorityStore>,
|
||||
app_data_root: State<'_, AppDataRoot>,
|
||||
) -> SuccessOrErrorMessage {
|
||||
info!("set_provider_order_command called");
|
||||
|
||||
provider_priority_store.set_priority(&request.providers)
|
||||
.map_err(|err| {
|
||||
error!("Failed to set provider order: {:?}", err);
|
||||
"Failed to set provider order"
|
||||
})?;
|
||||
|
||||
provider_priority_store.persist_to_filesystem(&app_data_root)
|
||||
.map_err(|err| {
|
||||
error!("Failed to persist provider order to filesystem: {:?}", err);
|
||||
"Failed to persist provider order"
|
||||
})?;
|
||||
|
||||
Ok(().into())
|
||||
}
|
||||
@@ -30,3 +30,19 @@ pub type Response<SuccessPayload, ErrType, ErrPayload> =
|
||||
|
||||
/// No inner payloads for this type. Just strings as messages.
|
||||
pub type SimpleResponse = Response<(), (), ()>;
|
||||
|
||||
/// Either an empty success payload or error message.
|
||||
pub type SuccessOrErrorMessage =
|
||||
Result<CommandSuccessResponseWrapper<()>, CommandErrorResponseWrapper<(), ()>>;
|
||||
|
||||
/// Either a success or error message.
|
||||
pub type ResponseOrErrorMessage<SuccessPayload> =
|
||||
Result<CommandSuccessResponseWrapper<SuccessPayload>, CommandErrorResponseWrapper<(), ()>>;
|
||||
|
||||
/// Either a success or error type.
|
||||
pub type ResponseOrErrorType<SuccessPayload, ErrType> =
|
||||
Result<CommandSuccessResponseWrapper<SuccessPayload>, CommandErrorResponseWrapper<ErrType, ()>>;
|
||||
|
||||
/// Either a success or error payload.
|
||||
pub type ResponseOrError<SuccessPayload, ErrPayload> =
|
||||
Result<CommandSuccessResponseWrapper<SuccessPayload>, CommandErrorResponseWrapper<(), ErrPayload>>;
|
||||
|
||||
@@ -11,13 +11,32 @@ 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 = "gpt_image_1")]
|
||||
GptImage1,
|
||||
#[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,
|
||||
#[serde(rename = "recraft_3")]
|
||||
Recraft3,
|
||||
#[serde(rename = "sora")]
|
||||
Sora,
|
||||
#[serde(rename = "seedance_1.0_lite")]
|
||||
Seedance10Lite,
|
||||
#[serde(rename = "veo_2")]
|
||||
Veo2,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize)]
|
||||
|
||||
@@ -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,20 @@
|
||||
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 ImageModel {
|
||||
#[serde(rename = "flux_1_dev")]
|
||||
Flux1Dev,
|
||||
#[serde(rename = "flux_1_schnell")]
|
||||
Flux1Schnell,
|
||||
#[serde(rename = "flux_pro_11")]
|
||||
FluxPro11,
|
||||
#[serde(rename = "flux_pro_11_ultra")]
|
||||
FluxPro11Ultra,
|
||||
#[serde(rename = "gpt_image_1")]
|
||||
GptImage1,
|
||||
#[serde(rename = "recraft_3")]
|
||||
Recraft3,
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
+34
@@ -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,15 @@ 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 get_provider_preferences_path(&self) -> PathBuf {
|
||||
self.path.join("provider_preferences.json")
|
||||
}
|
||||
|
||||
pub fn write_app_preferences(&self, prefs: &AppPreferences) -> AnyhowResult<()> {
|
||||
let serializable = prefs.to_serializable();
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
pub mod app_env_configs;
|
||||
pub mod app_preferences;
|
||||
pub mod app_startup_time;
|
||||
pub mod data_dir;
|
||||
pub mod expanduser;
|
||||
pub mod main_window_position;
|
||||
pub mod main_window_size;
|
||||
pub mod os_platform;
|
||||
pub mod os_platform;
|
||||
pub mod provider_priority;
|
||||
@@ -0,0 +1,162 @@
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use errors::AnyhowResult;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Provider {
|
||||
Artcraft,
|
||||
Fal,
|
||||
Sora,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct ProviderPriority {
|
||||
/// Providers may occur zero or one time.
|
||||
/// They are ordered by priority, highest to lowest.
|
||||
provider_priority: Vec<Provider>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ProviderPriorityStore {
|
||||
provider_priority: Arc<RwLock<ProviderPriority>>,
|
||||
}
|
||||
|
||||
impl ProviderPriority {
|
||||
pub fn default() -> Self {
|
||||
Self {
|
||||
provider_priority: vec![
|
||||
Provider::Artcraft,
|
||||
Provider::Sora,
|
||||
Provider::Fal,
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_priority(&mut self, ordered_list: &[Provider]) -> AnyhowResult<()> {
|
||||
let set : HashSet<&Provider> = HashSet::from_iter(ordered_list.iter());
|
||||
|
||||
if set.len() != ordered_list.len() {
|
||||
return Err(anyhow::anyhow!("Duplicate providers in the ordered list"));
|
||||
}
|
||||
|
||||
self.provider_priority = ordered_list.to_vec();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_priority(&self) -> &[Provider] {
|
||||
&self.provider_priority
|
||||
}
|
||||
|
||||
pub fn from_filesystem_configs(app_data_root: &AppDataRoot) -> AnyhowResult<Option<Self>> {
|
||||
let filename = app_data_root.settings_dir().get_provider_preferences_path();
|
||||
if !filename.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
let contents = std::fs::read_to_string(filename)?;
|
||||
let providers : Self = serde_json::from_str(&contents)?;
|
||||
Ok(Some(providers))
|
||||
}
|
||||
|
||||
pub fn persist_to_filesystem(&self, app_data_root: &AppDataRoot) -> AnyhowResult<()> {
|
||||
let filename = app_data_root.settings_dir().get_provider_preferences_path();
|
||||
let json = serde_json::to_string(self)?;
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(filename)?;
|
||||
file.write_all(json.as_bytes())?;
|
||||
file.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ProviderPriorityStore {
|
||||
pub fn default() -> Self {
|
||||
Self {
|
||||
provider_priority: Arc::new(RwLock::new(ProviderPriority::default())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_priority(&self, ordered_list: &[Provider]) -> AnyhowResult<()> {
|
||||
match self.provider_priority.write() {
|
||||
Err(err) => {
|
||||
Err(anyhow::anyhow!("Failed to acquire write lock on provider priority: {}", err))
|
||||
}
|
||||
Ok(mut providers) => {
|
||||
providers.set_priority(ordered_list)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_priority(&self) -> AnyhowResult<Vec<Provider>> {
|
||||
match self.provider_priority.read() {
|
||||
Err(err) => {
|
||||
Err(anyhow::anyhow!("Failed to acquire read lock on provider priority: {}", err))
|
||||
}
|
||||
Ok(providers) => {
|
||||
Ok(providers.get_priority().to_vec())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_filesystem_configs(app_data_root: &AppDataRoot) -> AnyhowResult<Option<Self>> {
|
||||
match ProviderPriority::from_filesystem_configs(app_data_root) {
|
||||
Err(err) => {
|
||||
Err(anyhow::anyhow!("Failed to read provider preferences from filesystem: {}", err))
|
||||
}
|
||||
Ok(None) => Ok(None),
|
||||
Ok(Some(providers)) => {
|
||||
Ok(Some(Self {
|
||||
provider_priority: Arc::new(RwLock::new(providers)),
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn persist_to_filesystem(&self, app_data_root: &AppDataRoot) -> AnyhowResult<()> {
|
||||
match self.provider_priority.read() {
|
||||
Err(err) => {
|
||||
Err(anyhow::anyhow!("Failed to acquire read lock on provider priority: {}", err))
|
||||
}
|
||||
Ok(providers) => {
|
||||
providers.persist_to_filesystem(app_data_root)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_default_priority() {
|
||||
let priority = ProviderPriority::default();
|
||||
assert_eq!(priority.get_priority(), &[Provider::Artcraft, Provider::Sora, Provider::Fal]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_priority() -> AnyhowResult<()> {
|
||||
let mut priority = ProviderPriority::default();
|
||||
priority.set_priority(&[Provider::Fal, Provider::Artcraft])?;
|
||||
assert_eq!(priority.get_priority(), &[Provider::Fal, Provider::Artcraft]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_priority_with_duplicates() {
|
||||
let mut priority = ProviderPriority::default();
|
||||
let result = priority.set_priority(&[Provider::Artcraft, Provider::Artcraft]);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,14 @@ use crate::core::state::app_startup_time::AppStartupTime;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::core::state::main_window_position::MainWindowPosition;
|
||||
use crate::core::state::main_window_size::MainWindowSize;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use crate::core::threads::main_window_thread::persist_storyteller_cookies_task::persist_storyteller_cookies_task;
|
||||
use crate::core::threads::main_window_thread::persist_window_position_task::persist_window_position_task;
|
||||
use crate::core::threads::main_window_thread::persist_window_resize_task::persist_window_resize_task;
|
||||
use crate::core::utils::clear_all_webview_cookies::clear_all_webview_cookies;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use anyhow::anyhow;
|
||||
use errors::AnyhowResult;
|
||||
use log::{error, info};
|
||||
use log::{error, info, warn};
|
||||
use memory_store::clone_slot::CloneSlot;
|
||||
use tauri::{AppHandle, Manager, Webview, Window};
|
||||
|
||||
@@ -31,6 +33,8 @@ pub async fn main_window_thread(
|
||||
.unwrap_or_else(|| "unknown".to_string()));
|
||||
info!("build timestamp: {:?}", build_metadata::build_timestamp()
|
||||
.to_string());
|
||||
|
||||
// debug_try_clear_all_webview_data(&app);
|
||||
|
||||
loop {
|
||||
for (window_name, window) in app.windows() {
|
||||
@@ -75,3 +79,15 @@ pub fn log_errors<T>(result: AnyhowResult<T>) {
|
||||
error!("Error persisting window size: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
fn debug_try_clear_all_webview_data(app: &AppHandle) {
|
||||
warn!("[!!!] THIS IS ONLY FOR DEBUGGING PURPOSES: Attempting to clear webview data...");
|
||||
|
||||
for (name, webview) in app.webviews() {
|
||||
if let Err(err) = webview.clear_all_browsing_data() {
|
||||
error!("Failed to clear cookies for '{}' window webview: {:?}", name, err);
|
||||
} else {
|
||||
warn!("Successfully cleared cookies for '{}' window webview", name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,16 @@ use crate::core::commands::enqueue::image::enqueue_text_to_image_command::enqueu
|
||||
use crate::core::commands::enqueue::object::enqueue_image_to_3d_object_command::enqueue_image_to_3d_object_command;
|
||||
use crate::core::commands::enqueue::video::enqueue_image_to_video_command::enqueue_image_to_video_command;
|
||||
use crate::core::commands::flip_image::flip_image;
|
||||
use crate::core::commands::get_build_info_command::get_build_info_command;
|
||||
use crate::core::commands::get_app_info_command::get_app_info_command;
|
||||
use crate::core::commands::platform_info_command::platform_info_command;
|
||||
use crate::core::commands::providers::get_provider_order_command::get_provider_order_command;
|
||||
use crate::core::commands::providers::set_provider_order_command::set_provider_order_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;
|
||||
use crate::core::state::main_window_size::MainWindowSize;
|
||||
use crate::core::state::provider_priority::ProviderPriorityStore;
|
||||
use crate::core::threads::discord_presence_thread::discord_presence_thread;
|
||||
use crate::core::threads::main_window_thread::main_window_thread::main_window_thread;
|
||||
use crate::core::utils::webview_unsafe::webview_unsafe_for_app;
|
||||
@@ -52,8 +56,10 @@ pub fn run() {
|
||||
println!("Loading app preferences...");
|
||||
let app_preferences = load_app_preferences_or_default(&app_data_root);
|
||||
|
||||
println!("Attempting to read existing artcraft credentials...");
|
||||
let storyteller_creds_manager = StorytellerCredentialManager::initialize_from_disk_infallible(&app_data_root);
|
||||
// NB: tauri-plugin-http stores the credentials on disk, so we can defer to that for now.
|
||||
// println!("Attempting to read existing artcraft credentials...");
|
||||
// let storyteller_creds_manager = StorytellerCredentialManager::initialize_from_disk_infallible(&app_data_root);
|
||||
let storyteller_creds_manager = StorytellerCredentialManager::initialize_empty(&app_data_root);
|
||||
let storyteller_creds_manager_2 = storyteller_creds_manager.clone();
|
||||
let storyteller_creds_manager_3 = storyteller_creds_manager.clone();
|
||||
|
||||
@@ -71,6 +77,24 @@ 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");
|
||||
|
||||
let provider_priority = match ProviderPriorityStore::from_filesystem_configs(&app_data_root) {
|
||||
Ok(Some(priority)) => {
|
||||
println!("Loaded provider priority from disk: {:?}", priority.get_priority());
|
||||
priority
|
||||
}
|
||||
Ok(None) => {
|
||||
println!("No provider priority found on disk, using default.");
|
||||
ProviderPriorityStore::default()
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Failed to read provider priority from disk: {:?}", err);
|
||||
ProviderPriorityStore::default()
|
||||
}
|
||||
};
|
||||
|
||||
println!("Initializing backend runtime...");
|
||||
|
||||
@@ -155,9 +179,11 @@ pub fn run() {
|
||||
Ok(())
|
||||
})
|
||||
.manage(app_data_root)
|
||||
.manage(app_env_configs)
|
||||
.manage(app_preferences)
|
||||
.manage(fal_creds_manager)
|
||||
.manage(fal_task_queue)
|
||||
.manage(provider_priority)
|
||||
.manage(sora_creds_manager)
|
||||
.manage(sora_task_queue)
|
||||
.manage(storyteller_creds_manager_3)
|
||||
@@ -170,9 +196,11 @@ pub fn run() {
|
||||
fal_hunyuan_image_to_3d_command,
|
||||
fal_kling_image_to_video_command,
|
||||
flip_image,
|
||||
get_app_info_command,
|
||||
get_app_preferences_command,
|
||||
get_build_info_command,
|
||||
get_fal_api_key_command,
|
||||
get_provider_order_command,
|
||||
set_provider_order_command,
|
||||
open_sora_login_command,
|
||||
platform_info_command,
|
||||
set_fal_api_key_command,
|
||||
|
||||
@@ -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;
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::services::fal::state::read_fal_api_key_from_disk::read_fal_api_key_from_disk;
|
||||
use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::services::fal::state::read_fal_api_key_from_disk::read_fal_api_key_from_disk;
|
||||
use anyhow::anyhow;
|
||||
use errors::AnyhowResult;
|
||||
use fal_client::creds::fal_api_key::FalApiKey;
|
||||
@@ -42,6 +42,9 @@ impl FalCredentialManager {
|
||||
}
|
||||
|
||||
pub fn set_key(&self, key: &FalApiKey) -> AnyhowResult<()> {
|
||||
if key.0.trim().is_empty() {
|
||||
return Err(anyhow!("FAL API key cannot be empty")); // TODO: Also handle upstream.
|
||||
}
|
||||
self.key.set_clone(key)
|
||||
}
|
||||
|
||||
|
||||
+7
@@ -20,6 +20,13 @@ pub struct StorytellerCredentialManager {
|
||||
|
||||
impl StorytellerCredentialManager {
|
||||
|
||||
pub fn initialize_empty(app_data_root: &AppDataRoot) -> Self {
|
||||
Self {
|
||||
holder: StorytellerCredentialHolder::new(),
|
||||
app_data_root: app_data_root.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initialize_from_disk_infallible(app_data_root: &AppDataRoot) -> Self {
|
||||
let holder = StorytellerCredentialHolder::new();
|
||||
|
||||
|
||||
@@ -21,7 +21,10 @@ pub enum ApiError {
|
||||
TooManyRequests(String),
|
||||
|
||||
/// 500. An internal server error occurred.
|
||||
InternalServerError(String),
|
||||
InternalServerError {
|
||||
body: String,
|
||||
backend_hostname: Option<String>,
|
||||
},
|
||||
|
||||
/// A deserialization error with the response.
|
||||
DeserializationError(serde_json::Error),
|
||||
@@ -53,7 +56,8 @@ impl Display for ApiError {
|
||||
ApiError::Forbidden(msg) => write!(f, "Forbidden: {}", msg),
|
||||
ApiError::NotFound(msg) => write!(f, "Not found: {}", msg),
|
||||
ApiError::TooManyRequests(msg) => write!(f, "Too many requests: {}", msg),
|
||||
ApiError::InternalServerError(msg) => write!(f, "Internal server error: {}", msg),
|
||||
ApiError::InternalServerError {body, backend_hostname} =>
|
||||
write!(f, "Internal Server Error; backend hostname: {:?} ; body: {}; ", backend_hostname, body),
|
||||
// Server response handling errors
|
||||
ApiError::DeserializationError(error) => write!(f, "Deserialization error: {}", error),
|
||||
// Network errors
|
||||
|
||||
+19
@@ -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?)
|
||||
}
|
||||
+19
@@ -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?)
|
||||
}
|
||||
+19
@@ -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?)
|
||||
}
|
||||
+7
-57
@@ -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;
|
||||
+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::object::generate_hunyuan_2_0_image_to_3d::{GenerateHunyuan20ImageTo3dRequest, GenerateHunyuan20ImageTo3dResponse, GENERATE_HUNYUAN_2_0_IMAGE_TO_3D_URL_PATH};
|
||||
|
||||
pub async fn generate_hunyuan3d_2_0_image_to_3d(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: GenerateHunyuan20ImageTo3dRequest,
|
||||
) -> Result<GenerateHunyuan20ImageTo3dResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
GENERATE_HUNYUAN_2_0_IMAGE_TO_3D_URL_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
+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::object::generate_hunyuan_2_1_image_to_3d::{GenerateHunyuan21ImageTo3dRequest, GenerateHunyuan21ImageTo3dResponse, GENERATE_HUNYUAN_2_1_IMAGE_TO_3D_URL_PATH};
|
||||
|
||||
pub async fn generate_hunyuan3d_2_1_image_to_3d(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: GenerateHunyuan21ImageTo3dRequest,
|
||||
) -> Result<GenerateHunyuan21ImageTo3dResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
GENERATE_HUNYUAN_2_1_IMAGE_TO_3D_URL_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
@@ -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;
|
||||
+7
-57
@@ -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::video::generate_kling_1_6_pro_image_to_video::{GenerateKling16ProImageToVideoRequest, GenerateKling16ProImageToVideoResponse, GENERATE_KLING_1_6_PRO_IMAGE_TO_VIDEO_URL_PATH};
|
||||
use log::{debug, info};
|
||||
use reqwest::Client;
|
||||
|
||||
pub async fn generate_kling_16_pro_image_to_video(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: GenerateKling16ProImageToVideoRequest,
|
||||
) -> Result<GenerateKling16ProImageToVideoResponse, 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_1_6_PRO_IMAGE_TO_VIDEO_URL_PATH)
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
GENERATE_KLING_1_6_PRO_IMAGE_TO_VIDEO_URL_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
|
||||
+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::video::generate_kling_2_1_master_image_to_video::{GenerateKling21MasterImageToVideoRequest, GenerateKling21MasterImageToVideoResponse, GENERATE_KLING_2_1_MASTER_IMAGE_TO_VIDEO_URL_PATH};
|
||||
|
||||
pub async fn generate_kling_21_master_image_to_video(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: GenerateKling21MasterImageToVideoRequest,
|
||||
) -> Result<GenerateKling21MasterImageToVideoResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
GENERATE_KLING_2_1_MASTER_IMAGE_TO_VIDEO_URL_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
+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::video::generate_kling_2_1_pro_image_to_video::{GenerateKling21ProImageToVideoRequest, GenerateKling21ProImageToVideoResponse, GENERATE_KLING_2_1_PRO_IMAGE_TO_VIDEO_URL_PATH};
|
||||
|
||||
pub async fn generate_kling_21_pro_image_to_video(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: GenerateKling21ProImageToVideoRequest,
|
||||
) -> Result<GenerateKling21ProImageToVideoResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
GENERATE_KLING_2_1_PRO_IMAGE_TO_VIDEO_URL_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
+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::video::generate_seedance_1_0_lite_image_to_video::{GenerateSeedance10LiteImageToVideoRequest, GenerateSeedance10LiteImageToVideoResponse, GENERATE_SEEDANCE_1_0_LITE_IMAGE_TO_VIDEO_URL_PATH};
|
||||
|
||||
pub async fn generate_seedance_1_0_lite_image_to_video(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: GenerateSeedance10LiteImageToVideoRequest,
|
||||
) -> Result<GenerateSeedance10LiteImageToVideoResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
GENERATE_SEEDANCE_1_0_LITE_IMAGE_TO_VIDEO_URL_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
+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::video::generate_veo_2_image_to_video::{GenerateVeo2ImageToVideoRequest, GenerateVeo2ImageToVideoResponse, GENERATE_VEO_2_IMAGE_TO_VIDEO_URL_PATH};
|
||||
|
||||
pub async fn generate_veo_2_image_to_video(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: GenerateVeo2ImageToVideoRequest,
|
||||
) -> Result<GenerateVeo2ImageToVideoResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
GENERATE_VEO_2_IMAGE_TO_VIDEO_URL_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+15
-12
@@ -5,17 +5,20 @@ 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::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 generate_hunyuan2_image_to_3d(
|
||||
pub async fn basic_json_post_request<Req: Serialize, Res: DeserializeOwned>(
|
||||
api_host: &ApiHost,
|
||||
route_path: &str,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: GenerateHunyuan2ImageTo3dRequest,
|
||||
) -> Result<GenerateHunyuan2ImageTo3dResponse, StorytellerError> {
|
||||
request: Req,
|
||||
) -> Result<Res, StorytellerError> {
|
||||
|
||||
let url = get_route(api_host);
|
||||
let url = get_route(api_host, route_path);
|
||||
|
||||
info!("Requesting {:?}", &url);
|
||||
|
||||
@@ -34,12 +37,12 @@ pub async fn generate_hunyuan2_image_to_3d(
|
||||
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()
|
||||
@@ -49,20 +52,20 @@ pub async fn generate_hunyuan2_image_to_3d(
|
||||
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_IMAGE_TO_3D_URL_PATH)
|
||||
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,6 +1,7 @@
|
||||
use crate::error::api_error::ApiError;
|
||||
use crate::utils::status_codes::*;
|
||||
use anyhow::anyhow;
|
||||
use log::error;
|
||||
|
||||
/// Pass request errors to this method for standard error handling behavior.
|
||||
/// This function needs to take temporary ownership, but will return it to the caller.
|
||||
@@ -13,6 +14,15 @@ pub async fn filter_bad_response(response: reqwest::Response) -> Result<reqwest:
|
||||
let status = response.status();
|
||||
|
||||
if !status.is_success() {
|
||||
// The host in the fleet that handled the request.
|
||||
let backend_hostname= response.headers()
|
||||
.get("x-backend-hostname")
|
||||
.map(|header| header.to_str().unwrap_or_else(|err| {
|
||||
error!("Failed to parse x-backend-hostname header: {:?}", err);
|
||||
"unknown"
|
||||
}))
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let response_body = match response.text().await {
|
||||
Ok(text) => text.to_string(),
|
||||
Err(err) => format!("Could not read response body: {:?}", err),
|
||||
@@ -23,7 +33,7 @@ pub async fn filter_bad_response(response: reqwest::Response) -> Result<reqwest:
|
||||
STATUS_403_FORBIDDEN => Err(ApiError::Forbidden(response_body)),
|
||||
STATUS_404_NOT_FOUND => Err(ApiError::NotFound(response_body)),
|
||||
STATUS_429_TOO_MANY_REQUESTS => Err(ApiError::TooManyRequests(response_body)),
|
||||
STATUS_500_INTERNAL_SERVER_ERROR => Err(ApiError::InternalServerError(response_body)),
|
||||
STATUS_500_INTERNAL_SERVER_ERROR => Err(ApiError::InternalServerError { body: response_body, backend_hostname }),
|
||||
_ => Err(ApiError::Other(anyhow!("Bad status code: {}; message: {:?}", status, response_body))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
-3
@@ -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,
|
||||
|
||||
+7
-7
@@ -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,
|
||||
}))
|
||||
+3
-3
@@ -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;
|
||||
+12
-3
@@ -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))
|
||||
|
||||
+124
@@ -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()))
|
||||
)
|
||||
)
|
||||
|
||||
+4
@@ -41,6 +41,10 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
std::fs::create_dir_all(&cache_dir)?;
|
||||
|
||||
let path = cache_dir.join(COOKIES_FILENAME);
|
||||
|
||||
// NB(bt): Let's always log this.
|
||||
println!("Tauri plugin http: using cookie store at {}", path.display());
|
||||
|
||||
let file = File::options()
|
||||
.create(true)
|
||||
.append(true)
|
||||
|
||||
@@ -2,19 +2,14 @@ import { useSignals } from "@preact/signals-react/runtime";
|
||||
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";
|
||||
// import ProfileDropdown from "./ProfileDropdown";
|
||||
|
||||
export const AuthButtons = () => {
|
||||
export const AuthButtons = ({
|
||||
loginSignUpPressed,
|
||||
}: {
|
||||
loginSignUpPressed: () => void;
|
||||
}) => {
|
||||
useSignals();
|
||||
const [showLoginModal, setShowLoginModal] = useState(false);
|
||||
const [isSignUp, setIsSignUp] = useState(false);
|
||||
|
||||
const { status, userInfo } = authentication;
|
||||
|
||||
@@ -22,7 +17,7 @@ export const AuthButtons = () => {
|
||||
console.log(status.value);
|
||||
if (status.value === AUTH_STATUS.LOGGED_IN) {
|
||||
console.log("SHOWING DROPDOWN");
|
||||
return <ProfileDropdown />;
|
||||
return null;
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
@@ -31,34 +26,12 @@ 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);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import {
|
||||
faGear,
|
||||
faImages,
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
faFilm,
|
||||
faPaintbrush,
|
||||
faImage,
|
||||
// faGem,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { AuthButtons } from "./AuthButtons";
|
||||
@@ -18,7 +19,7 @@ import {
|
||||
} from "@storyteller/ui-gallery-modal";
|
||||
import { SettingsModal } from "@storyteller/ui-settings-modal";
|
||||
import { Tooltip } from "@storyteller/ui-tooltip";
|
||||
import { downloadFileFromUrl } from "@storyteller/api";
|
||||
import { downloadFileFromUrl, FilterMediaClasses } from "@storyteller/api";
|
||||
import {
|
||||
MenuIconSelector,
|
||||
MenuIconItem,
|
||||
@@ -34,10 +35,15 @@ import {
|
||||
is3DSceneLoaded,
|
||||
set3DPageMounted,
|
||||
} from "~/pages/PageEnigma/Editor/editor";
|
||||
// import { usePricingModalStore } from "@storyteller/ui-pricing-modal"; - Uncomment for pricing modal - BFlat
|
||||
import toast from "react-hot-toast";
|
||||
interface Props {
|
||||
pageName: string;
|
||||
loginSignUpPressed: () => void;
|
||||
}
|
||||
|
||||
const SWITCHER_THROTTLE_TIME = 500; // milliseconds
|
||||
|
||||
const appMenuTabs: MenuIconItem[] = [
|
||||
{
|
||||
id: "2D",
|
||||
@@ -70,7 +76,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);
|
||||
@@ -84,14 +90,33 @@ export const TopBar = ({ pageName }: Props) => {
|
||||
|
||||
const is3DSceneReady = is3DSceneLoaded.value;
|
||||
const is3DEditorReady = is3DEditorInitialized.value;
|
||||
const [disableSwitcher, setDisableSwitcher] = useState(false);
|
||||
const switcherThrottle = useRef(false);
|
||||
|
||||
const disableTabSwitcher = () => {
|
||||
return (
|
||||
useTabStore.getState().activeTabId === "3D" &&
|
||||
!is3DEditorReady &&
|
||||
!is3DSceneReady
|
||||
disableSwitcher ||
|
||||
(useTabStore.getState().activeTabId === "3D" &&
|
||||
!is3DEditorReady &&
|
||||
!is3DSceneReady)
|
||||
);
|
||||
};
|
||||
|
||||
const downloadFile = async (url: string, mediaClass?: string) => {
|
||||
try {
|
||||
await downloadFileFromUrl(url);
|
||||
if (mediaClass === FilterMediaClasses.DIMENSIONAL) {
|
||||
toast.success(`Downloaded 3D model`);
|
||||
} else {
|
||||
toast.success(`Downloaded ${mediaClass}`);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("Failed to download file");
|
||||
}
|
||||
};
|
||||
|
||||
// const { toggleModal } = usePricingModalStore(); - Uncomment for pricing modal - BFlat
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="fixed left-0 top-0 z-[60] w-full border-b border-white/5 bg-ui-background">
|
||||
@@ -113,6 +138,13 @@ export const TopBar = ({ pageName }: Props) => {
|
||||
activeMenu={tabStore.activeTabId}
|
||||
disabled={disableTabSwitcher()}
|
||||
onMenuChange={(tabId) => {
|
||||
// Prevent a second input if the switcher is throttled.
|
||||
if (switcherThrottle.current) {
|
||||
return;
|
||||
}
|
||||
switcherThrottle.current = true;
|
||||
setDisableSwitcher(true);
|
||||
|
||||
// Disable 3d engine to prevent memory leak.
|
||||
if (tabId === "3D") {
|
||||
set3DPageMounted(true);
|
||||
@@ -120,6 +152,12 @@ export const TopBar = ({ pageName }: Props) => {
|
||||
set3DPageMounted(false);
|
||||
}
|
||||
useTabStore.getState().setActiveTab(tabId);
|
||||
setTimeout(() => {
|
||||
// Clear the throttle
|
||||
switcherThrottle.current = false;
|
||||
// Trigger a new re-render (important)
|
||||
setDisableSwitcher(false);
|
||||
}, SWITCHER_THROTTLE_TIME);
|
||||
}}
|
||||
className="w-fit"
|
||||
/>
|
||||
@@ -139,8 +177,17 @@ export const TopBar = ({ pageName }: Props) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3.5">
|
||||
<div className="flex justify-end gap-3.5 pr-2">
|
||||
<div className="flex gap-2">
|
||||
{/* - Uncomment for pricing modal - BFlat */}
|
||||
{/* <Button
|
||||
variant="primary"
|
||||
icon={faGem}
|
||||
onClick={toggleModal}
|
||||
className="shadow-md shadow-primary-500/50 transition-all duration-300 hover:shadow-md hover:shadow-primary-500/75"
|
||||
>
|
||||
Upgrade Now
|
||||
</Button> */}
|
||||
<Tooltip content="Settings" position="bottom" delay={300}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
@@ -159,9 +206,7 @@ export const TopBar = ({ pageName }: Props) => {
|
||||
|
||||
<Activity />
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<AuthButtons />
|
||||
</div>
|
||||
<AuthButtons loginSignUpPressed={loginSignUpPressed} />
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
@@ -172,7 +217,7 @@ export const TopBar = ({ pageName }: Props) => {
|
||||
globalAccountLogoutCallback={() => setLogoutStates()}
|
||||
/>
|
||||
|
||||
<GalleryModal mode="view" onDownloadClicked={downloadFileFromUrl} />
|
||||
<GalleryModal mode="view" onDownloadClicked={downloadFile} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import "./App.css";
|
||||
import PromptEditor from "./PromptEditor/PromptEditor";
|
||||
import SideToolbar from "./components/ui/SideToolbar";
|
||||
// Import the Zustand store
|
||||
import { useSceneStore } from "./stores/SceneState";
|
||||
import { AspectRatioType, useSceneStore } from "./stores/SceneState";
|
||||
import { useUndoRedoHotkeys } from "./hooks/useUndoRedoHotkeys";
|
||||
import { useDeleteHotkeys } from "./hooks/useDeleteHotkeys";
|
||||
import { useCopyPasteHotkeys } from "./hooks/useCopyPasteHotkeys"; // Import the hook
|
||||
@@ -196,6 +196,26 @@ const PageDraw = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const onFitPressed = async () => {
|
||||
// Get the stage and its container dimensions
|
||||
const stage = stageRef.current;
|
||||
|
||||
// Get container dimensions
|
||||
const containerWidth = stage.container().offsetWidth;
|
||||
const containerHeight = stage.container().offsetHeight;
|
||||
|
||||
// Get canvas dimensions from store aspect ratio
|
||||
const canvasW = store.getAspectRatioDimensions().width;
|
||||
const canvasH = store.getAspectRatioDimensions().height;
|
||||
|
||||
// Calculate position to center canvas in container
|
||||
stage.position({
|
||||
x: (containerWidth - canvasW) / 2,
|
||||
y: (containerHeight - canvasH) / 2
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@@ -219,7 +239,23 @@ const PageDraw = () => {
|
||||
}}
|
||||
onAspectRatioChange={(ratio: string) => {
|
||||
console.log("Aspect ratio:", ratio);
|
||||
// Handle aspect ratio changes here
|
||||
// Convert ratio string to AspectRatioType enum
|
||||
const ratioToType = (ratio: string): AspectRatioType => {
|
||||
switch (ratio) {
|
||||
case "2:3":
|
||||
return AspectRatioType.PORTRAIT;
|
||||
case "3:2":
|
||||
return AspectRatioType.LANDSCAPE;
|
||||
case "1:1":
|
||||
return AspectRatioType.SQUARE;
|
||||
default:
|
||||
return AspectRatioType.NONE;
|
||||
}
|
||||
};
|
||||
|
||||
const aspectRatioType = ratioToType(ratio);
|
||||
store.setAspectRatioType(aspectRatioType);
|
||||
onFitPressed()
|
||||
}}
|
||||
onEnqueuePressed={onEnqueuedPressed}
|
||||
/>
|
||||
@@ -413,6 +449,12 @@ const PageDraw = () => {
|
||||
showIconsInList
|
||||
triggerLabel="Model"
|
||||
/>
|
||||
<button
|
||||
className="bg-transparent p-2 text-lg text-white hover:text-white hover:bg-white/20 rounded transition"
|
||||
onClick={onFitPressed}
|
||||
>
|
||||
Fit
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -49,6 +49,9 @@ export const PaintSurface = ({
|
||||
stageRef,
|
||||
transformerRefs,
|
||||
}: MiraiProps) => {
|
||||
// switch off to be preview panel mode.
|
||||
const singlePaneMode = true
|
||||
|
||||
const store = useSceneStore(); // Use store directly
|
||||
const imageRef = React.useRef<HTMLImageElement>(null);
|
||||
const [snapshotImage, setSnapshotImage] = useState<HTMLImageElement | null>(
|
||||
@@ -66,7 +69,7 @@ export const PaintSurface = ({
|
||||
const [rightPanelHeight, setRightPanelHeight] = useState(1024);
|
||||
|
||||
/* 1️⃣ Track SplitPane percent so we can re-measure */
|
||||
const [leftPct, setLeftPct] = useState(50);
|
||||
const [leftPct, setLeftPct] = useState(singlePaneMode ? 100:50);
|
||||
const [isDrawing, setIsDrawing] = useState(false);
|
||||
const [lastPoint, setLastPoint] = useState<{ x: number; y: number } | null>(
|
||||
null,
|
||||
@@ -131,17 +134,17 @@ export const PaintSurface = ({
|
||||
y: number;
|
||||
}): { x: number; y: number } => {
|
||||
return {
|
||||
x: Math.max(0, Math.min(point.x, leftPanelWidth)),
|
||||
y: Math.max(0, Math.min(point.y, leftPanelHeight)),
|
||||
x: Math.max(0, Math.min(point.x, store.getAspectRatioDimensions().width)), //leftPanelWidth)),
|
||||
y: Math.max(0, Math.min(point.y, store.getAspectRatioDimensions().height))//leftPanelHeight)),
|
||||
};
|
||||
};
|
||||
|
||||
const isWithinLeftPanel = (point: { x: number; y: number }): boolean => {
|
||||
return (
|
||||
point.x >= 0 &&
|
||||
point.x <= leftPanelWidth &&
|
||||
point.x <= store.getAspectRatioDimensions().width && //leftPanelWidth &&
|
||||
point.y >= 0 &&
|
||||
point.y <= leftPanelHeight
|
||||
point.y <= store.getAspectRatioDimensions().height // leftPanelHeight
|
||||
);
|
||||
};
|
||||
|
||||
@@ -194,7 +197,8 @@ export const PaintSurface = ({
|
||||
stroke: activeTool === "draw" ? brushColor : fillColor || "#ffffff",
|
||||
strokeWidth: brushSize / stage.scaleX(),
|
||||
draggable: true,
|
||||
opacity: opacity
|
||||
opacity: opacity,
|
||||
locked: false,
|
||||
};
|
||||
store.selectNode(null);
|
||||
store.addLineNode(newLineNode, false); // Don't save state when starting line
|
||||
@@ -659,9 +663,10 @@ export const PaintSurface = ({
|
||||
"bottom-right",
|
||||
]}
|
||||
rotateEnabled={true}
|
||||
keepRatio={false}
|
||||
keepRatio={true}
|
||||
centeredScaling={true}
|
||||
padding={5}
|
||||
shiftBehavior="inverted"
|
||||
ignoreStroke={true}
|
||||
onTransformEnd={(e: Konva.KonvaEventObject<Event>) => {
|
||||
// Typed event
|
||||
@@ -975,10 +980,11 @@ export const PaintSurface = ({
|
||||
}
|
||||
}, [selectedNodeIds]);
|
||||
|
||||
|
||||
return (
|
||||
<SplitPane
|
||||
singlePaneMode={true}
|
||||
initialPercent={50}
|
||||
singlePaneMode={singlePaneMode}
|
||||
initialPercent={singlePaneMode?100 : 50}
|
||||
onChange={setLeftPct}
|
||||
left={
|
||||
<div className="flex h-full w-full items-center justify-center overflow-hidden">
|
||||
@@ -989,7 +995,7 @@ export const PaintSurface = ({
|
||||
scaleX={1} // Initial scale, controlled by wheel/zoom
|
||||
scaleY={1} // Initial scale, controlled by wheel/zoom
|
||||
style={{
|
||||
display: "block",
|
||||
// display: "block",
|
||||
background: "transparent", // Or use fillColor if stage background is desired directly
|
||||
}}
|
||||
x={stagePosition.x} // Set the x position
|
||||
@@ -1006,14 +1012,14 @@ export const PaintSurface = ({
|
||||
<Layer
|
||||
ref={leftPanelRef}
|
||||
clipFunc={(ctx) => {
|
||||
ctx.rect(0, 0, leftPanelWidth, leftPanelHeight);
|
||||
ctx.rect(0, 0, store.getAspectRatioDimensions().width, store.getAspectRatioDimensions().height); // leftPanelWidth, leftPanelHeight);
|
||||
}}
|
||||
>
|
||||
<Rect
|
||||
x={0}
|
||||
y={0}
|
||||
width={leftPanelWidth}
|
||||
height={leftPanelHeight}
|
||||
width={store.getAspectRatioDimensions().width}
|
||||
height={store.getAspectRatioDimensions().height}
|
||||
fill={fillColor}
|
||||
listening={false}
|
||||
zIndex={-1}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { getCanvasRenderBitmap } from "../../../signals/canvasRenderBitmap"
|
||||
const PromptEditor: React.FC<PromptEditorProps> = ({
|
||||
onImageStyleChange,
|
||||
onEnqueuePressed,
|
||||
onAspectRatioChange,
|
||||
}) => {
|
||||
const [images, setImages] = useState<ImageStyle[]>([]);
|
||||
|
||||
@@ -40,6 +41,7 @@ const PromptEditor: React.FC<PromptEditorProps> = ({
|
||||
EncodeImageBitmapToBase64={EncodeImageBitmapToBase64}
|
||||
useJobContext={useJobContext}
|
||||
onEnqueuePressed={onEnqueuePressed}
|
||||
onAspectRatioChange={onAspectRatioChange}
|
||||
/>
|
||||
</JobProvider>
|
||||
|
||||
|
||||
@@ -21,6 +21,13 @@ export type LineNode = {
|
||||
locked?: boolean; // Add locked property
|
||||
};
|
||||
|
||||
// Add this enum at the top of the file with other types
|
||||
export enum AspectRatioType {
|
||||
PORTRAIT = '2:3', // 683 x 1024
|
||||
LANDSCAPE = '3:2', // 1024 x 683
|
||||
SQUARE = '1:1', // 1024 x 1024
|
||||
NONE = 'none' // No aspect ratio constraint
|
||||
}
|
||||
|
||||
// Logic to remove background from image nodes would go here
|
||||
const convertFileToBase64 = (file: File): Promise<string> => {
|
||||
@@ -133,10 +140,17 @@ 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;
|
||||
|
||||
// Add minimal aspect ratio property
|
||||
aspectRatioType: AspectRatioType;
|
||||
|
||||
// Add aspect ratio action
|
||||
setAspectRatioType: (type: AspectRatioType) => void;
|
||||
getAspectRatioDimensions: () => { width: number; height: number };
|
||||
}
|
||||
|
||||
const generateId = (): string => {
|
||||
@@ -182,6 +196,9 @@ export const useSceneStore = create<SceneState>((set, get) => ({
|
||||
cursorPosition: null,
|
||||
cursorVisible: false,
|
||||
|
||||
// Add initial aspect ratio state
|
||||
aspectRatioType: AspectRatioType.NONE,
|
||||
|
||||
// Actions
|
||||
addNode: (node: Node) => {
|
||||
set((state) => {
|
||||
@@ -614,34 +631,21 @@ export const useSceneStore = create<SceneState>((set, get) => ({
|
||||
},
|
||||
|
||||
// Add method for URL-based images
|
||||
createImageFromUrl: (x: number, y: number, url: string, width?: number, height?: number) => {
|
||||
// Similar to createImageFromFile but handles URL
|
||||
const img = new Image();
|
||||
img.crossOrigin = "Anonymous"; // Handle CORS if necessary
|
||||
img.onload = () => {
|
||||
const aspectRatio = img.width / img.height;
|
||||
const newWidth = width || (height ? height * aspectRatio : 200); // Default width or calculate from height
|
||||
const newHeight = height || (width ? width / aspectRatio : newWidth / aspectRatio); // Default height or calculate from width
|
||||
createImageFromUrl: async (x: number, y: number, url: string, width?: number, height?: number) => {
|
||||
try {
|
||||
// Fetch the image and convert to blob
|
||||
const response = await fetch(url);
|
||||
const blob = await response.blob();
|
||||
|
||||
const node = new Node({
|
||||
id: generateId(),
|
||||
type: 'image',
|
||||
x,
|
||||
y,
|
||||
width: newWidth,
|
||||
height: newHeight,
|
||||
fill: 'transparent',
|
||||
imageElement: img, // Use the loaded image object
|
||||
draggable: true,
|
||||
imageUrl: url // Store the URL as the source
|
||||
});
|
||||
get().addNode(node);
|
||||
};
|
||||
img.onerror = () => {
|
||||
console.error("Error loading image from URL:", url);
|
||||
// Optionally handle the error, e.g., by creating a placeholder or notifying the user
|
||||
};
|
||||
img.src = url;
|
||||
// Create a File from the blob
|
||||
const filename = url.split('/').pop() || 'image.png';
|
||||
const file = new File([blob], filename, { type: blob.type });
|
||||
|
||||
// Use existing createImage function
|
||||
get().createImageFromFile(x, y, file, width, height);
|
||||
} catch (error) {
|
||||
console.error("Error loading image from URL:", url, error);
|
||||
}
|
||||
},
|
||||
|
||||
// Update the createImage method to handle both URLs and Files
|
||||
@@ -866,6 +870,7 @@ export const useSceneStore = create<SceneState>((set, get) => ({
|
||||
brushColor: state.brushColor,
|
||||
brushSize: state.brushSize,
|
||||
fillColor: state.fillColor,
|
||||
aspectRatioType: state.aspectRatioType,
|
||||
version: "1.0"
|
||||
};
|
||||
|
||||
@@ -924,7 +929,8 @@ export const useSceneStore = create<SceneState>((set, get) => ({
|
||||
selectedNodeIds: [],
|
||||
brushColor: sceneData.brushColor || '#000000',
|
||||
brushSize: sceneData.brushSize || 5,
|
||||
fillColor: sceneData.fillColor || 'white'
|
||||
fillColor: sceneData.fillColor || 'white',
|
||||
aspectRatioType: sceneData.aspectRatioType || AspectRatioType.NONE
|
||||
});
|
||||
|
||||
isRestoring = false;
|
||||
@@ -1162,58 +1168,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
|
||||
@@ -1247,5 +1241,25 @@ export const useSceneStore = create<SceneState>((set, get) => ({
|
||||
};
|
||||
});
|
||||
get().saveState();
|
||||
}
|
||||
},
|
||||
|
||||
// Add aspect ratio actions
|
||||
setAspectRatioType: (type: AspectRatioType) => {
|
||||
set({ aspectRatioType: type });
|
||||
get().saveState();
|
||||
},
|
||||
|
||||
getAspectRatioDimensions: () => {
|
||||
const { aspectRatioType } = get();
|
||||
switch (aspectRatioType) {
|
||||
case AspectRatioType.PORTRAIT:
|
||||
return { width: 683, height: 1024 };
|
||||
case AspectRatioType.LANDSCAPE:
|
||||
return { width: 1024, height: 683 };
|
||||
case AspectRatioType.SQUARE:
|
||||
return { width: 1024, height: 1024 };
|
||||
default:
|
||||
return { width: 1024, height: 683 }; // Default to landscape
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -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,
|
||||
@@ -72,12 +73,20 @@ import {
|
||||
// videoGenerationModels,
|
||||
// useModelSelectorStore,
|
||||
} from "@storyteller/ui-model-selector";
|
||||
import { LoginModal } from "@storyteller/ui-login-modal";
|
||||
import { LoginModal, useLoginModalStore } from "@storyteller/ui-login-modal";
|
||||
import PageDraw from "../PageDraw/PageDraw";
|
||||
import { useTabStore } from "../Stores/TabState";
|
||||
|
||||
export const PageEditor = () => {
|
||||
useSignals();
|
||||
const { triggerRecheck } = useLoginModalStore();
|
||||
|
||||
const { status } = authentication;
|
||||
useEffect(() => {
|
||||
if (status.value === AUTH_STATUS.LOGGED_OUT) {
|
||||
triggerRecheck();
|
||||
}
|
||||
}, [status.value, triggerRecheck]);
|
||||
|
||||
const tabStore = useTabStore();
|
||||
const handleOverlayClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
@@ -408,7 +417,28 @@ export const PageEditor = () => {
|
||||
|
||||
return (
|
||||
<div className="w-screen">
|
||||
<TopBar pageName="Edit Scene" />
|
||||
<TopBar
|
||||
loginSignUpPressed={() => {
|
||||
console.log("PRESSED");
|
||||
triggerRecheck();
|
||||
}}
|
||||
pageName="Edit Scene"
|
||||
/>
|
||||
<LoginModal
|
||||
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 +557,7 @@ export const PageEditor = () => {
|
||||
)}
|
||||
{tabStore.activeTabId == "IMAGE" && (
|
||||
<div>
|
||||
<ImageToVideo
|
||||
<TextToImage
|
||||
imageMediaId={topNavMediaId.value}
|
||||
imageUrl={topNavMediaUrl.value}
|
||||
/>
|
||||
@@ -535,30 +565,12 @@ 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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -27,6 +27,7 @@ import { useGenerationCompleteEvent } from "./TauriEvents/generations/useGenerat
|
||||
import { PageEditor } from "~/pages/PageEnigma/PageEditor";
|
||||
import { GalleryDragComponent } from "@storyteller/ui-gallery-modal";
|
||||
import { LoadingDots } from "@storyteller/ui-loading";
|
||||
import { PricingModal } from "@storyteller/ui-pricing-modal";
|
||||
|
||||
import {
|
||||
isActionReminderOpen,
|
||||
@@ -132,6 +133,8 @@ export const PageEnigma = ({ sceneToken }: { sceneToken?: string }) => {
|
||||
openAiLogo={currentReminderModalProps.openAiLogo}
|
||||
/>
|
||||
)}
|
||||
|
||||
<PricingModal />
|
||||
</EngineProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { PageEditor } from "~/pages/PageEnigma/PageEditor";
|
||||
|
||||
// TODO: Remove these file
|
||||
import { currentPage } from "~/signals";
|
||||
import { Pages } from "~/pages/PageEnigma/constants/page";
|
||||
|
||||
export const PageEnigmaComponent = () => {
|
||||
useSignals();
|
||||
return (
|
||||
<>
|
||||
<PageEditor />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -13,14 +13,19 @@ interface TextToImageProps {
|
||||
imageMediaId?: string;
|
||||
imageUrl?: string;
|
||||
}
|
||||
import { IMAGE_MODELS_BY_LABEL } from "@storyteller/model-list";
|
||||
import { ModelInfo } from "@storyteller/model-list";
|
||||
|
||||
const TextToImage = ({ imageMediaId, imageUrl }: TextToImageProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const { selectedModels } = useModelSelectorStore();
|
||||
|
||||
const selectedModel =
|
||||
selectedModels[ModelCategory.TextToImage] ||
|
||||
imageGenerationModels[0]?.label;
|
||||
|
||||
const selectedModelInfo : ModelInfo | undefined = IMAGE_MODELS_BY_LABEL[selectedModel];
|
||||
|
||||
const jobContext: JobContextType = {
|
||||
jobTokens: [],
|
||||
addJobToken: () => {},
|
||||
@@ -48,6 +53,7 @@ const TextToImage = ({ imageMediaId, imageUrl }: TextToImageProps) => {
|
||||
return jobContext;
|
||||
}}
|
||||
model={selectedModel}
|
||||
modelInfo={selectedModelInfo}
|
||||
imageMediaId={imageMediaId}
|
||||
url={imageUrl ?? undefined}
|
||||
onEnqueuePressed={async () => {
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
useModelSelectorStore,
|
||||
videoGenerationModels,
|
||||
} from "@storyteller/ui-model-selector";
|
||||
import { VIDEO_MODELS_BY_LABEL } from "@storyteller/model-list";
|
||||
import { ModelInfo } from "@storyteller/model-list";
|
||||
|
||||
interface ImageToVideoProps {
|
||||
imageMediaId?: string;
|
||||
@@ -17,10 +19,13 @@ interface ImageToVideoProps {
|
||||
const ImageToVideo = ({ imageMediaId, imageUrl }: ImageToVideoProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const { selectedModels } = useModelSelectorStore();
|
||||
|
||||
const selectedModel =
|
||||
selectedModels[ModelCategory.ImageToVideo] ||
|
||||
videoGenerationModels[0]?.label;
|
||||
|
||||
const selectedModelInfo : ModelInfo | undefined = VIDEO_MODELS_BY_LABEL[selectedModel];
|
||||
|
||||
const jobContext: JobContextType = {
|
||||
jobTokens: [],
|
||||
addJobToken: () => {},
|
||||
@@ -49,6 +54,7 @@ const ImageToVideo = ({ imageMediaId, imageUrl }: ImageToVideoProps) => {
|
||||
return jobContext;
|
||||
}}
|
||||
model={selectedModel}
|
||||
modelInfo={selectedModelInfo}
|
||||
imageMediaId={imageMediaId}
|
||||
url={imageUrl ?? undefined}
|
||||
onEnqueuePressed={async () => {
|
||||
|
||||
@@ -29,6 +29,12 @@ export default {
|
||||
white: colors.white,
|
||||
gray: colors.gray,
|
||||
black: colors.black,
|
||||
blue: colors.blue,
|
||||
green: colors.green,
|
||||
yellow: colors.yellow,
|
||||
purple: colors.purple,
|
||||
pink: colors.pink,
|
||||
orange: colors.orange,
|
||||
primary: {
|
||||
DEFAULT: "#2d81ff",
|
||||
"50": "#eef6ff",
|
||||
|
||||
@@ -65,10 +65,10 @@
|
||||
"path": "../../libs/components/button-dropdown"
|
||||
},
|
||||
{
|
||||
"path": "../../libs/tauri-api"
|
||||
"path": "../../libs/components/action-reminder-modal"
|
||||
},
|
||||
{
|
||||
"path": "../../libs/components/action-reminder-modal"
|
||||
"path": "../../libs/components/pricing-modal"
|
||||
},
|
||||
{
|
||||
"path": "../../libs/components/toaster"
|
||||
@@ -82,6 +82,9 @@
|
||||
{
|
||||
"path": "../../libs/components/promptbox"
|
||||
},
|
||||
{
|
||||
"path": "../../libs/tauri-api"
|
||||
},
|
||||
{
|
||||
"path": "../../libs/components/popover"
|
||||
},
|
||||
@@ -106,6 +109,9 @@
|
||||
{
|
||||
"path": "../../libs/components/gravatar"
|
||||
},
|
||||
{
|
||||
"path": "../../libs/components/login-modal"
|
||||
},
|
||||
{
|
||||
"path": "../../libs/components/select"
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
@@ -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,3 +1,2 @@
|
||||
|
||||
export * from './lib/BuildEnv';
|
||||
export * from './lib/BuildEnvType';
|
||||
export * from './lib/build-env';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/// <reference types='vitest' />
|
||||
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import dts from 'vite-plugin-dts';
|
||||
@@ -7,7 +7,7 @@ import * as path from 'path';
|
||||
export default defineConfig(() => ({
|
||||
root: __dirname,
|
||||
cacheDir: '../../node_modules/.vite/libs/build-env',
|
||||
plugins: [react(), dts({ entryRoot: 'src', tsconfigPath: path.join(__dirname, 'tsconfig.lib.json') })],
|
||||
plugins: [react(), dts({ entryRoot: 'src', tsconfigPath: path.join(__dirname, 'tsconfig.lib.json') }), ],
|
||||
// Uncomment this if you are using workers.
|
||||
// worker: {
|
||||
// plugins: [ nxViteTsPaths() ],
|
||||
@@ -15,35 +15,33 @@ export default defineConfig(() => ({
|
||||
// Configuration for building your library.
|
||||
// See: https://vitejs.dev/guide/build.html#library-mode
|
||||
build: {
|
||||
outDir: './dist',
|
||||
emptyOutDir: true,
|
||||
reportCompressedSize: true,
|
||||
commonjsOptions: {
|
||||
transformMixedEsModules: true,
|
||||
},
|
||||
lib: {
|
||||
// Could also be a dictionary or array of multiple entry points.
|
||||
entry: 'src/index.ts',
|
||||
name: '@frontend/build-env',
|
||||
fileName: 'index',
|
||||
// Change this to the formats you want to support.
|
||||
// Don't forget to update your package.json as well.
|
||||
formats: ['es' as const]
|
||||
},
|
||||
rollupOptions: {
|
||||
// External packages that should not be bundled into your library.
|
||||
external: ['react','react-dom','react/jsx-runtime']
|
||||
'emptyOutDir': true,
|
||||
'transformMixedEsModules': true,
|
||||
'entry': 'src/index.ts',
|
||||
'name': '@frontend/build-env',
|
||||
'fileName': 'index',
|
||||
'formats': ['es' as const],
|
||||
'external': ['react','react-dom','react/jsx-runtime'],
|
||||
'lib': {
|
||||
'entry': "src/index.ts",
|
||||
'name': "@frontend/build-env",
|
||||
'fileName': "index",
|
||||
'formats': ['es' as const],
|
||||
},
|
||||
'rollupOptions': {"external":["'react'","'react-dom'","'react/jsx-runtime'"]},
|
||||
'outDir': "./dist",
|
||||
'reportCompressedSize': true,
|
||||
'commonjsOptions': {"transformMixedEsModules":true},
|
||||
},
|
||||
test: {
|
||||
watch: false,
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||
reporters: ['default'],
|
||||
coverage: {
|
||||
reportsDirectory: './test-output/vitest/coverage',
|
||||
provider: 'v8' as const,
|
||||
}
|
||||
'watch': false,
|
||||
'globals': true,
|
||||
'environment': "jsdom",
|
||||
'include': ["src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
|
||||
'reporters': ["default"],
|
||||
'coverage': {
|
||||
'reportsDirectory': './test-output/vitest/coverage',
|
||||
'provider': 'v8' as const,
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -2,6 +2,15 @@
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../tauri-api"
|
||||
},
|
||||
{
|
||||
"path": "../../api"
|
||||
},
|
||||
{
|
||||
"path": "../image-input"
|
||||
},
|
||||
{
|
||||
"path": "../gallery-modal"
|
||||
},
|
||||
|
||||
@@ -43,6 +43,15 @@
|
||||
"src/**/*.tsx"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../tauri-api/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../../api/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../image-input/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../gallery-modal/tsconfig.lib.json"
|
||||
},
|
||||
|
||||
@@ -62,7 +62,7 @@ interface GalleryModalProps {
|
||||
onSelectItem?: (id: string) => void;
|
||||
maxSelections?: number;
|
||||
onUseSelected?: (selectedItems: GalleryItem[]) => void;
|
||||
onDownloadClicked?: (url: string) => Promise<void>;
|
||||
onDownloadClicked?: (url: string, mediaClass?: string) => Promise<void>;
|
||||
onAddToSceneClicked?: (
|
||||
url: string,
|
||||
media_id: string | undefined
|
||||
@@ -376,7 +376,7 @@ export const GalleryModal = React.memo(
|
||||
)}
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="border-b border-white/10 p-4 py-3">
|
||||
<div className="grid grid-cols-2 items-center">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-4">
|
||||
<h2 className="text-xl font-semibold">
|
||||
{mode === "select" ? "Select Images" : "My Library"}
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "../button"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
|
||||
@@ -41,5 +41,10 @@
|
||||
"src/**/*.jsx",
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../button/tsconfig.lib.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ interface LightboxModalProps {
|
||||
additionalInfo?: React.ReactNode;
|
||||
downloadUrl?: string;
|
||||
mediaId?: string;
|
||||
onDownloadClicked?: (url: string) => Promise<void>;
|
||||
onDownloadClicked?: (url: string, mediaClass?: string) => Promise<void>;
|
||||
onAddToSceneClicked?: (
|
||||
url: string,
|
||||
media_id: string | undefined
|
||||
@@ -164,7 +164,7 @@ export function LightboxModal({
|
||||
icon={faDownToLine}
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
await onDownloadClicked(downloadUrl);
|
||||
await onDownloadClicked(downloadUrl, mediaClass);
|
||||
}}
|
||||
>
|
||||
Download
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "../loading-spinner"
|
||||
},
|
||||
{
|
||||
"path": "../../tauri-api"
|
||||
},
|
||||
@@ -9,7 +12,7 @@
|
||||
"path": "../button"
|
||||
},
|
||||
{
|
||||
"path": "../close-button"
|
||||
"path": "../modal"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
|
||||
@@ -43,6 +43,9 @@
|
||||
"src/**/*.tsx"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../loading-spinner/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../../tauri-api/tsconfig.lib.json"
|
||||
},
|
||||
@@ -50,7 +53,7 @@
|
||||
"path": "../button/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../close-button/tsconfig.lib.json"
|
||||
"path": "../modal/tsconfig.lib.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
|
||||
export * from './lib/login-modal';
|
||||
export * from "./lib/login-modal";
|
||||
export * from "./lib/useLoginModalStore";
|
||||
|
||||
@@ -1,26 +1,20 @@
|
||||
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 { faArrowRight } 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";
|
||||
import { useLoginModalStore } from "./useLoginModalStore";
|
||||
interface LoginModalProps {
|
||||
onClose: () => void;
|
||||
onClose?: () => void;
|
||||
videoSrc2D?: string;
|
||||
videoSrc3D?: string;
|
||||
openAiLogo?: string;
|
||||
onOpenChange?: (isOpen: boolean) => void;
|
||||
onArtCraftAuthSuccess?: (userInfo: any) => void;
|
||||
isSignUp?: boolean;
|
||||
@@ -30,14 +24,13 @@ export function LoginModal({
|
||||
onClose,
|
||||
videoSrc2D,
|
||||
videoSrc3D,
|
||||
openAiLogo,
|
||||
onOpenChange,
|
||||
onArtCraftAuthSuccess,
|
||||
isSignUp: initialIsSignUp = true,
|
||||
}: LoginModalProps) {
|
||||
const { isOpen, recheckTrigger, closeModal } = useLoginModalStore();
|
||||
const [step, setStep] = useState(1);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isLoggedInArtCraft, setIsLoggedInArtCraft] = useState(false);
|
||||
const [isSignUp, setIsSignUp] = useState(initialIsSignUp);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
@@ -67,26 +60,35 @@ export function LoginModal({
|
||||
setStep(4); // Always go to the final step after Sora login
|
||||
});
|
||||
|
||||
// Check session on component mount
|
||||
// Check session on component mount and when recheckTrigger changes
|
||||
useEffect(() => {
|
||||
checkArtCraftLogin().then((loggedIn) => {
|
||||
if (loggedIn) {
|
||||
setIsLoggedInArtCraft(true);
|
||||
setIsOpen(false);
|
||||
closeModal();
|
||||
} else {
|
||||
setIsOpen(true);
|
||||
setStep(1); // Always start at step 1
|
||||
// Reset all modal state to initial values
|
||||
setStep(1);
|
||||
setIsLoading(false);
|
||||
setIsSignUp(initialIsSignUp);
|
||||
setErrorMessage("");
|
||||
setShowDiscord(false);
|
||||
setShowSuccess(false);
|
||||
setIsLoggedInArtCraft(false);
|
||||
|
||||
const { openModal } = useLoginModalStore.getState();
|
||||
openModal();
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
}, [recheckTrigger, closeModal, initialIsSignUp]);
|
||||
|
||||
useEffect(() => {
|
||||
if (onOpenChange) onOpenChange(isOpen);
|
||||
}, [isOpen, onOpenChange]);
|
||||
|
||||
const handleClose = () => {
|
||||
setIsOpen(false);
|
||||
onClose();
|
||||
closeModal();
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
const handleNext = async () => {
|
||||
@@ -155,7 +157,8 @@ export function LoginModal({
|
||||
Join Our Community
|
||||
</h2>
|
||||
<p className="text-white/70 mb-6 text-center px-24">
|
||||
Connect with other creators, share your work, and get the latest updates in our Discord community.
|
||||
Connect with other creators, share your work, and get the latest
|
||||
updates in our Discord community.
|
||||
</p>
|
||||
<div className="flex gap-4">
|
||||
<Button
|
||||
@@ -240,15 +243,15 @@ export function LoginModal({
|
||||
try {
|
||||
let signupResponse, loginResponse;
|
||||
if (isSignUp) {
|
||||
console.log("Sign up!")
|
||||
console.log("Sign up!");
|
||||
signupResponse = await usersApi.Signup({
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
passwordConfirmation,
|
||||
});
|
||||
console.log(signupResponse)
|
||||
|
||||
console.log(signupResponse);
|
||||
|
||||
if (!signupResponse.success) {
|
||||
setErrorMessage(
|
||||
signupResponse.errorMessage ||
|
||||
@@ -257,17 +260,21 @@ 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) {
|
||||
setErrorMessage(
|
||||
loginResponse.errorMessage ||
|
||||
@@ -278,12 +285,12 @@ export function LoginModal({
|
||||
}
|
||||
|
||||
setIsLoggedInArtCraft(true);
|
||||
console.log("ARTCRAFT LOGIN SUCCeSS")
|
||||
console.log("ARTCRAFT LOGIN SUCCeSS");
|
||||
if (onArtCraftAuthSuccess) {
|
||||
const session = await usersApi.GetSession();
|
||||
const userInfo = session.data?.user;
|
||||
console.log("USERINFO")
|
||||
console.log(userInfo)
|
||||
console.log("USERINFO");
|
||||
console.log(userInfo);
|
||||
if (userInfo) onArtCraftAuthSuccess(userInfo);
|
||||
}
|
||||
setShowDiscord(true); // Show Discord step after successful login
|
||||
@@ -315,11 +322,7 @@ export function LoginModal({
|
||||
return (
|
||||
<div className="flex items-end justify-center gap-2.5 mt-10 grow">
|
||||
{step === 2 && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleBack}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Button variant="secondary" onClick={handleBack} disabled={isLoading}>
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
@@ -336,7 +339,7 @@ export function LoginModal({
|
||||
);
|
||||
};
|
||||
|
||||
return createPortal(
|
||||
return (
|
||||
<Transition appear show={isOpen}>
|
||||
<div className="fixed inset-0 z-[100]">
|
||||
<TransitionChild
|
||||
@@ -378,8 +381,7 @@ export function LoginModal({
|
||||
</TransitionChild>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>,
|
||||
document.body
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { create } from "zustand";
|
||||
|
||||
interface LoginModalStore {
|
||||
isOpen: boolean;
|
||||
recheckTrigger: number;
|
||||
openModal: () => void;
|
||||
closeModal: () => void;
|
||||
triggerRecheck: () => void;
|
||||
}
|
||||
|
||||
export const useLoginModalStore = create<LoginModalStore>((set) => ({
|
||||
isOpen: false,
|
||||
recheckTrigger: 0,
|
||||
openModal: () => set({ isOpen: true }),
|
||||
closeModal: () => set({ isOpen: false }),
|
||||
triggerRecheck: () =>
|
||||
set((state) => ({ recheckTrigger: state.recheckTrigger + 1 })),
|
||||
}));
|
||||
@@ -25,6 +25,7 @@ import { DomLevels } from "@storyteller/common";
|
||||
declare global {
|
||||
interface Window {
|
||||
__inertStripperInstalled?: boolean;
|
||||
__modalEscListenerInstalled?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +59,56 @@ if (!window.__inertStripperInstalled) {
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Modal ESC handling & stack tracking
|
||||
// If several modals are stacked, Esc will dismiss the one visually on top (highest z-index).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface ModalRegistryEntry {
|
||||
id: number;
|
||||
zIndex: number;
|
||||
onClose: () => void;
|
||||
closeOnEsc: boolean;
|
||||
}
|
||||
|
||||
const modalRegistry: ModalRegistryEntry[] = [];
|
||||
|
||||
const registerModal = (entry: ModalRegistryEntry) => {
|
||||
modalRegistry.push(entry);
|
||||
};
|
||||
|
||||
const unregisterModal = (id: number) => {
|
||||
const idx = modalRegistry.findIndex((m) => m.id === id);
|
||||
if (idx !== -1) modalRegistry.splice(idx, 1);
|
||||
};
|
||||
|
||||
const updateModal = (
|
||||
id: number,
|
||||
data: Partial<{ zIndex: number; onClose: () => void; closeOnEsc: boolean }>
|
||||
) => {
|
||||
const entry = modalRegistry.find((m) => m.id === id);
|
||||
if (entry) Object.assign(entry, data);
|
||||
};
|
||||
|
||||
const getTopEscapableModal = (): ModalRegistryEntry | undefined => {
|
||||
return modalRegistry
|
||||
.filter((m) => m.closeOnEsc)
|
||||
.sort((a, b) => b.zIndex - a.zIndex)[0];
|
||||
};
|
||||
|
||||
// Install one global ESC listener (once per page load)
|
||||
if (typeof window !== "undefined" && !window.__modalEscListenerInstalled) {
|
||||
window.__modalEscListenerInstalled = true;
|
||||
window.addEventListener("keydown", (e: KeyboardEvent) => {
|
||||
if (e.key !== "Escape" && e.key !== "Esc") return;
|
||||
const top = getTopEscapableModal();
|
||||
if (top) {
|
||||
e.preventDefault();
|
||||
top.onClose();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Simple global z-index tracker for stacked modals
|
||||
let modalZCounter = 70;
|
||||
|
||||
@@ -176,6 +227,7 @@ export const Modal = ({
|
||||
resizable = false,
|
||||
initialPosition,
|
||||
closeOnOutsideClick = true,
|
||||
closeOnEsc = true,
|
||||
allowBackgroundInteraction = false,
|
||||
expandable = false,
|
||||
}: {
|
||||
@@ -203,6 +255,10 @@ export const Modal = ({
|
||||
* If false, clicking the backdrop will NOT close the modal
|
||||
*/
|
||||
closeOnOutsideClick?: boolean;
|
||||
/**
|
||||
* If true, pressing the Escape key will close the modal
|
||||
*/
|
||||
closeOnEsc?: boolean;
|
||||
/**
|
||||
* If true, allow interacting with background (removes pointer events from backdrop)
|
||||
*/
|
||||
@@ -747,6 +803,31 @@ export const Modal = ({
|
||||
};
|
||||
}, [allowBackgroundInteraction]);
|
||||
|
||||
// Register this modal in the global registry for ESC handling
|
||||
const idRef = useRef<number>(Math.floor(Math.random() * 1e9));
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
registerModal({
|
||||
id: idRef.current,
|
||||
zIndex,
|
||||
onClose,
|
||||
closeOnEsc,
|
||||
});
|
||||
|
||||
return () => {
|
||||
unregisterModal(idRef.current);
|
||||
};
|
||||
// We intentionally want to run this only on mount/unmount when isOpen true
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isOpen]);
|
||||
|
||||
// Keep registry entry updated when props change
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
updateModal(idRef.current, { zIndex, onClose, closeOnEsc });
|
||||
}, [isOpen, zIndex, onClose, closeOnEsc]);
|
||||
|
||||
return (
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog
|
||||
|
||||
@@ -1,49 +1,93 @@
|
||||
import type { PopoverItem } from "@storyteller/ui-popover";
|
||||
import { faClock, faFilm, faImage } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { IMAGE_MODELS } from "@storyteller/model-list";
|
||||
|
||||
export type ModelList = Omit<PopoverItem, "selected">[];
|
||||
|
||||
export const allModels = {
|
||||
video: [
|
||||
{
|
||||
label: "Kling 2.0",
|
||||
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} /> }],
|
||||
},
|
||||
{
|
||||
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",
|
||||
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} /> }],
|
||||
},
|
||||
{
|
||||
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} /> }],
|
||||
},
|
||||
{
|
||||
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} /> }],
|
||||
},
|
||||
],
|
||||
image: [
|
||||
{
|
||||
label: "Flux Pro 1.1 Ultra",
|
||||
icon: <FontAwesomeIcon icon={faImage} className="h-4 w-4" />,
|
||||
description: "High quality model",
|
||||
badges: [{ label: "15 sec.", icon: <FontAwesomeIcon icon={faClock} /> }],
|
||||
modelInfo: IMAGE_MODELS.flux_pro_1_1_ultra,
|
||||
},
|
||||
{
|
||||
label: "Flux Pro 1.1",
|
||||
icon: <FontAwesomeIcon icon={faImage} className="h-4 w-4" />,
|
||||
description: "High quality model",
|
||||
badges: [{ label: "15 sec.", icon: <FontAwesomeIcon icon={faClock} /> }],
|
||||
modelInfo: IMAGE_MODELS.flux_pro_1_1,
|
||||
},
|
||||
{
|
||||
label: "Flux 1 Dev",
|
||||
icon: <FontAwesomeIcon icon={faImage} className="h-4 w-4" />,
|
||||
description: "High quality model",
|
||||
badges: [{ label: "15 sec.", icon: <FontAwesomeIcon icon={faClock} /> }],
|
||||
modelInfo: IMAGE_MODELS.flux_1_dev,
|
||||
},
|
||||
{
|
||||
label: "Flux 1 Schnell",
|
||||
icon: <FontAwesomeIcon icon={faImage} className="h-4 w-4" />,
|
||||
description: "High quality model",
|
||||
badges: [{ label: "15 sec.", icon: <FontAwesomeIcon icon={faClock} /> }],
|
||||
modelInfo: IMAGE_MODELS.flux_1_schnell,
|
||||
},
|
||||
{
|
||||
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} /> }],
|
||||
modelInfo: IMAGE_MODELS.gpt_image_1,
|
||||
},
|
||||
{
|
||||
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} /> }],
|
||||
},
|
||||
{
|
||||
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} /> }],
|
||||
},
|
||||
{
|
||||
label: "Flux.1 Kontext",
|
||||
icon: <FontAwesomeIcon icon={faImage} className="h-4 w-4" />,
|
||||
description: "New model with advanced context",
|
||||
badges: [{ label: "25 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",
|
||||
// badges: [{ label: "25 sec.", icon: <FontAwesomeIcon icon={faClock} /> }],
|
||||
//},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user