Compare commits

...
Author SHA1 Message Date
Brandon Thomas 2f767ef655 update download links 2025-07-17 21:53:54 -04:00
Brandon Thomasandbflatastic da859fc2f8 marketing updates for the website (#722)
This is currently a draft / work in progress.

---------

Co-authored-by: bflatastic <bombayapirat@gmail.com>
2025-07-17 10:49:14 -04:00
Brandon Thomas 5f832d5d17 fix thumbnails in gallery and jobs, fix veo2 args, add hunyuan args, etc. (#731) 2025-07-17 09:51:59 -04:00
Brandon Thomas 612e58483c add model type to tasks db (#730) 2025-07-16 21:49:46 -04:00
Brandon Thomas 11b07b5833 debugging, resizing improvements; cleanup + refactor (#729) 2025-07-16 19:19:30 -04:00
Kasisnu a3f3bd8b3f bump nx version 2025-07-16 20:48:44 +05:30
bflatastic a93e921b51 3d model upload fix, upload button, modal block propagation of keyboard events 2025-07-16 13:57:09 +07:00
Brandon Thomas 58d5a172c4 consolidate cloudflare handling code (#728) 2025-07-16 01:09:50 -04:00
Brandon Thomas 012b7a9567 artcraft task polling (#727) 2025-07-15 22:28:03 -04:00
BFlat 18f4411f2a 2D reset canvas button and persist prompt boxes (#726)
- 2D reset canvas button under the left side toolbar. on click prompts a
confirmation to clear the canvas.
- all prompt boxes persist prompts, aspect ratio, system prompt and
reference images for that session until refreshed.
2025-07-15 18:46:58 -04:00
Brandon Thomas 0ed4d58387 refactor job api types for later reuse in http client (#725) 2025-07-15 01:09:10 -04:00
Brandon Thomas 9782bd33bc enable text prompts for videos (#724) 2025-07-14 22:14:36 -04:00
Brandon Thomas 2c9be7c262 fix aspect ratio controls on image gen (first pass) (#723)
This doesn't support all aspect ratios for all models. We'll need to
come back and offer per-model options.
2025-07-14 21:51:32 -04:00
Brandon Thomas a1f38b3695 upgrade nx (seems to be blocking netflify) (#721)
- tested artcraft locally
- tested website build
2025-07-14 16:27:08 -04:00
Brandon Thomas d5566c3466 remove unnecessary javascript package files (#720)
plus ignore some secrets that keep my git status dirty
2025-07-14 16:01:16 -04:00
Brandon Thomas 27d2ae47b3 remove "secrets" (not ours) that Netlify is complaining about (#719)
I don't think these are secrets. They're literally in publicly used and
consumed HTML content.

I'll need to remove the old HTML scraping libraries as they're no longer
relevant.
2025-07-14 15:42:24 -04:00
Brandon Thomas 926076441b update artcraft download links (#718) 2025-07-14 15:38:39 -04:00
Brandon Thomas 64353f1be9 video thumbnails from fal (#717) 2025-07-14 02:24:38 -04:00
bflatastic a8bb7f2720 windows dev script sqlx offline 2025-07-14 09:51:27 +07:00
Brandon Thomas ec1257ccf6 fix sqlx sqlite build for dev and CI (#716) 2025-07-13 20:26:57 -04:00
Brandon Thomas 6038fa4fd3 rename and retitle macos build action, add additional branch (#715) 2025-07-13 19:07:33 -04:00
Brandon ThomasandKasisnu 8ae6867e05 Kasisnu's artcraft release changes (#713)
Co-authored-by: Kasisnu <5044120+kasisnu@users.noreply.github.com>
2025-07-13 18:55:32 -04:00
Brandon Thomas 9bc24ac216 introduce sqlite with Tauri; task tracking database (#712) 2025-07-13 18:48:47 -04:00
bflatastic 590c2fbf96 multiselect and line node resizing/moving fix 2025-07-11 22:32:09 +07:00
bflatastic 212283e8d2 node sizing fix 2025-07-11 18:42:47 +07:00
BFlat 40d22b8ad0 2d bug fixes (#711)
**fixes:**
- artcraft logo link removed
- buggy layering system fixed
- no more circle resize jumping issue
- undo stack doesn't break images anymore
- undo, redo, copy, paste, delete keybinds work on mac now
- white bar at bottom removed
2025-07-11 15:58:33 +07:00
262 changed files with 6282 additions and 13034 deletions
@@ -0,0 +1,232 @@
name: "Artcraft MacOS Publish"
on:
push:
branches:
- release
- artcraft-release
- artcraft-macos # Additional branch for MacOS testing
# 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-xlarge'
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-xlarge' && '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: |
# Navigate to the frontend directory to install dependencies
cd ./frontend
npm cache clean --force
echo "Installing all workspace dependencies..."
npm ci
echo "=== Version Information ==="
echo "Node version:"
node --version
echo "NPM version:"
npm --version
# Remove mismatched @nx packages and reinstall with correct versions
npm install --save-dev @nx/remix@21.1.1 @nx/js@21.1.1 @nx/react@21.1.1 @nx/vite@21.1.1 @nx/eslint@21.1.1
echo "=== Nx versions after adjustment ==="
npm list --depth=0 | grep nx
- name: import Apple Developer Certificate
if: matrix.platform == 'macos-latest-xlarge'
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
echo "Starting certificate import process..."
# Create certificate file
echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12
# Debug: Check if certificate file exists and has content
echo "Certificate file info:"
ls -la certificate.p12
file certificate.p12
# Debug: Check certificate contents without password (just structure)
echo "Certificate structure (may show error if password protected):"
openssl pkcs12 -info -in certificate.p12 -passin pass:$APPLE_CERTIFICATE_PASSWORD -noout || echo "Could not read certificate structure"
# Debug: Try to list what's in the p12 file
echo "Attempting to list certificate contents:"
security cms -D -i certificate.p12 || echo "Not a CMS file, trying PKCS12..."
openssl pkcs12 -in certificate.p12 -passin pass:$APPLE_CERTIFICATE_PASSWORD -info -nodes 2>&1 | head -20 || echo "Failed to read PKCS12 contents"
# Debug: Extract certificate details to verify type and expiration
echo "Extracting certificate details..."
# Extract just the certificate portion and examine it
openssl pkcs12 -in certificate.p12 -passin pass:$APPLE_CERTIFICATE_PASSWORD -nokeys 2>/dev/null | openssl x509 -text -noout | grep -E "(Subject:|Issuer:|Not Before|Not After)" || echo "Failed to parse certificate details"
# Also try to see what type of certificate this is
echo "Certificate type check:"
openssl pkcs12 -in certificate.p12 -passin pass:$APPLE_CERTIFICATE_PASSWORD -nokeys 2>/dev/null | openssl x509 -text -noout | grep -E "(CN=|OU=)" || echo "Could not determine certificate type"
# Debug: Check if private key exists
echo "Checking for private key..."
openssl pkcs12 -in certificate.p12 -passin pass:$APPLE_CERTIFICATE_PASSWORD -nocerts -nodes 2>&1 | grep -E "(PRIVATE KEY|RSA KEY)" | head -5 || echo "No private key found"
# Create temporary keychain
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
# Import certificate - ensure both certificate and private key are imported
echo "Importing certificate into keychain..."
# Import the p12 directly - this is the most reliable method
security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild
# Alternative: Try with additional flags if the first import didn't create an identity
echo "Verifying import..."
if ! security find-identity -v build.keychain | grep -q "1 valid identities found"; then
echo "First import didn't create identity, trying with -A flag..."
security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -A
fi
# Set trust settings for the certificate
echo "Setting trust settings..."
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
# Unlock the keychain again to ensure it's accessible
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
# Add the keychain to the search list
security list-keychains -d user -s build.keychain $(security list-keychains -d user | sed 's/^[[:space:]]*"//;s/"$//')
# List all certificates to debug
echo "Listing all keychains:"
security list-keychains
echo "Listing all certificates in build keychain:"
security find-certificate -a -p build.keychain || echo "No certificates found"
echo "Detailed certificate info:"
security find-certificate -c "Learning Machines" build.keychain || echo "Certificate 'Learning Machines' not found"
# Check certificate trust settings
echo "Checking certificate trust settings:"
security dump-trust-settings || echo "No custom trust settings"
# Try to find codesigning identities
echo "Finding all identities in keychain:"
security find-identity -v build.keychain || echo "No identities found"
echo "Finding codesigning identities:"
security find-identity -v -p codesigning build.keychain || echo "No codesigning identities found"
# Additional debug: Check if this is a valid Developer ID cert
echo "Checking for Developer ID certificates:"
security find-certificate -a build.keychain | grep -A 5 -B 5 "Developer ID" || echo "No Developer ID certificates found"
- name: verify certificate
if: matrix.platform == 'macos-latest-xlarge'
run: |
echo "=== Certificate Verification ==="
echo "1. Checking codesigning identities:"
security find-identity -v -p codesigning build.keychain || echo "No codesigning identities found"
echo -e "\n2. Checking all identities:"
security find-identity -v build.keychain || echo "No identities found"
echo -e "\n3. Checking certificates in keychain:"
security find-certificate -c "Learning Machines" build.keychain -p | openssl x509 -text -noout | grep -E "(Subject:|CN=)" || echo "Could not parse certificate"
# Set the correct signing identity
echo "APPLE_SIGNING_IDENTITY=Developer ID Application: Learning Machines LLC (DJ6XS33FX8)" >> $GITHUB_ENV
echo "Proceeding with identity: Developer ID Application: Learning Machines LLC (DJ6XS33FX8)"
# Also set the team ID
echo "APPLE_DEVELOPMENT_TEAM=DJ6XS33FX8" >> $GITHUB_ENV
- name: Pre-build frontend to avoid Tauri build issues
run: |
# Build the frontend app from the correct directory
cd ./frontend
# Increase Node.js memory limit for the build
export NODE_OPTIONS="--max-old-space-size=8192"
# Use npx to ensure we use the local nx version
echo "Building with npx nx..."
npx nx run editor3d:build
echo "Checking build output..."
ls -la apps/editor3d/dist/ | head -10
# Update Tauri config to skip the build (since we already built)
cd ${{ github.workspace }}/crates/desktop/artcraft
cp tauri.conf.json tauri.conf.json.bak
# Set beforeBuildCommand to empty or a no-op command
if command -v jq &> /dev/null; then
jq '.build.beforeBuildCommand = ""' tauri.conf.json > tauri.conf.json.tmp
mv tauri.conf.json.tmp tauri.conf.json
else
node -e "const fs = require('fs'); const config = JSON.parse(fs.readFileSync('tauri.conf.json', 'utf8')); config.build.beforeBuildCommand = ''; fs.writeFileSync('tauri.conf.json', JSON.stringify(config, null, 2));"
fi
- 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_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ env.APPLE_SIGNING_IDENTITY }}
APPLE_DEVELOPMENT_TEAM: ${{ env.APPLE_DEVELOPMENT_TEAM }}
# Use the SQLx query cache for SQLite
SQLX_OFFLINE: true
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 }}
-107
View File
@@ -1,107 +0,0 @@
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 }}
@@ -39,24 +39,22 @@ jobs:
# Navigate to the frontend directory to install dependencies
cd ./frontend
yarn cache clean
npm cache clean --force
echo "Installing all workspace dependencies..."
yarn install --production=false --frozen-lockfile
npm ci
echo "=== Version Information ==="
echo "Node version:"
node --version
echo "NPM version:"
npm --version
echo "Yarn version:"
yarn --version
# Remove mismatched @nx packages and reinstall with correct versions
yarn add @nx/remix@20.7.2 @nx/js@20.7.2 @nx/react@20.7.2 @nx/vite@20.7.2 @nx/eslint@20.7.2 --dev -W
npm install --save-dev @nx/remix@21.1.1 @nx/js@21.1.1 @nx/react@21.1.1 @nx/vite@21.1.1 @nx/eslint@21.1.1
echo "=== Nx versions after adjustment ==="
yarn list --pattern "nx" --depth=0
npm list --depth=0 | findstr nx
- name: Pre-build frontend to avoid Tauri build issues
run: |
@@ -76,7 +74,7 @@ jobs:
# Update Tauri config to skip the build (since we already built)
cd ${{ github.workspace }}/crates/desktop/artcraft
Copy-Item tauri.conf.json tauri.conf.json.bak
# Set beforeBuildCommand to empty using PowerShell
$config = Get-Content tauri.conf.json | ConvertFrom-Json
$config.build.beforeBuildCommand = ""
@@ -86,6 +84,8 @@ jobs:
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Use the SQLx query cache for SQLite
SQLX_OFFLINE: true
with:
projectPath: ./crates/desktop/artcraft
tagName: artcraft-windows-v__VERSION__ # the action automatically replaces __VERSION__ with the app version.
+4 -2
View File
@@ -24,11 +24,13 @@ twitch_secrets.toml
*-secrets.env
# Utilities with secrets
script/production_database_secret_backup.sh
script/production_database_secret_connect.sh
production_database_secret_connect.sh
production_database_secret_query.sh
production_database_voice_clone_report_download.sh
script/artcraft/mac_signing_build.sh
script/production_database_secret_backup.sh
script/production_database_secret_connect.sh
script/socat_dev_database_connect.sh
# Scratch data
scratch/
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO tasks (\n id,\n task_status,\n task_type,\n model_type,\n provider,\n provider_job_id,\n frontend_subscriber_id,\n frontend_subscriber_payload\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 8
},
"nullable": []
},
"hash": "22b91a21cbb88abc9bc17b338cf2f9418e08e414667d44476623eb141ab17b87"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n UPDATE tasks\n SET task_status = ?\n WHERE id = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "dc0367e9e43c3d7dda3623e77337ab4529eac2192475be7d08589886703ba1cb"
}
Generated
+52 -54
View File
@@ -656,6 +656,7 @@ dependencies = [
"directories",
"dirs",
"discord-rich-presence",
"enums",
"errors",
"fal_client",
"filesys",
@@ -675,6 +676,7 @@ dependencies = [
"serde_derive",
"serde_json",
"serial_test",
"sqlite_tasks",
"storyteller_client",
"tauri",
"tauri-build",
@@ -696,11 +698,13 @@ dependencies = [
name = "artcraft_api_defs"
version = "0.0.1"
dependencies = [
"chrono",
"enums",
"serde",
"serde_derive",
"serde_json",
"tokens",
"url",
"utoipa",
"workspace-build-acceleration",
]
@@ -985,16 +989,6 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "atomic-write-file"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c232177ba50b16fe7a4588495bd474a62a9e45a8e4ca6fd7d0b7ac29d164631e"
dependencies = [
"nix 0.26.4",
"rand 0.8.5",
]
[[package]]
name = "attohttpc"
version = "0.28.0"
@@ -1924,6 +1918,13 @@ dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "cloudflare_errors"
version = "0.0.1"
dependencies = [
"workspace-build-acceleration",
]
[[package]]
name = "coarsetime"
version = "0.1.34"
@@ -2285,7 +2286,7 @@ dependencies = [
"autocfg",
"cfg-if 1.0.0",
"crossbeam-utils 0.8.20",
"memoffset 0.9.0",
"memoffset",
"scopeguard",
]
@@ -3667,7 +3668,7 @@ version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f"
dependencies = [
"memoffset 0.9.0",
"memoffset",
"rustc_version 0.4.0",
]
@@ -5988,15 +5989,6 @@ version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "memoffset"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4"
dependencies = [
"autocfg",
]
[[package]]
name = "memoffset"
version = "0.9.0"
@@ -6379,19 +6371,6 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
[[package]]
name = "nix"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b"
dependencies = [
"bitflags 1.3.2",
"cfg-if 1.0.0",
"libc",
"memoffset 0.7.1",
"pin-utils",
]
[[package]]
name = "nix"
version = "0.28.0"
@@ -6402,7 +6381,7 @@ dependencies = [
"cfg-if 1.0.0",
"cfg_aliases 0.1.1",
"libc",
"memoffset 0.9.0",
"memoffset",
]
[[package]]
@@ -6869,6 +6848,7 @@ dependencies = [
"anyhow",
"base64 0.22.1",
"chrono",
"cloudflare_errors",
"cookie 0.18.1",
"errors",
"idempotency",
@@ -9645,11 +9625,31 @@ dependencies = [
"unicode_categories",
]
[[package]]
name = "sqlite_tasks"
version = "0.0.1"
dependencies = [
"anyhow",
"chrono",
"enums",
"errors",
"log",
"once_cell",
"regex",
"serde",
"serde_derive",
"serde_json",
"sqlx",
"tempfile",
"tokens",
"workspace-build-acceleration",
]
[[package]]
name = "sqlx"
version = "0.7.3"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dba03c279da73694ef99763320dea58b51095dfe87d001b1d4b5fe78ba8763cf"
checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa"
dependencies = [
"sqlx-core",
"sqlx-macros",
@@ -9660,9 +9660,9 @@ dependencies = [
[[package]]
name = "sqlx-core"
version = "0.7.3"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d84b0a3c3739e220d94b3239fd69fb1f74bc36e16643423bd99de3b43c21bfbd"
checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6"
dependencies = [
"ahash 0.8.11",
"atoi",
@@ -9672,7 +9672,6 @@ dependencies = [
"chrono",
"crc",
"crossbeam-queue",
"dotenvy",
"either",
"event-listener 2.5.3",
"futures-channel",
@@ -9706,9 +9705,9 @@ dependencies = [
[[package]]
name = "sqlx-macros"
version = "0.7.3"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89961c00dc4d7dffb7aee214964b065072bff69e36ddb9e2c107541f75e4f2a5"
checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127"
dependencies = [
"proc-macro2",
"quote",
@@ -9719,11 +9718,10 @@ dependencies = [
[[package]]
name = "sqlx-macros-core"
version = "0.7.3"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0bd4519486723648186a08785143599760f7cc81c52334a55d6a83ea1e20841"
checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8"
dependencies = [
"atomic-write-file",
"dotenvy",
"either",
"heck 0.4.1",
@@ -9746,9 +9744,9 @@ dependencies = [
[[package]]
name = "sqlx-mysql"
version = "0.7.3"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e37195395df71fd068f6e2082247891bc11e3289624bbc776a0cdfa1ca7f1ea4"
checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418"
dependencies = [
"atoi",
"base64 0.21.7",
@@ -9790,9 +9788,9 @@ dependencies = [
[[package]]
name = "sqlx-postgres"
version = "0.7.3"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6ac0ac3b7ccd10cc96c7ab29791a7dd236bd94021f31eec7ba3d46a74aa1c24"
checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e"
dependencies = [
"atoi",
"base64 0.21.7",
@@ -9820,7 +9818,6 @@ dependencies = [
"rand 0.8.5",
"serde",
"serde_json",
"sha1 0.10.6",
"sha2 0.10.8",
"smallvec 1.13.2",
"sqlx-core",
@@ -9832,9 +9829,9 @@ dependencies = [
[[package]]
name = "sqlx-sqlite"
version = "0.7.3"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "210976b7d948c7ba9fced8ca835b11cbb2d677c59c79de41ac0d397e14547490"
checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa"
dependencies = [
"atoi",
"chrono",
@@ -10002,6 +9999,7 @@ dependencies = [
"artcraft_api_defs",
"backoff",
"chrono",
"cloudflare_errors",
"cookie 0.18.1",
"enums",
"enums_public",
@@ -11589,7 +11587,7 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9"
dependencies = [
"memoffset 0.9.0",
"memoffset",
"tempfile",
"winapi 0.3.9",
]
@@ -13174,7 +13172,7 @@ dependencies = [
"futures-sink",
"futures-util",
"hex",
"nix 0.28.0",
"nix",
"ordered-stream",
"rand 0.8.5",
"serde",
+4
View File
@@ -40,6 +40,7 @@ members = [
"crates/lib/caching/memory_caching",
"crates/lib/caching/memory_store",
"crates/lib/caching/redis_caching",
"crates/lib/cloudflare_errors",
"crates/lib/concurrency",
"crates/lib/cookies",
"crates/lib/data/collections",
@@ -90,6 +91,7 @@ members = [
"crates/schema/database/mysql_queries",
"crates/schema/database/redis_common",
"crates/schema/database/redis_schema",
"crates/schema/database/sqlite_tasks",
#"crates/schema/database/sqlite_queries",
# Schema (public): common strongly-typed tokens, ids, payloads, etc.
@@ -215,6 +217,7 @@ url = { version = "2.5.4", features = ["serde"] }
artcraft_api_defs = { path = "crates/schema/public/api/artcraft_api_defs" }
build_metadata = { path = "crates/lib/build_metadata" }
cloud_storage = { path = "crates/api_clients/cloud_storage" }
cloudflare_errors = { path = "crates/lib/cloudflare_errors" }
crockford = { path = "crates/lib/data/crockford" }
enums = { path = "crates/schema/public/enums" }
enums_public = { path = "crates/schema/public/enums_public" }
@@ -231,6 +234,7 @@ mimetypes = { path = "crates/lib/files/mimetypes" }
openai_sora_client = { path = "crates/api_clients/openai_sora_client" }
primitives = { path = "crates/lib/data/primitives" }
shared_service_components = { path = "crates/lib/shared_service_components" }
sqlite_tasks = { path = "crates/schema/database/sqlite_tasks" }
storyteller_client = { path = "crates/api_clients/storyteller_client" }
subprocess_common = { path = "crates/lib/subprocess_common" }
test_data = { path = "crates/lib/test_data" }
@@ -0,0 +1,30 @@
-- noinspection SqlDialectInspectionForFile
-- Migration history:
-- tasks_v1.sqlite - initial version
-- tasks_v2.sqlite - added model_type (nullable)
CREATE TABLE tasks (
id TEXT NOT NULL PRIMARY KEY,
task_status TEXT NOT NULL,
task_type TEXT NOT NULL,
model_type TEXT,
provider TEXT,
provider_job_id TEXT,
frontend_subscriber_id TEXT,
frontend_subscriber_payload TEXT,
created_at INTEGER NOT NULL DEFAULT (unixepoch('now')),
updated_at INTEGER NOT NULL DEFAULT (unixepoch('now')),
completed_at INTEGER DEFAULT NULL
);
-- Unique Indices
CREATE UNIQUE INDEX idx_tasks_on_provider_job_id ON tasks(provider_job_id);
-- Indices
CREATE INDEX idx_tasks_on_status ON tasks(task_status);
CREATE INDEX idx_tasks_on_provider ON tasks(provider);
@@ -37,10 +37,10 @@ pub async fn enqueue_veo_2_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = match args.duration {
Veo2Duration::Default => None,
Veo2Duration::FiveSeconds => Some("5".to_string()),
Veo2Duration::SixSeconds => Some("6".to_string()),
Veo2Duration::SevenSeconds => Some("7".to_string()),
Veo2Duration::EightSeconds => Some("8".to_string()),
Veo2Duration::FiveSeconds => Some("5s".to_string()),
Veo2Duration::SixSeconds => Some("6s".to_string()),
Veo2Duration::SevenSeconds => Some("7s".to_string()),
Veo2Duration::EightSeconds => Some("8s".to_string()),
};
let aspect_ratio = match args.aspect_ratio {
@@ -23,6 +23,7 @@ idempotency.workspace = true
anyhow.workspace = true
base64.workspace = true
chrono.workspace = true
cloudflare_errors.workspace = true
cookie.workspace = true
log.workspace = true
once_cell.workspace = true
@@ -1,3 +1,4 @@
use cloudflare_errors::cloudflare_error::CloudflareError;
use std::error::Error;
use std::fmt::{Display, Formatter};
use std::io;
@@ -42,15 +43,9 @@ pub enum SoraError {
/// Typically served from Cloudflare
/// We preserve the message for debugging.
BadGateway(String),
/// HTTP 403 + Cloudflare Challenge Interstitial
/// Cloudflare decided to block the request and present a challenge.
CloudFlareChallenge,
/// HTTP 524 (Cloudflare-specific)
/// Cloudflare formed a TCP connection to Sora, but no payload was delivered before timeout
/// We preserve the message for debugging
CloudFlareTimeout(String),
/// Specific Cloudflare errors.
CloudflareError(CloudflareError),
/// Another error occurred.
OtherBadStatus(anyhow::Error),
@@ -72,6 +67,17 @@ pub enum SoraError {
JsonErrorWithBody(serde_json::Error, String),
}
impl SoraError {
pub fn is_sora_having_downtime_issues(&self) -> bool {
match self {
Self::BadGateway(_) => true,
Self::CloudflareError(CloudflareError::GatewayTimeout504) => true,
Self::CloudflareError(CloudflareError::TimeoutOccurred524) => true,
_ => false,
}
}
}
impl Error for SoraError {}
impl Display for SoraError {
@@ -98,11 +104,8 @@ impl Display for SoraError {
Self::BadGateway(message) => {
write!(f, "Bad Gateway; Sora is likely having issues: {:?}", message)
}
Self::CloudFlareChallenge => {
write!(f, "Cloudflare Challenge (403); Cloudflare wants to verify the request with a CAPTCHA challenge.")
}
Self::CloudFlareTimeout(message) => {
write!(f, "Cloudflare Timeout (524); Sora is likely having issues: {:?}", message)
Self::CloudflareError(error) => {
write!(f, "Cloudflare error: {}", error)
}
Self::OtherBadStatus(err) => {
write!(f, "Other error: {}", err)
@@ -149,3 +152,9 @@ impl From<serde_json::Error> for SoraError {
Self::JsonError(err)
}
}
impl From<CloudflareError> for SoraError {
fn from(err: CloudflareError) -> Self {
Self::CloudflareError(err)
}
}
@@ -1,6 +1,6 @@
use crate::sora_error::SoraError;
use cloudflare_errors::filter_cloudflare_errors::filter_cloudflare_errors;
use reqwest::StatusCode;
/*
Old cookie -
@@ -50,32 +50,15 @@ pub async fn classify_general_http_status_code_and_body(status: StatusCode, resp
}
let status_code = status.as_u16();
if let Err(err) = filter_cloudflare_errors(status_code, &response_body) {
return SoraError::CloudflareError(err);
}
match status_code {
403 => {
if message.contains("challenge-platform")
|| message.contains("challenge-error-text")
|| message.contains("cType: 'managed'")
|| message.contains("Just a moment...") {
return SoraError::CloudFlareChallenge;
}
}
502 => {
return SoraError::BadGateway(message);
}
504 => {
if message.contains("cloudflare")
|| message.contains("Cloudflare") {
return SoraError::CloudFlareTimeout(message);
}
}
524 => {
// NB: 524 is a cloudflare specific gateway timeout error with slightly different semantics than 504.
if message.contains("cloudflare")
|| message.contains("Cloudflare") {
return SoraError::CloudFlareTimeout(message);
}
}
_ => {}, // Fall-through
}
@@ -24,12 +24,13 @@ workspace-build-acceleration.workspace = true
# Internal
artcraft_api_defs.workspace = true
cloudflare_errors.workspace = true
enums.workspace = true
enums_public.workspace = true
errors.workspace = true
idempotency.workspace = true
tokens.workspace = true
mimetypes.workspace = true
tokens.workspace = true
# External
anyhow.workspace = true
@@ -1,7 +1,8 @@
use cloudflare_errors::cloudflare_error::CloudflareError;
use errors::AnyhowError;
use std::error::Error;
use std::fmt::{Display, Formatter};
use std::io;
use errors::AnyhowError;
#[derive(Debug)]
pub enum ApiError {
@@ -26,6 +27,9 @@ pub enum ApiError {
backend_hostname: Option<String>,
},
/// Cloudflare errors.
CloudflareError(CloudflareError),
/// A deserialization error with the response.
DeserializationError(serde_json::Error),
@@ -63,6 +67,8 @@ impl Display for ApiError {
// Network errors
ApiError::Timeout(msg) => write!(f, "Timeout: {}", msg),
ApiError::NetworkError(msg) => write!(f, "Network error: {}", msg),
// Cloudflare errors
ApiError::CloudflareError(error) => write!(f, "Cloudflare Error: {}", error),
// I/O errors
ApiError::IoError(error) => write!(f, "IO error: {}", error),
// Other
@@ -95,3 +101,9 @@ impl From<io::Error> for ApiError {
ApiError::IoError(error)
}
}
impl From<CloudflareError> for ApiError {
fn from(error: CloudflareError) -> Self {
ApiError::CloudflareError(error)
}
}
@@ -0,0 +1,43 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_get_request::basic_json_get_request;
use artcraft_api_defs::jobs::list_session_jobs::{ListSessionJobsSuccessResponse, LIST_SESSION_JOBS_URL_PATH};
use enums::common::job_status_plus::JobStatusPlus;
use std::collections::HashSet;
#[derive(Clone)]
pub enum States {
All,
Include(HashSet<JobStatusPlus>),
Exclude(HashSet<JobStatusPlus>),
}
pub async fn list_session_jobs(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
job_states: States,
) -> Result<ListSessionJobsSuccessResponse, StorytellerError> {
let url = match job_states {
States::All =>
LIST_SESSION_JOBS_URL_PATH.to_string(),
States::Include(states) =>
format!("{}?include_states={}", LIST_SESSION_JOBS_URL_PATH, states_string(&states)),
States::Exclude(states) =>
format!("{}?exclude_states={}", LIST_SESSION_JOBS_URL_PATH, states_string(&states)),
};
Ok(basic_json_get_request(
api_host,
&url,
maybe_creds,
).await?)
}
fn states_string(states: &HashSet<JobStatusPlus>) -> String {
states.iter()
.map(|state| state.to_str())
.collect::<Vec<_>>()
.join(",")
}
@@ -0,0 +1 @@
pub mod list_session_jobs;
@@ -3,6 +3,7 @@
pub mod credentials;
pub mod error;
pub mod generate;
pub mod jobs;
pub mod media_files;
pub mod recipes;
pub mod shared_response_types;
@@ -0,0 +1,63 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::api_error::ApiError;
use crate::error::client_error::ClientError;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
use crate::utils::filter_bad_response::filter_bad_response;
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::{GenerateFlux1SchnellTextToImageRequest, GenerateFlux1SchnellTextToImageResponse, GENERATE_FLUX_1_SCHNELL_TEXT_TO_IMAGE_PATH};
use log::debug;
use reqwest::Client;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
pub async fn basic_json_get_request<Res: DeserializeOwned>(
api_host: &ApiHost,
route_path: &str,
maybe_creds: Option<&StorytellerCredentialSet>,
) -> Result<Res, StorytellerError> {
let url = get_route(api_host, route_path);
debug!("Requesting {:?}", &url);
let client = Client::builder()
.gzip(true)
.build()
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
let mut request_builder = client.get(url)
.header("User-Agent", USER_AGENT)
.header("Accept", 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 response = request_builder
.send()
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let response = filter_bad_response(response)
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let response_body = &response.text()
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
debug!("Response body: {:?}", response_body);
let response = serde_json::from_str(&response_body)
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
Ok(response)
}
fn get_route(api_host: &ApiHost, route_path: &str) -> String {
let api_hostname = api_host.to_api_hostname_and_scheme();
format!("{}{}", api_hostname, route_path)
}
@@ -1,6 +1,7 @@
use crate::error::api_error::ApiError;
use crate::utils::status_codes::*;
use anyhow::anyhow;
use cloudflare_errors::filter_cloudflare_errors::filter_cloudflare_errors;
use log::error;
/// Pass request errors to this method for standard error handling behavior.
@@ -13,30 +14,34 @@ use log::error;
pub async fn filter_bad_response(response: reqwest::Response) -> Result<reqwest::Response, ApiError> {
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),
};
return match status.as_u16() {
STATUS_401_UNAUTHORIZED => Err(ApiError::Unauthorized(response_body)),
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 { body: response_body, backend_hostname }),
_ => Err(ApiError::Other(anyhow!("Bad status code: {}; message: {:?}", status, response_body))),
}
if status.is_success() {
return Ok(response);
}
Ok(response)
}
// 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),
};
let status_code = status.as_u16();
filter_cloudflare_errors(status_code, &response_body)?;
match status_code {
STATUS_401_UNAUTHORIZED => Err(ApiError::Unauthorized(response_body)),
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 { 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_get_request;
pub mod basic_json_post_request;
pub mod constants;
pub mod filter_bad_response;
@@ -43,7 +43,7 @@ rand.workspace = true
# NB: We switched to Tokio from `async-std` for broader ecosystem support.
tokio = { version = "1.38.0", features = ["macros"] }
sqlx = { version = "0.7.2", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] }
sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] }
elasticsearch = "8.5.0-alpha.1"
@@ -39,7 +39,7 @@ log.workspace = true
# NB: We switched to Tokio from `async-std` for broader ecosystem support.
tokio.workspace = true
sqlx = { version = "0.7.2", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] }
sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] }
[dev-dependencies]
# None yet
+1 -1
View File
@@ -36,7 +36,7 @@ log = "0.4.14"
# Bigger pieces
futures = { version = "0.3.17", features = ["thread-pool"] }
sqlx = { version = "0.7.2", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] }
sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] }
tokio = { version = "1.38.0", features = ["macros"] }
# Serialization
+1 -1
View File
@@ -36,7 +36,7 @@ log = "0.4.14"
# Bigger pieces
tokio = { version = "1.38.0", features = ["macros"] }
sqlx = { version = "0.7.2", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] }
sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] }
# Serialization
serde = "1.0.125"
@@ -38,7 +38,7 @@ strum = { version = "0.26", features = ["derive"] }
# Bigger pieces
tokio = { version = "1.38.0", features = ["macros"] }
sqlx = { version = "0.7.2", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] }
sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] }
elasticsearch = "8.5.0-alpha.1"
# Serialization
@@ -40,7 +40,7 @@ tempdir.workspace = true
# Async + clients
tokio.workspace = true
sqlx = { version = "0.7.2", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] }
sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] }
# Serialization
serde.workspace = true
+3 -1
View File
@@ -28,6 +28,7 @@ workspace-build-acceleration = { workspace = true }
# Internal
artcraft_api_defs = { workspace = true }
build_metadata = { workspace = true }
enums = { workspace = true }
errors = { workspace = true }
fal_client = { workspace = true }
filesys = { workspace = true }
@@ -36,6 +37,7 @@ memory_store = { workspace = true }
mimetypes = { workspace = true }
openai_sora_client = { workspace = true }
primitives = { workspace = true }
sqlite_tasks.workspace = true
storyteller_client = { workspace = true }
tokens = { workspace = true }
web_base64 = { workspace = true }
@@ -58,6 +60,7 @@ serde = { workspace = true }
serde_derive = { workspace = true }
serde_json = { workspace = true }
tauri = { workspace = true }
tauri-plugin-dialog = "2"
tauri-plugin-log = "2.3.1"
tauri-plugin-upload = "2.2.1"
tempdir = { workspace = true }
@@ -72,7 +75,6 @@ tauri-plugin-http = { path = "../../vendor/tauri-plugin-http" }
# NB: The version of the `discord-rich-presence` crate (0.2.5) last published has non-Send/Sync error
# types and can't do threading. We need to wait for a new version to unpin from Git.
discord-rich-presence = { git = "https://github.com/vionya/discord-rich-presence" }
tauri-plugin-dialog = "2"
[dev-dependencies]
serial_test = "3.2.0"
@@ -1,8 +1,8 @@
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::generic::handle_image_artcraft::handle_image_artcraft;
use crate::core::commands::enqueue::image::generic::handle_image_fal::handle_image_fal;
use crate::core::commands::enqueue::image::gpt_image_1::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::task_enqueue_success::TaskEnqueueSuccess;
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;
@@ -14,14 +14,19 @@ 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::provider_priority::{Provider, ProviderPriorityStore};
use crate::core::state::task_database::TaskDatabase;
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
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 enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_status::TaskStatus;
use enums::tauri::tasks::task_type::TaskType;
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 log::{error, info, warn};
use serde::{Deserialize, Serialize};
use sqlite_tasks::queries::create_task::{create_task, CreateTaskArgs};
use tauri::{AppHandle, State};
#[derive(Deserialize)]
@@ -31,8 +36,25 @@ pub struct EnqueueTextToImageRequest {
/// The model to use.
pub model: Option<ImageModel>,
/// Aspect ratio.
pub aspect_ratio: Option<TextToImageSize>,
}
// TODO(bt,2025-07-14): Support other aspect ratios / resolutions -
// Flux Dev has: 4:3, 16:9, 3:4, 9:16, and custom.
// Flux Schnell has: 4:3, 16:9, 3:4, 9:16, and custom.
// Flux Pro has: 4:3, 16:9, 3:4, 9:16, and custom.
// Flux Pro Ulra has: 4:3, 16:9, 3:4, 9:16, and custom.
#[derive(Deserialize, Debug, Copy, Clone)]
#[serde(rename_all = "snake_case")]
pub enum TextToImageSize {
Auto,
Square,
Wide,
Tall,
}
#[derive(Serialize)]
pub struct EnqueueTextToImageSuccessResponse {
@@ -60,11 +82,12 @@ pub enum EnqueueTextToImageErrorType {
#[tauri::command]
pub async fn enqueue_text_to_image_command(
app: AppHandle,
request: EnqueueTextToImageRequest,
app: AppHandle,
app_data_root: State<'_, AppDataRoot>,
app_env_configs: State<'_, AppEnvConfigs>,
provider_priority_store: State<'_, ProviderPriorityStore>,
task_database: State<'_, TaskDatabase>,
fal_creds_manager: State<'_, FalCredentialManager>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
fal_task_queue: State<'_, FalTaskQueue>,
@@ -75,10 +98,11 @@ pub async fn enqueue_text_to_image_command(
info!("enqueue_text_to_image called");
let result = handle_request(
&app,
request,
&app,
&app_data_root,
&provider_priority_store,
&task_database,
&fal_creds_manager,
&storyteller_creds_manager,
&app_env_configs,
@@ -123,9 +147,9 @@ pub async fn enqueue_text_to_image_command(
}
Ok(event) => {
let event = GenerationEnqueueSuccessEvent {
action: GenerationAction::GenerateImage,
service: event.service_provider,
model: Some(event.tauri_event_model()),
action: event.to_frontend_event_action(),
service: event.to_frontend_event_service(),
model: event.model,
};
if let Err(err) = event.send(&app) {
@@ -139,8 +163,52 @@ pub async fn enqueue_text_to_image_command(
pub async fn handle_request(
app: &AppHandle,
request: EnqueueTextToImageRequest,
app: &AppHandle,
app_data_root: &AppDataRoot,
provider_priority_store: &ProviderPriorityStore,
task_database: &TaskDatabase,
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<TaskEnqueueSuccess, InternalImageError> {
let result = dispatch_request(
request,
&app,
&app_data_root,
&provider_priority_store,
&fal_creds_manager,
&storyteller_creds_manager,
&app_env_configs,
&fal_task_queue,
&sora_creds_manager,
&sora_task_queue,
).await;
let success_event = match result {
Err(err) => return Err(err),
Ok(event) => event,
};
let result = success_event
.insert_into_task_database(task_database)
.await;
if let Err(err) = result {
error!("Failed to create task in database: {:?}", err);
// NB: Fail open, but find a way to flag this.
}
Ok(success_event)
}
pub async fn dispatch_request(
request: EnqueueTextToImageRequest,
app: &AppHandle,
app_data_root: &AppDataRoot,
provider_priority_store: &ProviderPriorityStore,
fal_creds_manager: &FalCredentialManager,
@@ -149,18 +217,14 @@ pub async fn handle_request(
fal_task_queue: &FalTaskQueue,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<SuccessEvent, InternalImageError> {
) -> Result<TaskEnqueueSuccess, InternalImageError> {
match request.model {
None => {
return Err(InternalImageError::NoModelSpecified);
}
Some(ImageModel::GptImage1) => {
handle_image_sora(&app, request, sora_creds_manager, sora_task_queue).await?;
return Ok(SuccessEvent {
service_provider: GenerationServiceProvider::Sora,
model: ImageModel::GptImage1,
});
return handle_image_sora(&app, request, sora_creds_manager, sora_task_queue).await;
}
_ => {
// Fall-through
@@ -1,18 +1,20 @@
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::EnqueueTextToImageRequest;
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
use crate::core::commands::enqueue::image::success_event::SuccessEvent;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
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::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::image::generate_flux_1_dev_text_to_image::{GenerateFlux1DevTextToImageAspectRatio, GenerateFlux1DevTextToImageRequest};
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::{GenerateFlux1SchnellTextToImageAspectRatio, GenerateFlux1SchnellTextToImageRequest};
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::{GenerateFluxPro11TextToImageAspectRatio, GenerateFluxPro11TextToImageRequest};
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::{GenerateFluxPro11UltraTextToImageAspectRatio, GenerateFluxPro11UltraTextToImageRequest};
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
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 idempotency::uuid::generate_random_uuid;
use log::{error, info};
@@ -28,7 +30,7 @@ pub async fn handle_image_artcraft(
app_env_configs: &AppEnvConfigs,
app_data_root: &AppDataRoot,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<SuccessEvent, InternalImageError> {
) -> Result<TaskEnqueueSuccess, InternalImageError> {
let creds = match storyteller_creds_manager.get_credentials()? {
Some(creds) => creds,
@@ -63,11 +65,19 @@ pub async fn handle_image_artcraft(
}
Some(ImageModel::Flux1Dev) => {
info!("enqueue Flux 1 Dev");
selected_model = ImageModel::Flux1Dev;
selected_model = GenerationModel::Flux1Dev;
let aspect_ratio = request.aspect_ratio
.map(|aspect| match aspect {
// TODO(bt,2025-07-14): Support other aspect ratios.
TextToImageSize::Tall => GenerateFlux1DevTextToImageAspectRatio::PortraitNineBySixteen,
TextToImageSize::Wide => GenerateFlux1DevTextToImageAspectRatio::LandscapeSixteenByNine,
TextToImageSize::Auto => GenerateFlux1DevTextToImageAspectRatio::SquareHd,
TextToImageSize::Square => GenerateFlux1DevTextToImageAspectRatio::SquareHd,
});
let request = GenerateFlux1DevTextToImageRequest {
uuid_idempotency_token,
prompt: request.prompt,
aspect_ratio: None,
aspect_ratio,
num_images: None,
};
let result = generate_flux_1_dev_text_to_image(
@@ -88,11 +98,19 @@ pub async fn handle_image_artcraft(
}
Some(ImageModel::Flux1Schnell) => {
info!("enqueue Flux 1 Schnell");
selected_model = ImageModel::Flux1Schnell;
selected_model = GenerationModel::Flux1Schnell;
let aspect_ratio = request.aspect_ratio
.map(|aspect| match aspect {
// TODO(bt,2025-07-14): Support other aspect ratios.
TextToImageSize::Tall => GenerateFlux1SchnellTextToImageAspectRatio::PortraitNineBySixteen,
TextToImageSize::Wide => GenerateFlux1SchnellTextToImageAspectRatio::LandscapeSixteenByNine,
TextToImageSize::Auto => GenerateFlux1SchnellTextToImageAspectRatio::SquareHd,
TextToImageSize::Square => GenerateFlux1SchnellTextToImageAspectRatio::SquareHd,
});
let request = GenerateFlux1SchnellTextToImageRequest {
uuid_idempotency_token,
prompt: request.prompt,
aspect_ratio: None,
aspect_ratio,
num_images: None,
};
let result = generate_flux_1_schnell_text_to_image(
@@ -113,11 +131,19 @@ pub async fn handle_image_artcraft(
}
Some(ImageModel::FluxPro11) => {
info!("enqueue Flux Pro 1.1");
selected_model = ImageModel::FluxPro11;
selected_model = GenerationModel::FluxPro11;
let aspect_ratio = request.aspect_ratio
.map(|aspect| match aspect {
// TODO(bt,2025-07-14): Support other aspect ratios.
TextToImageSize::Tall => GenerateFluxPro11TextToImageAspectRatio::PortraitNineBySixteen,
TextToImageSize::Wide => GenerateFluxPro11TextToImageAspectRatio::LandscapeSixteenByNine,
TextToImageSize::Auto => GenerateFluxPro11TextToImageAspectRatio::Square,
TextToImageSize::Square => GenerateFluxPro11TextToImageAspectRatio::Square,
});
let request = GenerateFluxPro11TextToImageRequest {
uuid_idempotency_token,
prompt: request.prompt,
aspect_ratio: None,
aspect_ratio,
num_images: None,
};
let result = generate_flux_pro_11_text_to_image(
@@ -138,11 +164,19 @@ pub async fn handle_image_artcraft(
}
Some(ImageModel::FluxPro11Ultra) => {
info!("enqueue Flux Pro 1.1 Ultra");
selected_model = ImageModel::FluxPro11Ultra;
selected_model = GenerationModel::FluxPro11Ultra;
let aspect_ratio = request.aspect_ratio
.map(|aspect| match aspect {
// TODO(bt,2025-07-14): Support other aspect ratios.
TextToImageSize::Tall => GenerateFluxPro11UltraTextToImageAspectRatio::PortraitNineBySixteen,
TextToImageSize::Wide => GenerateFluxPro11UltraTextToImageAspectRatio::LandscapeSixteenByNine,
TextToImageSize::Auto => GenerateFluxPro11UltraTextToImageAspectRatio::Square,
TextToImageSize::Square => GenerateFluxPro11UltraTextToImageAspectRatio::Square,
});
let request = GenerateFluxPro11UltraTextToImageRequest {
uuid_idempotency_token,
prompt: request.prompt,
aspect_ratio: None,
aspect_ratio,
num_images: None,
};
let result = generate_flux_pro_11_ultra_text_to_image(
@@ -163,8 +197,10 @@ pub async fn handle_image_artcraft(
}
};
Ok(SuccessEvent {
service_provider: GenerationServiceProvider::Artcraft,
model:selected_model,
Ok(TaskEnqueueSuccess {
provider: GenerationProvider::Artcraft,
task_type: TaskType::ImageGeneration,
model: Some(selected_model),
provider_job_id: Some(job_token.to_string()),
})
}
@@ -1,13 +1,15 @@
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::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::model::image_models::ImageModel;
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
use anyhow::anyhow;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
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 log::{error, info};
@@ -18,7 +20,7 @@ pub async fn handle_image_fal(
request: EnqueueTextToImageRequest,
fal_creds_manager: &FalCredentialManager,
fal_task_queue: &FalTaskQueue,
) -> Result<SuccessEvent, InternalImageError> {
) -> Result<TaskEnqueueSuccess, InternalImageError> {
let api_key = match fal_creds_manager.get_key()? {
Some(key) => key,
@@ -60,7 +62,7 @@ pub async fn handle_image_fal(
return Err(InternalImageError::AnyhowError(anyhow!("not yet implemented: {:?}", request.model)));
}
Some(ImageModel::FluxPro11Ultra) => {
model = ImageModel::FluxPro11Ultra;
model = GenerationModel::FluxPro11Ultra;
info!("enqueue Flux Pro 1.1 Ultra text-to-image with prompt: {}", prompt);
enqueue_flux_pro_11_ultra_text_to_image(FluxPro11UltraTextToImageArgs {
prompt,
@@ -68,7 +70,7 @@ pub async fn handle_image_fal(
}).await
}
Some(ImageModel::Recraft3) => {
model = ImageModel::Recraft3;
model = GenerationModel::Recraft3;
info!("enqueue Recraft v3 text-to-image with prompt: {}", prompt);
enqueue_recraft3_text_to_image(Recraft3TextToImageArgs {
prompt,
@@ -77,7 +79,7 @@ pub async fn handle_image_fal(
}
};
match result {
let success_result = match result {
Ok(enqueued) => {
info!("Successfully enqueued text to image");
@@ -95,6 +97,8 @@ pub async fn handle_image_fal(
error!("Failed to enqueue task: {:?}", err);
return Err(InternalImageError::AnyhowError(anyhow!("Failed to enqueue task: {:?}", err)));
}
enqueued
}
Err(err) => {
error!("Failed to enqueue text to image: {:?}", err);
@@ -112,10 +116,12 @@ pub async fn handle_image_fal(
return Err(InternalImageError::FalError(err));
}
}
};
Ok(SuccessEvent {
service_provider: GenerationServiceProvider::Fal,
model,
Ok(TaskEnqueueSuccess {
provider: GenerationProvider::Fal,
task_type: TaskType::ImageGeneration,
model: Some(model),
provider_job_id: Some(success_result.request_id.to_string()),
})
}
@@ -0,0 +1,2 @@
pub mod handle_image_artcraft;
pub mod handle_image_fal;
@@ -1,10 +1,14 @@
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::EnqueueTextToImageRequest;
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::model::image_models::ImageModel;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
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 log::{error, info};
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
@@ -20,7 +24,7 @@ pub async fn handle_image_sora(
request: EnqueueTextToImageRequest,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<(), InternalImageError> {
) -> Result<TaskEnqueueSuccess, InternalImageError> {
let mut creds = sora_creds_manager
.get_credentials_required()
@@ -42,12 +46,20 @@ pub async fn handle_image_sora(
}
let prompt = request.prompt.as_deref().unwrap_or("");
let size = match request.aspect_ratio {
None => ImageSize::Square,
Some(TextToImageSize::Auto) => ImageSize::Square,
Some(TextToImageSize::Square) => ImageSize::Square,
Some(TextToImageSize::Tall) => ImageSize::Tall,
Some(TextToImageSize::Wide) => ImageSize::Wide,
};
let result =
simple_image_gen_with_session_auto_renew(SimpleImageGenAutoRenewRequest {
prompt: prompt.to_string(),
num_images: NumImages::One,
image_size: ImageSize::Square,
image_size: size,
credentials: &creds,
request_timeout: Some(SORA_SIMPLE_IMAGE_GEN_TIMEOUT),
}).await;
@@ -80,5 +92,10 @@ pub async fn handle_image_sora(
sora_task_queue.insert(&response.task_id)?;
Ok(())
Ok(TaskEnqueueSuccess {
provider: GenerationProvider::Sora,
task_type: TaskType::ImageGeneration,
model: Some(GenerationModel::GptImage1),
provider_job_id: Some(response.task_id.to_string()),
})
}
@@ -0,0 +1 @@
pub mod handle_image_sora;
@@ -1,6 +1,4 @@
pub mod enqueue_text_to_image_command;
pub mod handle_image_artcraft;
pub mod handle_image_fal;
pub mod handle_image_sora;
pub mod generic;
pub mod internal_image_error;
mod success_event;
mod gpt_image_1;
@@ -1,20 +0,0 @@
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,
}
}
}
@@ -1,14 +1,10 @@
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, EnqueueTextToImageSuccessResponse};
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_bg_removal::errors::InternalBgRemovalError;
use crate::core::commands::enqueue::image_bg_removal::generic::handle_generic_bg_removal::handle_generic_bg_removal;
use crate::core::commands::enqueue::image_bg_removal::success_event::EnqueueImageBgRemovalSuccessEvent;
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1::handle_gpt_image_1;
use crate::core::commands::enqueue::image_edit::success_event::ContextualEditImageSuccessEvent;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use crate::core::commands::response::shorthand::{Response, ResponseOrErrorType};
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
@@ -23,6 +19,7 @@ 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::state::task_database::TaskDatabase;
use crate::core::utils::get_url_file_extension::get_url_file_extension;
use crate::core::utils::simple_http_download::simple_http_download;
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
@@ -30,6 +27,8 @@ use crate::services::fal::state::fal_task_queue::FalTaskQueue;
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 enums::tauri::tasks::task_status::TaskStatus;
use enums::tauri::tasks::task_type::TaskType;
use errors::AnyhowError;
use log::{error, info, warn};
use openai_sora_client::recipes::image_remix_with_session_auto_renew::{image_remix_with_session_auto_renew, ImageRemixAutoRenewRequest};
@@ -38,6 +37,7 @@ use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_o
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
use openai_sora_client::sora_error::SoraError;
use serde_derive::{Deserialize, Serialize};
use sqlite_tasks::queries::create_task::{create_task, CreateTaskArgs};
use std::time::Duration;
use storyteller_client::error::storyteller_error::StorytellerError;
use storyteller_client::media_files::get_media_file::get_media_file;
@@ -89,6 +89,7 @@ pub async fn enqueue_image_bg_removal_command(
app_data_root: State<'_, AppDataRoot>,
app_env_configs: State<'_, AppEnvConfigs>,
provider_priority_store: State<'_, ProviderPriorityStore>,
task_database: State<'_, TaskDatabase>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
fal_creds_manager: State<'_, FalCredentialManager>,
fal_task_queue: State<'_, FalTaskQueue>,
@@ -102,6 +103,7 @@ pub async fn enqueue_image_bg_removal_command(
&app_data_root,
&app_env_configs,
&provider_priority_store,
&task_database,
&storyteller_creds_manager,
&fal_creds_manager,
&fal_task_queue,
@@ -127,8 +129,8 @@ pub async fn enqueue_image_bg_removal_command(
}
Ok(event) => {
let event = GenerationEnqueueSuccessEvent {
action: GenerationAction::RemoveBackground,
service: event.service_provider,
action: event.to_frontend_event_action(),
service: event.to_frontend_event_service(),
model: None, // FIXME: This isn't right, though we probably don't care for simple bg removal.
};
@@ -147,14 +149,15 @@ pub async fn handle_request(
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
provider_priority_store: &ProviderPriorityStore,
task_database: &TaskDatabase,
storyteller_creds_manager: &StorytellerCredentialManager,
fal_creds_manager: &FalCredentialManager,
fal_task_queue: &FalTaskQueue,
) -> Result<EnqueueImageBgRemovalSuccessEvent, InternalBgRemovalError> {
) -> Result<TaskEnqueueSuccess, InternalBgRemovalError> {
// TODO(bt,2025-07-07): Other model/provider routing...
handle_generic_bg_removal(
let success_event = handle_generic_bg_removal(
request,
app,
app_data_root,
@@ -163,5 +166,16 @@ pub async fn handle_request(
storyteller_creds_manager,
fal_creds_manager,
fal_task_queue,
).await
).await?;
let result = success_event
.insert_into_task_database(task_database)
.await;
if let Err(err) = result {
error!("Failed to create task in database: {:?}", err);
// NB: Fail open, but find a way to flag this.
}
Ok(success_event)
}
@@ -2,12 +2,11 @@ use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_c
use crate::core::commands::enqueue::image_bg_removal::errors::InternalBgRemovalError;
use crate::core::commands::enqueue::image_bg_removal::generic::handle_generic_bg_removal_artcraft::handle_generic_bg_removal_artcraft;
use crate::core::commands::enqueue::image_bg_removal::generic::handle_generic_bg_removal_fal::handle_generic_bg_removal_fal;
use crate::core::commands::enqueue::image_bg_removal::success_event::EnqueueImageBgRemovalSuccessEvent;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageCommand;
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_artcraft::handle_gpt_image_1_artcraft;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_sora::handle_gpt_image_1_sora;
use crate::core::commands::enqueue::image_edit::success_event::ContextualEditImageSuccessEvent;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
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::provider_priority::{Provider, ProviderPriorityStore};
@@ -28,7 +27,7 @@ pub async fn handle_generic_bg_removal(
storyteller_creds_manager: &StorytellerCredentialManager,
fal_creds_manager: &FalCredentialManager,
fal_task_queue: &FalTaskQueue,
) -> Result<EnqueueImageBgRemovalSuccessEvent, InternalBgRemovalError> {
) -> Result<TaskEnqueueSuccess, InternalBgRemovalError> {
let priority = provider_priority_store.get_priority()?;
@@ -2,10 +2,9 @@ use crate::core::commands::enqueue::image::enqueue_text_to_image_command::Enqueu
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalCommand;
use crate::core::commands::enqueue::image_bg_removal::errors::InternalBgRemovalError;
use crate::core::commands::enqueue::image_bg_removal::success_event::EnqueueImageBgRemovalSuccessEvent;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize, EnqueueContextualEditImageCommand};
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::image_edit::success_event::ContextualEditImageSuccessEvent;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
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;
@@ -27,6 +26,8 @@ use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::G
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::image::remove_image_background::RemoveImageBackgroundRequest;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
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::webhook::image::enqueue_gpt_image_1_edit_image_webhook::GptEditImageQuality;
use idempotency::uuid::generate_random_uuid;
@@ -48,7 +49,7 @@ pub async fn handle_generic_bg_removal_artcraft(
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<EnqueueImageBgRemovalSuccessEvent, InternalBgRemovalError> {
) -> Result<TaskEnqueueSuccess, InternalBgRemovalError> {
let creds = match storyteller_creds_manager.get_credentials()? {
Some(creds) => creds,
@@ -79,20 +80,24 @@ pub async fn handle_generic_bg_removal_artcraft(
request,
).await;
match result {
let success_result = match result {
Ok(enqueued) => {
// TODO(bt,2025-07-05): Enqueue job token?
info!("Successfully enqueued Artcraft background removal. Job token: {}",
enqueued.inference_job_token);
enqueued
}
Err(err) => {
error!("Failed to use Artcraft background removal: {:?}", err);
return Err(InternalBgRemovalError::StorytellerError(err));
}
}
};
Ok(EnqueueImageBgRemovalSuccessEvent {
service_provider: GenerationServiceProvider::Artcraft,
Ok(TaskEnqueueSuccess {
provider: GenerationProvider::Artcraft,
task_type: TaskType::BackgroundRemoval,
provider_job_id: Some(success_result.inference_job_token.to_string()),
model: None,
})
}
@@ -1,6 +1,6 @@
use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalCommand;
use crate::core::commands::enqueue::image_bg_removal::errors::InternalBgRemovalError;
use crate::core::commands::enqueue::image_bg_removal::success_event::EnqueueImageBgRemovalSuccessEvent;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
@@ -14,6 +14,6 @@ pub async fn handle_generic_bg_removal_fal(
app_env_configs: &AppEnvConfigs,
fal_creds_manager: &FalCredentialManager,
fal_task_queue: &FalTaskQueue,
) -> Result<EnqueueImageBgRemovalSuccessEvent, InternalBgRemovalError> {
) -> Result<TaskEnqueueSuccess, InternalBgRemovalError> {
unimplemented!("TODO: Implement fal background removal handling");
}
@@ -1,4 +1,3 @@
pub mod enqueue_image_bg_removal_command;
pub mod errors;
pub mod generic;
pub mod success_event;
pub mod generic;
@@ -1,16 +0,0 @@
use crate::core::events::generation_events::common::{GenerationModel, GenerationServiceProvider};
use crate::core::model::contextual_image_edit_models::ContextualImageEditModel;
use crate::core::model::image_models::ImageModel;
pub struct EnqueueImageBgRemovalSuccessEvent {
pub service_provider: GenerationServiceProvider,
//pub model: ContextualImageEditModel,
}
impl EnqueueImageBgRemovalSuccessEvent {
//pub fn tauri_event_model(&self) -> GenerationModel {
// match self.model {
// ContextualImageEditModel::GptImage1 => GenerationModel::GptImage1,
// }
//}
}
@@ -1,11 +1,8 @@
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, EnqueueTextToImageSuccessResponse};
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_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1::handle_gpt_image_1;
use crate::core::commands::enqueue::image_edit::success_event::ContextualEditImageSuccessEvent;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use crate::core::commands::response::shorthand::{Response, ResponseOrErrorType};
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
@@ -19,7 +16,8 @@ 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::state::provider_priority::ProviderPriorityStore;
use crate::core::state::task_database::TaskDatabase;
use crate::core::utils::get_url_file_extension::get_url_file_extension;
use crate::core::utils::simple_http_download::simple_http_download;
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
@@ -27,6 +25,9 @@ use crate::services::fal::state::fal_task_queue::FalTaskQueue;
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 enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_status::TaskStatus;
use enums::tauri::tasks::task_type::TaskType;
use errors::AnyhowError;
use log::{error, info, warn};
use openai_sora_client::recipes::image_remix_with_session_auto_renew::{image_remix_with_session_auto_renew, ImageRemixAutoRenewRequest};
@@ -35,6 +36,7 @@ use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_o
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
use openai_sora_client::sora_error::SoraError;
use serde_derive::{Deserialize, Serialize};
use sqlite_tasks::queries::create_task::{create_task, CreateTaskArgs};
use std::time::Duration;
use storyteller_client::error::storyteller_error::StorytellerError;
use storyteller_client::media_files::get_media_file::get_media_file;
@@ -42,7 +44,6 @@ use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, Manager, State};
use tokens::tokens::media_files::MediaFileToken;
#[derive(Deserialize, Debug)]
pub struct EnqueueContextualEditImageCommand {
/// The model to use.
@@ -130,6 +131,7 @@ pub async fn enqueue_contextual_edit_image_command(
app_data_root: State<'_, AppDataRoot>,
app_env_configs: State<'_, AppEnvConfigs>,
provider_priority_store: State<'_, ProviderPriorityStore>,
task_database: State<'_, TaskDatabase>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
fal_creds_manager: State<'_, FalCredentialManager>,
fal_task_queue: State<'_, FalTaskQueue>,
@@ -146,6 +148,7 @@ pub async fn enqueue_contextual_edit_image_command(
&app_data_root,
&app_env_configs,
&provider_priority_store,
&task_database,
&storyteller_creds_manager,
&fal_creds_manager,
&fal_task_queue,
@@ -173,9 +176,9 @@ pub async fn enqueue_contextual_edit_image_command(
}
Ok(event) => {
let event = GenerationEnqueueSuccessEvent {
action: GenerationAction::GenerateImage,
service: event.service_provider,
model: Some(event.tauri_event_model()),
action: event.to_frontend_event_action(),
service: event.to_frontend_event_service(),
model: event.model,
};
if let Err(err) = event.send(&app) {
@@ -193,15 +196,16 @@ pub async fn handle_request(
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
provider_priority_store: &ProviderPriorityStore,
task_database: &TaskDatabase,
storyteller_creds_manager: &StorytellerCredentialManager,
fal_creds_manager: &FalCredentialManager,
fal_task_queue: &FalTaskQueue,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<ContextualEditImageSuccessEvent, InternalContextualEditImageError> {
match request.model {
) -> Result<TaskEnqueueSuccess, InternalContextualEditImageError> {
let success_event= match request.model {
None => {
Err(InternalContextualEditImageError::NoModelSpecified)
return Err(InternalContextualEditImageError::NoModelSpecified)
}
Some(ContextualImageEditModel::GptImage1) => {
handle_gpt_image_1(
@@ -215,8 +219,19 @@ pub async fn handle_request(
fal_task_queue,
sora_creds_manager,
sora_task_queue,
).await
).await?
}
// TODO(bt,2025-07-05): Flux Kontext, etc.
};
let result = success_event
.insert_into_task_database(task_database)
.await;
if let Err(err) = result {
error!("Failed to create task in database: {:?}", err);
// NB: Fail open, but find a way to flag this.
}
Ok(success_event)
}
@@ -2,7 +2,7 @@ use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_co
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_artcraft::handle_gpt_image_1_artcraft;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_sora::handle_gpt_image_1_sora;
use crate::core::commands::enqueue::image_edit::success_event::ContextualEditImageSuccessEvent;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
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::provider_priority::{Provider, ProviderPriorityStore};
@@ -27,7 +27,7 @@ pub async fn handle_gpt_image_1(
fal_task_queue: &FalTaskQueue,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<ContextualEditImageSuccessEvent, InternalContextualEditImageError> {
) -> Result<TaskEnqueueSuccess, InternalContextualEditImageError> {
let priority = provider_priority_store.get_priority()?;
@@ -3,9 +3,9 @@ use crate::core::commands::enqueue::image::internal_image_error::InternalImageEr
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize, EnqueueContextualEditImageCommand};
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1::MAX_IMAGES;
use crate::core::commands::enqueue::image_edit::success_event::ContextualEditImageSuccessEvent;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::model::contextual_image_edit_models::ContextualImageEditModel;
use crate::core::model::image_models::ImageModel;
@@ -33,6 +33,8 @@ use storyteller_client::generate::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 tauri::AppHandle;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
pub async fn handle_gpt_image_1_artcraft(
request: &EnqueueContextualEditImageCommand,
@@ -40,7 +42,7 @@ pub async fn handle_gpt_image_1_artcraft(
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<ContextualEditImageSuccessEvent, InternalContextualEditImageError> {
) -> Result<TaskEnqueueSuccess, InternalContextualEditImageError> {
let creds = match storyteller_creds_manager.get_credentials()? {
Some(creds) => creds,
@@ -117,20 +119,23 @@ pub async fn handle_gpt_image_1_artcraft(
request,
).await;
match result {
let job_id = match result {
Ok(enqueued) => {
// TODO(bt,2025-07-05): Enqueue job token?
info!("Successfully enqueued Artcraft gpt-image-1. Job token: {}",
enqueued.inference_job_token);
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft gpt-image-1: {:?}", err);
return Err(InternalContextualEditImageError::StorytellerError(err));
}
}
};
Ok(ContextualEditImageSuccessEvent {
service_provider: GenerationServiceProvider::Artcraft,
model: ContextualImageEditModel::GptImage1,
Ok(TaskEnqueueSuccess {
provider: GenerationProvider::Artcraft,
model: Some(GenerationModel::GptImage1),
provider_job_id: Some(job_id.to_string()),
task_type: TaskType::ImageGeneration,
})
}
@@ -3,10 +3,10 @@ use crate::core::commands::enqueue::image::internal_image_error::InternalImageEr
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageSize, EnqueueContextualEditImageCommand, EnqueueContextualEditImageErrorType};
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1::MAX_IMAGES;
use crate::core::commands::enqueue::image_edit::success_event::ContextualEditImageSuccessEvent;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
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::contextual_image_edit_models::ContextualImageEditModel;
@@ -27,6 +27,8 @@ use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::Gener
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 enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use errors::AnyhowResult;
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 idempotency::uuid::generate_random_uuid;
@@ -53,8 +55,7 @@ pub async fn handle_gpt_image_1_sora(
app_env_configs: &AppEnvConfigs,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<ContextualEditImageSuccessEvent, InternalContextualEditImageError> {
) -> Result<TaskEnqueueSuccess, InternalContextualEditImageError> {
let sora_creds = match sora_creds_manager.get_credentials() {
Ok(Some(creds)) => creds,
@@ -182,8 +183,10 @@ pub async fn handle_gpt_image_1_sora(
sora_task_queue.insert(&response.task_id)?;
Ok(ContextualEditImageSuccessEvent {
service_provider: GenerationServiceProvider::Sora,
model: ContextualImageEditModel::GptImage1,
Ok(TaskEnqueueSuccess {
task_type: TaskType::ImageGeneration,
model: Some(GenerationModel::GptImage1),
provider: GenerationProvider::Sora,
provider_job_id: Some(response.task_id.to_string()),
})
}
@@ -1,4 +1,3 @@
pub mod enqueue_contextual_edit_image_command;
pub mod errors;
pub mod gpt_image_1;
pub mod success_event;
pub mod gpt_image_1;
@@ -1,16 +0,0 @@
use crate::core::events::generation_events::common::{GenerationModel, GenerationServiceProvider};
use crate::core::model::contextual_image_edit_models::ContextualImageEditModel;
use crate::core::model::image_models::ImageModel;
pub struct ContextualEditImageSuccessEvent {
pub service_provider: GenerationServiceProvider,
pub model: ContextualImageEditModel,
}
impl ContextualEditImageSuccessEvent {
pub fn tauri_event_model(&self) -> GenerationModel {
match self.model {
ContextualImageEditModel::GptImage1 => GenerationModel::GptImage1,
}
}
}
@@ -1,5 +1,6 @@
pub mod image;
pub mod image_bg_removal;
pub mod image_edit;
pub mod task_enqueue_success;
pub mod object;
pub mod video;
@@ -1,9 +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::object::generic::handle_object::handle_object;
use crate::core::commands::enqueue::object::internal_object_error::InternalObjectError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
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::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
@@ -15,7 +16,9 @@ use log::{error, info, warn};
use serde_derive::{Deserialize, Serialize};
use tauri::{AppHandle, State};
use tokens::tokens::media_files::MediaFileToken;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::core::state::task_database::TaskDatabase;
#[derive(Deserialize)]
pub struct EnqueueImageTo3dObjectRequest {
@@ -69,6 +72,7 @@ pub async fn enqueue_image_to_3d_object_command(
app_env_configs: State<'_, AppEnvConfigs>,
app_data_root: State<'_, AppDataRoot>,
provider_priority_store: State<'_, ProviderPriorityStore>,
task_database: State<'_, TaskDatabase>,
fal_creds_manager: State<'_, FalCredentialManager>,
fal_task_queue: State<'_, FalTaskQueue>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
@@ -83,6 +87,7 @@ pub async fn enqueue_image_to_3d_object_command(
&app_env_configs,
&app_data_root,
&provider_priority_store,
&task_database,
&fal_creds_manager,
&storyteller_creds_manager,
&fal_task_queue,
@@ -127,7 +132,17 @@ pub async fn enqueue_image_to_3d_object_command(
error_details: None,
})
}
Ok(()) => {
Ok(event) => {
let event = GenerationEnqueueSuccessEvent {
action: event.to_frontend_event_action(),
service: event.to_frontend_event_service(),
model: event.model,
};
if let Err(err) = event.send(&app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
Ok(EnqueueImageTo3dObjectSuccessResponse {}.into())
}
}
@@ -140,39 +155,37 @@ pub async fn handle_request(
app_env_configs: &AppEnvConfigs,
app_data_root: &AppDataRoot,
provider_priority_store: &ProviderPriorityStore,
task_database: &TaskDatabase,
fal_creds_manager: &FalCredentialManager,
storyteller_creds_manager: &StorytellerCredentialManager,
fal_task_queue: &FalTaskQueue,
) -> Result<(), InternalObjectError> {
) -> Result<TaskEnqueueSuccess, InternalObjectError> {
let priority = provider_priority_store.get_priority()?;
let result = handle_object(
request,
&app,
&app_env_configs,
&app_data_root,
&provider_priority_store,
&fal_creds_manager,
&storyteller_creds_manager,
&fal_task_queue,
).await;
let success_event = match result {
Err(err) => return Err(err),
Ok(event) => event,
};
for provider in priority.iter() {
match provider {
Provider::Sora => {} // Fallthrough
Provider::Artcraft => {
return Ok(handle_object_artcraft(
request,
app,
app_env_configs,
app_data_root,
storyteller_creds_manager
).await?);
}
Provider::Fal => {
if fal_creds_manager.has_apparent_api_token()? {
return Ok(handle_object_fal(
&app,
app_env_configs,
app_data_root,
request,
fal_creds_manager,
fal_task_queue
).await?);
}
}
}
let result = success_event
.insert_into_task_database(task_database)
.await;
if let Err(err) = result {
error!("Failed to create task in database: {:?}", err);
// NB: Fail open, but find a way to flag this.
}
Err(InternalObjectError::NoProviderAvailable)
Ok(success_event)
}
@@ -0,0 +1,55 @@
use crate::core::commands::enqueue::object::enqueue_image_to_3d_object_command::EnqueueImageTo3dObjectRequest;
use crate::core::commands::enqueue::object::generic::handle_object_artcraft::handle_object_artcraft;
use crate::core::commands::enqueue::object::generic::handle_object_fal::handle_object_fal;
use crate::core::commands::enqueue::object::internal_object_error::InternalObjectError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
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::provider_priority::{Provider, ProviderPriorityStore};
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use tauri::AppHandle;
pub async fn handle_object(
request: EnqueueImageTo3dObjectRequest,
app: &AppHandle,
app_env_configs: &AppEnvConfigs,
app_data_root: &AppDataRoot,
provider_priority_store: &ProviderPriorityStore,
fal_creds_manager: &FalCredentialManager,
storyteller_creds_manager: &StorytellerCredentialManager,
fal_task_queue: &FalTaskQueue,
) -> Result<TaskEnqueueSuccess, InternalObjectError> {
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_env_configs,
app_data_root,
storyteller_creds_manager
).await?);
}
Provider::Fal => {
if fal_creds_manager.has_apparent_api_token()? {
return Ok(handle_object_fal(
&app,
app_env_configs,
app_data_root,
request,
fal_creds_manager,
fal_task_queue
).await?);
}
}
}
}
Err(InternalObjectError::NoProviderAvailable)
}
@@ -1,7 +1,8 @@
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::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
@@ -9,6 +10,8 @@ use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
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 enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
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 idempotency::uuid::generate_random_uuid;
use log::{error, info};
@@ -22,7 +25,7 @@ pub async fn handle_object_artcraft(
app_env_configs: &AppEnvConfigs,
app_data_root: &AppDataRoot,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<(), InternalObjectError> {
) -> Result<TaskEnqueueSuccess, InternalObjectError> {
let creds = match storyteller_creds_manager.get_credentials()? {
Some(creds) => creds,
@@ -39,18 +42,12 @@ pub async fn handle_object_artcraft(
},
};
//let api_key = fal_creds_manager.get_key_required()
// .map_err(|err| {
// error!("EnqueueTextToImage FAL api key required: {:?}", err);
// InternalObjectError::NeedsFalApiKey
// })?;
info!("Calling Artcraft image to 3d ...");
let uuid_idempotency_token = generate_random_uuid();
let mut used_model = None;
let job_token = match request.model {
None => {
return Err(InternalObjectError::NoModelSpecified);
@@ -60,6 +57,7 @@ pub async fn handle_object_artcraft(
EnqueueImageTo3dObjectModel::Hunyuan3d2_0
) => {
info!("enqueue Artcraft Hunyuan 3D 2.0");
used_model = Some(GenerationModel::Hunyuan3d2_0);
let request = GenerateHunyuan20ImageTo3dRequest {
uuid_idempotency_token,
media_file_token: request.image_media_token,
@@ -82,6 +80,7 @@ pub async fn handle_object_artcraft(
}
Some(EnqueueImageTo3dObjectModel::Hunyuan3d2_1) => {
info!("enqueue Artcraft Hunyuan 3D 2.1");
used_model = Some(GenerationModel::Hunyuan3d2_1);
let request = GenerateHunyuan21ImageTo3dRequest {
uuid_idempotency_token,
media_file_token: request.image_media_token,
@@ -106,41 +105,20 @@ pub async fn handle_object_artcraft(
info!("Successfully enqueued 3D generation");
let event = GenerationEnqueueSuccessEvent {
action: GenerationAction::ImageTo3d,
service: GenerationServiceProvider::Artcraft,
model: None,
};
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);
// return Err(InternalObjectError::AnyhowError(anyhow!("Failed to enqueue task: {:?}", err)));
//let event = GenerationEnqueueSuccessEvent {
// action: GenerationAction::ImageTo3d,
// service: GenerationServiceProvider::Artcraft,
// model: None,
//};
//
//if let Err(err) = event.send(app) {
// error!("Failed to emit event: {:?}", err); // Fail open.
//}
// match result {
// Ok(enqueued) => {
// }
// Err(err) => {
// error!("Failed to enqueue image to 3d: {:?}", err);
//
// let event = GenerationEnqueueFailureEvent {
// action: GenerationAction::ImageTo3d,
// service: GenerationServiceProvider::Fal,
// model: None,
// reason: None,
// };
//
// if let Err(err) = event.send(app) {
// error!("Failed to emit event: {:?}", err); // Fail open.
// }
//
// return Err(InternalObjectError::FalError(err));
// }
// }
Ok(())
Ok(TaskEnqueueSuccess {
task_type: TaskType::ObjectGeneration,
model: used_model,
provider: GenerationProvider::Artcraft,
provider_job_id: Some(job_token.to_string()),
})
}
@@ -1,7 +1,8 @@
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::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
@@ -10,6 +11,8 @@ use crate::core::utils::download_media_file_to_temp_dir::download_media_file_to_
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
use anyhow::anyhow;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
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 log::{error, info};
@@ -22,7 +25,7 @@ pub async fn handle_object_fal(
request: EnqueueImageTo3dObjectRequest,
fal_creds_manager: &FalCredentialManager,
fal_task_queue: &FalTaskQueue,
) -> Result<(), InternalObjectError> {
) -> Result<TaskEnqueueSuccess, InternalObjectError> {
let api_key = match fal_creds_manager.get_key()? {
Some(key) => key,
@@ -60,6 +63,8 @@ pub async fn handle_object_fal(
info!("Calling FAL image to 3d ...");
let filename = temp_download.path().to_path_buf();
let mut used_model = None;
let result = match request.model {
None => {
@@ -69,6 +74,7 @@ pub async fn handle_object_fal(
EnqueueImageTo3dObjectModel::Hunyuan3d2_0 |
EnqueueImageTo3dObjectModel::Hunyuan3d2
) => {
used_model = Some(GenerationModel::Hunyuan3d2_0);
info!("enqueue Hunyuan 3D 2.0");
enqueue_hunyuan2_image_to_3d(Hunyuan2Args {
image_path: filename,
@@ -80,9 +86,13 @@ pub async fn handle_object_fal(
}
};
let mut job_id = None;
match result {
Ok(enqueued) => {
info!("Successfully enqueued text to image");
job_id = Some(enqueued.request_id.to_string());
let event = GenerationEnqueueSuccessEvent {
action: GenerationAction::ImageTo3d,
@@ -117,5 +127,10 @@ pub async fn handle_object_fal(
}
}
Ok(())
Ok(TaskEnqueueSuccess {
task_type: TaskType::ObjectGeneration,
model: used_model,
provider: GenerationProvider::Fal,
provider_job_id: job_id,
})
}
@@ -0,0 +1,3 @@
pub (super) mod handle_object;
pub (super) mod handle_object_artcraft;
pub (super) mod handle_object_fal;
@@ -1,4 +1,3 @@
pub mod enqueue_image_to_3d_object_command;
pub mod handle_object_artcraft;
pub mod handle_object_fal;
pub mod internal_object_error;
pub mod internal_object_error;
mod generic;
@@ -0,0 +1,83 @@
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::state::task_database::TaskDatabase;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_model_type::TaskModelType;
use enums::tauri::tasks::task_status::TaskStatus;
use enums::tauri::tasks::task_type::TaskType;
use sqlite_tasks::error::SqliteTasksError;
use sqlite_tasks::queries::create_task::{create_task, CreateTaskArgs};
use tokens::tokens::sqlite::tasks::TaskId;
pub struct TaskEnqueueSuccess {
pub task_type: TaskType,
pub model: Option<GenerationModel>,
pub provider: GenerationProvider,
pub provider_job_id: Option<String>,
// TODO: We may want to change the `model` type - this has weird ownership and semantics
}
impl TaskEnqueueSuccess{
pub fn to_frontend_event_action(&self) -> GenerationAction {
match self.task_type {
TaskType::ImageGeneration => GenerationAction::GenerateImage,
TaskType::VideoGeneration => GenerationAction::GenerateVideo,
TaskType::BackgroundRemoval => GenerationAction::RemoveBackground,
TaskType::ObjectGeneration => GenerationAction::ImageTo3d,
}
}
pub fn to_frontend_event_service(&self) -> GenerationServiceProvider {
match self.provider {
GenerationProvider::Artcraft => GenerationServiceProvider::Artcraft,
GenerationProvider::Fal => GenerationServiceProvider::Fal,
GenerationProvider::Sora => GenerationServiceProvider::Sora,
}
}
pub async fn insert_into_task_database(&self, task_database: &TaskDatabase) -> Result<TaskId, SqliteTasksError> {
// TODO: Move this mapping elsewhere, or remove the other models.
let model_type = match self.model {
None => None,
Some(GenerationModel::Flux1Dev) => Some(TaskModelType::Flux1Dev),
Some(GenerationModel::Flux1Schnell) => Some(TaskModelType::Flux1Schnell),
Some(GenerationModel::FluxPro11) => Some(TaskModelType::FluxPro11),
Some(GenerationModel::FluxPro11Ultra) => Some(TaskModelType::FluxPro11Ultra),
Some(GenerationModel::GptImage1) => Some(TaskModelType::GptImage1),
Some(GenerationModel::Recraft3) => Some(TaskModelType::Recraft3),
Some(GenerationModel::Kling21Pro) => Some(TaskModelType::Kling21Pro),
Some(GenerationModel::Kling21Master) => Some(TaskModelType::Kling21Master),
Some(GenerationModel::Seedance10Lite) => Some(TaskModelType::Seedance10Lite),
Some(GenerationModel::Veo2) => Some(TaskModelType::Veo2),
Some(GenerationModel::Hunyuan3d2_0) => Some(TaskModelType::Hunyuan3d2_0),
Some(GenerationModel::Hunyuan3d2_1) => Some(TaskModelType::Hunyuan3d2_1),
// TODO: These seem wrong -
Some(GenerationModel::Kling1_6) => Some(TaskModelType::Kling16Pro), // NB: `VideoModel::Kling16Pro`.
Some(GenerationModel::Kling2_0) => None, // TODO: unused elsewhere?
Some(GenerationModel::Sora) => None, // TODO: unused elsewhere?
};
create_task(CreateTaskArgs {
db: task_database.get_connection(),
status: TaskStatus::Pending,
task_type: self.task_type,
model_type: model_type,
provider: self.provider,
provider_job_id: self.provider_job_id.as_deref(),
frontend_subscriber_id: None,
frontend_subscriber_payload: None,
}).await
}
// 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,
// }
// }
}
@@ -1,13 +1,16 @@
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::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::enqueue::video::generic::handle_video::handle_video;
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::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::core::model::video_models::VideoModel;
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::provider_priority::{Provider, ProviderPriorityStore};
use crate::core::state::task_database::TaskDatabase;
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
@@ -26,6 +29,9 @@ pub struct EnqueueImageToVideoRequest {
/// The model to use.
pub model: Option<VideoModel>,
/// Optional text prompt.
pub prompt: Option<String>,
}
#[derive(Serialize)]
@@ -58,6 +64,7 @@ pub async fn enqueue_image_to_video_command(
app_env_configs: State<'_, AppEnvConfigs>,
app_data_root: State<'_, AppDataRoot>,
provider_priority_store: State<'_, ProviderPriorityStore>,
task_database: State<'_, TaskDatabase>,
fal_creds_manager: State<'_, FalCredentialManager>,
fal_task_queue: State<'_, FalTaskQueue>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
@@ -72,6 +79,7 @@ pub async fn enqueue_image_to_video_command(
&app_env_configs,
&app_data_root,
&provider_priority_store,
&task_database,
&fal_creds_manager,
&storyteller_creds_manager,
&fal_task_queue,
@@ -116,7 +124,17 @@ pub async fn enqueue_image_to_video_command(
error_details: None,
})
}
Ok(()) => {
Ok(event) => {
let event = GenerationEnqueueSuccessEvent {
action: event.to_frontend_event_action(),
service: event.to_frontend_event_service(),
model: event.model,
};
if let Err(err) = event.send(&app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
Ok(EnqueueImageToVideoSuccessResponse {}.into())
}
}
@@ -129,39 +147,36 @@ pub async fn handle_request(
app_env_configs: &AppEnvConfigs,
app_data_root: &AppDataRoot,
provider_priority_store: &ProviderPriorityStore,
task_database: &TaskDatabase,
fal_creds_manager: &FalCredentialManager,
storyteller_creds_manager: &StorytellerCredentialManager,
fal_task_queue: &FalTaskQueue,
) -> Result<(), InternalVideoError> {
) -> Result<TaskEnqueueSuccess, InternalVideoError> {
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_env_configs,
app_data_root,
storyteller_creds_manager
).await?);
}
Provider::Fal => {
if fal_creds_manager.has_apparent_api_token()? {
return Ok(handle_video_fal(
&app,
app_env_configs,
app_data_root,
request,
fal_creds_manager,
fal_task_queue
).await?);
}
}
}
let result = handle_video(
request,
&app,
&app_env_configs,
&app_data_root,
&provider_priority_store,
&fal_creds_manager,
&storyteller_creds_manager,
&fal_task_queue,
).await;
let success_event = match result {
Err(err) => return Err(err),
Ok(event) => event,
};
let result = success_event
.insert_into_task_database(task_database)
.await;
if let Err(err) = result {
error!("Failed to create task in database: {:?}", err);
// NB: Fail open, but find a way to flag this.
}
Err(InternalVideoError::NoProviderAvailable)
Ok(success_event)
}
@@ -0,0 +1,55 @@
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::enqueue::video::enqueue_image_to_video_command::EnqueueImageToVideoRequest;
use crate::core::commands::enqueue::video::generic::handle_video_artcraft::handle_video_artcraft;
use crate::core::commands::enqueue::video::generic::handle_video_fal::handle_video_fal;
use crate::core::commands::enqueue::video::internal_video_error::InternalVideoError;
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::provider_priority::{Provider, ProviderPriorityStore};
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use tauri::AppHandle;
pub async fn handle_video(
request: EnqueueImageToVideoRequest,
app: &AppHandle,
app_env_configs: &AppEnvConfigs,
app_data_root: &AppDataRoot,
provider_priority_store: &ProviderPriorityStore,
fal_creds_manager: &FalCredentialManager,
storyteller_creds_manager: &StorytellerCredentialManager,
fal_task_queue: &FalTaskQueue,
) -> Result<TaskEnqueueSuccess, InternalVideoError> {
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_env_configs,
app_data_root,
storyteller_creds_manager
).await?);
}
Provider::Fal => {
if fal_creds_manager.has_apparent_api_token()? {
return Ok(handle_video_fal(
&app,
app_env_configs,
app_data_root,
request,
fal_creds_manager,
fal_task_queue
).await?);
}
}
}
}
Err(InternalVideoError::NoProviderAvailable)
}
@@ -1,3 +1,4 @@
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
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;
@@ -13,6 +14,8 @@ use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video
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 enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
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::video_gen::enqueue_kling_16_pro_image_to_video::Kling16ProAspectRatio;
use fal_client::requests::webhook::video::enqueue_veo_2_image_to_video_webhook::Veo2AspectRatio;
@@ -32,7 +35,7 @@ pub async fn handle_video_artcraft(
app_env_configs: &AppEnvConfigs,
app_data_root: &AppDataRoot,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<(), InternalVideoError> {
) -> Result<TaskEnqueueSuccess, InternalVideoError> {
let creds = match storyteller_creds_manager.get_credentials()? {
Some(creds) => creds,
@@ -74,7 +77,7 @@ pub async fn handle_video_artcraft(
uuid_idempotency_token,
media_file_token: request.image_media_token,
aspect_ratio: Some(GenerateKling16ProAspectRatio::WideSixteenNine),
prompt: None,
prompt: request.prompt,
duration: None,
};
let result = generate_kling_16_pro_image_to_video(
@@ -100,7 +103,7 @@ pub async fn handle_video_artcraft(
uuid_idempotency_token,
media_file_token: request.image_media_token,
aspect_ratio: Some(GenerateKling21MasterAspectRatio::WideSixteenNine),
prompt: None,
prompt: request.prompt,
duration: None,
};
let result = generate_kling_21_master_image_to_video(
@@ -126,7 +129,7 @@ pub async fn handle_video_artcraft(
uuid_idempotency_token,
media_file_token: request.image_media_token,
aspect_ratio: Some(GenerateKling21ProAspectRatio::WideSixteenNine),
prompt: None,
prompt: request.prompt,
duration: None,
};
let result = generate_kling_21_pro_image_to_video(
@@ -151,7 +154,7 @@ pub async fn handle_video_artcraft(
let request = GenerateSeedance10LiteImageToVideoRequest {
uuid_idempotency_token,
media_file_token: request.image_media_token,
prompt: None,
prompt: request.prompt,
resolution: None,
duration: None,
};
@@ -178,7 +181,7 @@ pub async fn handle_video_artcraft(
uuid_idempotency_token,
media_file_token: request.image_media_token,
aspect_ratio: Some(GenerateVeo2AspectRatio::WideSixteenNine),
prompt: None,
prompt: request.prompt,
duration: None,
};
let result = generate_veo_2_image_to_video(
@@ -201,41 +204,10 @@ pub async fn handle_video_artcraft(
info!("Successfully enqueued video generation");
let event = GenerationEnqueueSuccessEvent {
action: GenerationAction::GenerateVideo,
service: GenerationServiceProvider::Artcraft,
Ok(TaskEnqueueSuccess {
task_type: TaskType::VideoGeneration,
model: selected_model,
};
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);
// return Err(InternalVideoError::AnyhowError(anyhow!("Failed to enqueue task: {:?}", err)));
//}
// match result {
// Ok(enqueued) => {
// }
// Err(err) => {
// error!("Failed to enqueue image to 3d: {:?}", err);
//
// let event = GenerationEnqueueFailureEvent {
// action: GenerationAction::ImageTo3d,
// service: GenerationServiceProvider::Fal,
// model: None,
// reason: None,
// };
//
// if let Err(err) = event.send(app) {
// error!("Failed to emit event: {:?}", err); // Fail open.
// }
//
// return Err(InternalVideoError::FalError(err));
// }
// }
Ok(())
provider: GenerationProvider::Artcraft,
provider_job_id: Some(job_token.to_string()),
})
}
@@ -1,3 +1,4 @@
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
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;
@@ -11,6 +12,8 @@ use crate::core::utils::download_media_file_to_temp_dir::download_media_file_to_
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
use anyhow::anyhow;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
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::video_gen::enqueue_kling_16_pro_image_to_video::{enqueue_kling_16_pro_image_to_video, Kling16ProArgs, Kling16ProAspectRatio, Kling16ProDuration};
use log::{error, info};
@@ -23,7 +26,7 @@ pub async fn handle_video_fal(
request: EnqueueImageToVideoRequest,
fal_creds_manager: &FalCredentialManager,
fal_task_queue: &FalTaskQueue,
) -> Result<(), InternalVideoError> {
) -> Result<TaskEnqueueSuccess, InternalVideoError> {
let api_key = match fal_creds_manager.get_key()? {
Some(key) => key,
@@ -74,28 +77,22 @@ pub async fn handle_video_fal(
api_key: &api_key,
duration: Kling16ProDuration::Default,
aspect_ratio: Kling16ProAspectRatio::WideSixteenNine,
prompt: "",
prompt: request.prompt.as_deref().unwrap_or(""),
}).await
}
_ => {
return Err(InternalVideoError::AnyhowError(anyhow!("Unsupported model: {:?}", request.model)));
}
};
let mut job_id = None;
match result {
Ok(enqueued) => {
info!("Successfully enqueued Kling 1.6 image to video");
let event = GenerationEnqueueSuccessEvent {
action: GenerationAction::GenerateVideo,
service: GenerationServiceProvider::Fal,
model: selected_model,
};
if let Err(err) = event.send(app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
job_id = Some(enqueued.request_id.to_string());
if let Err(err) = fal_task_queue.insert(&enqueued) {
error!("Failed to enqueue task: {:?}", err);
return Err(InternalVideoError::AnyhowError(anyhow!("Failed to enqueue task: {:?}", err)));
@@ -119,5 +116,10 @@ pub async fn handle_video_fal(
}
}
Ok(())
Ok(TaskEnqueueSuccess {
task_type: TaskType::VideoGeneration,
model: selected_model,
provider: GenerationProvider::Fal,
provider_job_id: job_id,
})
}
@@ -0,0 +1,3 @@
pub (super) mod handle_video;
pub (super) mod handle_video_artcraft;
pub (super) mod handle_video_fal;
@@ -1,4 +1,3 @@
pub mod enqueue_image_to_video_command;
pub mod handle_video_artcraft;
pub mod handle_video_fal;
pub mod internal_video_error;
pub mod internal_video_error;
mod generic;
@@ -21,8 +21,15 @@ pub enum GenerationModel {
FluxPro11Ultra,
#[serde(rename = "gpt_image_1")]
GptImage1,
#[serde(rename = "hunyuan_3d_2_0")]
Hunyuan3d2_0,
#[serde(rename = "hunyuan_3d_2_1")]
Hunyuan3d2_1,
// TODO: Should be Kling16Pro
#[serde(rename = "kling_1.6")]
Kling1_6,
#[serde(rename = "kling_2.0")]
Kling2_0,
#[serde(rename = "kling_2.1_master")]
@@ -0,0 +1 @@
pub mod startup;
@@ -0,0 +1,80 @@
use crate::core::lifecycle::startup::tasks::bootstrap_task_database::bootstrap_task_database;
use crate::core::lifecycle::startup::tasks::load_provider_priority_state::load_provider_priority_state;
use crate::core::lifecycle::startup::tasks::set_app_log_level::set_app_log_level;
use crate::core::lifecycle::startup::tasks::initially_size_and_position_windows::initially_size_and_position_windows;
use crate::core::lifecycle::startup::tasks::spawn_discord_presence_thread::spawn_discord_presence_thread;
use crate::core::lifecycle::startup::tasks::spawn_fal_task_polling_thread::spawn_fal_task_polling_thread;
use crate::core::lifecycle::startup::tasks::spawn_main_window_thread::spawn_main_window_thread;
use crate::core::lifecycle::startup::tasks::spawn_sora_task_polling_thread::spawn_sora_task_polling_thread;
use crate::core::lifecycle::startup::tasks::spawn_storytoller_task_polling_thread::spawn_storyteller_task_polling_thread;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
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;
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 errors::AnyhowResult;
use tauri::{AppHandle, Manager};
pub async fn handle_tauri_startup(
app: AppHandle,
root: AppDataRoot,
app_env_configs: AppEnvConfigs,
storyteller_creds_manager: StorytellerCredentialManager,
sora_credential_manager: SoraCredentialManager,
sora_task_queue: SoraTaskQueue,
fal_credential_manager: FalCredentialManager,
fal_task_queue: FalTaskQueue,
) -> AnyhowResult<()> {
set_app_log_level(
&app,
&root,
)?;
let task_database =
bootstrap_task_database(&app, &root).await?;
load_provider_priority_state(
&app,
&root,
)?;
spawn_main_window_thread(
&app,
&root,
&storyteller_creds_manager,
)?;
spawn_storyteller_task_polling_thread(
&app,
&app_env_configs,
&task_database,
&storyteller_creds_manager,
)?;
spawn_sora_task_polling_thread(
&app,
&root,
&app_env_configs,
&sora_credential_manager,
&storyteller_creds_manager,
&sora_task_queue,
)?;
spawn_fal_task_polling_thread(
&app,
&root,
&app_env_configs,
&fal_credential_manager,
&fal_task_queue,
&storyteller_creds_manager,
)?;
spawn_discord_presence_thread()?;
initially_size_and_position_windows(&app, &root);
Ok(())
}
@@ -0,0 +1,2 @@
pub mod handle_tauri_startup;
mod tasks;
@@ -0,0 +1,10 @@
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::task_database::TaskDatabase;
use errors::AnyhowResult;
use tauri::{AppHandle, Manager};
pub async fn bootstrap_task_database(app: &AppHandle, root: &AppDataRoot) -> AnyhowResult<TaskDatabase> {
let task_database = TaskDatabase::connect(root).await?;
app.manage(task_database.clone());
Ok(task_database)
}
@@ -0,0 +1,42 @@
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::window::main_window_position::MainWindowPosition;
use crate::core::state::window::main_window_size::MainWindowSize;
use crate::core::utils::window::position_main_window::position_main_window;
use crate::core::utils::window::resize_main_window::resize_main_window;
use tauri::AppHandle;
pub fn initially_size_and_position_windows(
app: &AppHandle,
root: &AppDataRoot,
) {
println!("Sizing and positioning window...");
match MainWindowSize::from_filesystem_configs(&root) {
Ok(None) => {}
Ok(Some(size)) => {
println!("Resizing window to: {:?}", size);
let result = resize_main_window(app, &size);
if let Err(err) = result {
eprintln!("Could not set window size: {:?}", err);
}
}
Err(err) => {
eprintln!("Failed to read window size from disk: {:?}", err);
}
}
match MainWindowPosition::from_filesystem_configs(&root) {
Ok(None) => {}
Ok(Some(pos)) => {
println!("Moving window to: {:?}", pos);
let result = position_main_window(app, &pos);
if let Err(err) = result {
eprintln!("Could not set window position: {:?}", err);
}
}
Err(err) => {
eprintln!("Failed to read window position from disk: {:?}", err);
}
}
}
@@ -0,0 +1,34 @@
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::ProviderPriorityStore;
use anyhow::anyhow;
use errors::AnyhowResult;
use tauri::{AppHandle, Manager};
pub fn load_provider_priority_state(
app: &AppHandle,
root: &AppDataRoot,
) -> AnyhowResult<()> {
let provider_priority = match ProviderPriorityStore::from_filesystem_configs(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()
}
};
let success = app.manage(provider_priority);
if !success {
eprintln!("Failed to manage provider priority state.");
return Err(anyhow!("Failed to manage provider priority state."));
}
Ok(())
}
@@ -0,0 +1,9 @@
pub (super) mod bootstrap_task_database;
pub (super) mod set_app_log_level;
pub (super) mod initially_size_and_position_windows;
pub (super) mod spawn_discord_presence_thread;
pub (super) mod spawn_fal_task_polling_thread;
pub (super) mod spawn_main_window_thread;
pub (super) mod spawn_sora_task_polling_thread;
pub (super) mod spawn_storytoller_task_polling_thread;
pub (super) mod load_provider_priority_state;
@@ -0,0 +1,73 @@
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use errors::AnyhowResult;
use log::LevelFilter;
use std::env;
use std::env::VarError;
use tauri::AppHandle;
use tauri_plugin_log::{Target, TargetKind};
const RUST_LOG: &str = "RUST_LOG";
pub fn set_app_log_level(
app: &AppHandle,
root: &AppDataRoot,
) -> AnyhowResult<()> {
//print_env_var(RUST_LOG);
// NB: The semantics of this do not match `RUST_LOG` exactly as it's not set per-package.
let maybe_level = env::var(RUST_LOG)
.map(|value| value.trim().to_ascii_lowercase());
let mut log_level = LevelFilter::Info;
match maybe_level {
Ok(level) => {
println!("The `RUST_LOG` environment variable was set to: `{}`.", level);
if level.contains("debug") {
log_level = LevelFilter::Debug;
} else if level.contains("info") {
log_level = LevelFilter::Info;
} else if level.contains("warn") {
log_level = LevelFilter::Warn;
} else if level.contains("error") {
log_level = LevelFilter::Error;
} else if level.contains("off") {
log_level = LevelFilter::Off;
}
}
Err(VarError::NotPresent) => {
// NB: This demonstrably tests stderr output in the default case.
eprintln!("No `RUST_LOG` environment variable found.");
}
Err(VarError::NotUnicode(_)) => {
// NB: This demonstrably tests stderr output in the default case.
eprintln!("Could not parse `RUST_LOG` environment variable; not unicode.");
}
}
println!("Setting app log level to: {:?}", log_level);
app.plugin(
tauri_plugin_log::Builder::default()
.level(log_level)
.targets(vec![
Target::new(TargetKind::Stdout),
Target::new(TargetKind::LogDir {
file_name: Some(root.log_file_name_str().to_string())
}),
])
.build(),
)?;
Ok(())
}
// pub fn print_env_var(var_name: &str) {
// match env::var(var_name) {
// Ok(value) => println!("Environment variable `{}` = {}", var_name, value),
// Err(VarError::NotPresent) => println!("Environment variable `{}` is not set.", var_name),
// Err(VarError::NotUnicode(_)) => println!("Environment variable `{}` is not a valid Unicode string", var_name),
// }
// }
@@ -0,0 +1,10 @@
use crate::core::threads::discord_presence_thread::discord_presence_thread;
use errors::AnyhowResult;
pub fn spawn_discord_presence_thread(
) -> AnyhowResult<()> {
tauri::async_runtime::spawn(discord_presence_thread());
Ok(())
}
@@ -0,0 +1,29 @@
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
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;
use crate::services::fal::threads::fal_task_polling_thread::fal_task_polling_thread;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use errors::AnyhowResult;
use tauri::{AppHandle, Manager};
pub fn spawn_fal_task_polling_thread(
app: &AppHandle,
root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
fal_credential_manager: &FalCredentialManager,
fal_task_queue: &FalTaskQueue,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> AnyhowResult<()> {
tauri::async_runtime::spawn(fal_task_polling_thread(
app.clone(),
app_env_configs.clone(),
root.clone(),
fal_credential_manager.clone(),
fal_task_queue.clone(),
storyteller_creds_manager.clone(),
));
Ok(())
}
@@ -0,0 +1,20 @@
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::threads::main_window_thread::main_window_thread::main_window_thread;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use errors::AnyhowResult;
use tauri::{AppHandle, Manager};
pub fn spawn_main_window_thread(
app: &AppHandle,
root: &AppDataRoot,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> AnyhowResult<()> {
tauri::async_runtime::spawn(main_window_thread(
app.clone(),
root.clone(),
storyteller_creds_manager.clone(),
));
Ok(())
}
@@ -0,0 +1,29 @@
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::sora::threads::sora_task_polling_thread::sora_task_polling_thread;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use errors::AnyhowResult;
use tauri::{AppHandle, Manager};
pub fn spawn_sora_task_polling_thread(
app: &AppHandle,
root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
sora_credential_manager: &SoraCredentialManager,
storyteller_creds_manager: &StorytellerCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> AnyhowResult<()> {
tauri::async_runtime::spawn(sora_task_polling_thread(
app.clone(),
app_env_configs.clone(),
root.clone(),
sora_credential_manager.clone(),
storyteller_creds_manager.clone(),
sora_task_queue.clone(),
));
Ok(())
}
@@ -0,0 +1,23 @@
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::task_database::TaskDatabase;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use crate::services::storyteller::threads::storyteller_task_polling_thread::storyteller_task_polling_thread;
use errors::AnyhowResult;
use tauri::{AppHandle, Manager};
pub fn spawn_storyteller_task_polling_thread(
app: &AppHandle,
app_env_configs: &AppEnvConfigs,
task_database: &TaskDatabase,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> AnyhowResult<()> {
tauri::async_runtime::spawn(storyteller_task_polling_thread(
app.clone(),
app_env_configs.clone(),
task_database.clone(),
storyteller_creds_manager.clone(),
));
Ok(())
}
+1
View File
@@ -1,6 +1,7 @@
pub mod artcraft_error;
pub mod commands;
pub mod events;
pub mod lifecycle;
pub mod model;
pub mod state;
pub mod threads;
@@ -2,7 +2,7 @@ use crate::core::state::data_dir::app_assets_dir::AppAssetsDir;
use crate::core::state::data_dir::app_credentials_dir::AppCredentialsDir;
use crate::core::state::data_dir::app_downloads_dir::AppDownloadsDir;
use crate::core::state::data_dir::app_settings_dir::AppSettingsDir;
use crate::core::state::data_dir::app_weights_dir::AppWeightsDir;
use crate::core::state::data_dir::app_state_dir::AppStateDir;
use crate::core::state::data_dir::temporary_dir::TemporaryDir;
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use crate::core::state::expanduser::expanduser;
@@ -20,14 +20,16 @@ const LOG_FILE_NAME : &str = "application_debug";
#[derive(Clone)]
pub struct AppDataRoot {
path: PathBuf,
log_file_name: PathBuf,
log_file_name_string: String,
assets_dir: AppAssetsDir,
credentials_dir: AppCredentialsDir,
downloads_dir: AppDownloadsDir,
settings_dir: AppSettingsDir,
weights_dir: AppWeightsDir,
state_dir: AppStateDir,
temp_dir: TemporaryDir,
log_file_name: PathBuf,
log_file_name_string: String,
}
impl AppDataRoot {
@@ -65,8 +67,8 @@ impl AppDataRoot {
let credentials_dir = AppCredentialsDir::get_or_create_in_root_dir(&dir)?;
let downloads_dir = AppDownloadsDir::get_or_create_in_root_dir(&dir)?;
let settings_dir = AppSettingsDir::get_or_create_in_root_dir(&dir)?;
let state_dir = AppStateDir::get_or_create_in_root_dir(&dir)?;
let temp_dir = TemporaryDir::get_or_create_in_root_dir(&dir)?;
let weights_dir = AppWeightsDir::get_or_create_in_root_dir(&dir)?;
let log_file_name = dir.join(LOG_FILE_NAME);
let log_file_name_string = log_file_name
.to_str()
@@ -75,14 +77,14 @@ impl AppDataRoot {
Ok(Self {
path: dir,
log_file_name,
log_file_name_string,
assets_dir,
credentials_dir,
downloads_dir,
settings_dir,
weights_dir,
state_dir,
temp_dir,
log_file_name,
log_file_name_string,
})
}
@@ -101,9 +103,9 @@ impl AppDataRoot {
pub fn settings_dir(&self) -> &AppSettingsDir {
&self.settings_dir
}
pub fn weights_dir(&self) -> &AppWeightsDir {
&self.weights_dir
pub fn state_dir(&self) -> &AppStateDir {
&self.state_dir
}
pub fn temp_dir(&self) -> &TemporaryDir {
@@ -135,15 +137,15 @@ impl AppDataRoot {
}
pub fn get_window_size_config_file(&self) -> PathBuf {
self.path.join("window_size.json")
self.state_dir.get_window_size_config_file()
}
pub fn get_window_position_config_file(&self) -> PathBuf {
self.path.join("window_position.json")
self.state_dir.get_window_position_config_file()
}
}
// eg. /home/bob/artcraft, /Users/bob/artcraft, or C:\Users\Alice\artcraft
// eg. /home/bob/artcraft, /Users/alice/artcraft, or C:\Users\Taylor\artcraft
fn get_default_data_dir() -> anyhow::Result<PathBuf> {
Ok(UserDirs::new()
.ok_or_else(|| anyhow!("could not determine user home directory"))?
@@ -0,0 +1,35 @@
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use std::path::{Path, PathBuf};
#[derive(Clone)]
pub struct AppStateDir {
path: PathBuf,
}
impl DataSubdir for AppStateDir{
const DIRECTORY_NAME: &'static str = "state";
fn new_from<P: AsRef<Path>>(dir: P) -> Self {
Self {
path: dir.as_ref().to_path_buf(),
}
}
fn path(&self) -> &Path {
&self.path
}
}
impl AppStateDir {
pub fn get_tasks_sqlite_database_path(&self) -> PathBuf {
self.path.join("tasks_v2.sqlite")
}
pub fn get_window_size_config_file(&self) -> PathBuf {
self.path.join("window_size.json")
}
pub fn get_window_position_config_file(&self) -> PathBuf {
self.path.join("window_position.json")
}
}
@@ -3,6 +3,7 @@ pub mod app_credentials_dir;
pub mod app_data_root;
pub mod app_downloads_dir;
pub mod app_settings_dir;
pub mod app_state_dir;
pub mod app_weights_dir;
pub mod temporary_dir;
pub mod trait_data_subdir;
@@ -3,7 +3,7 @@ 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 provider_priority;
pub mod provider_priority;
pub mod task_database;
pub mod window;
@@ -0,0 +1,20 @@
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use errors::AnyhowResult;
use sqlite_tasks::connection::TaskDbConnection;
#[derive(Clone)]
pub struct TaskDatabase {
connection: TaskDbConnection,
}
impl TaskDatabase {
pub async fn connect(root: &AppDataRoot) -> AnyhowResult<Self> {
let path = root.state_dir().get_tasks_sqlite_database_path();
let connection = TaskDbConnection::connect_and_migrate(path).await?;
Ok(Self { connection})
}
pub fn get_connection(&self) -> &TaskDbConnection {
&self.connection
}
}
@@ -42,14 +42,6 @@ impl MainWindowPosition {
Ok(Some(pos))
}
pub fn apply_to_main_window(&self, app: &AppHandle) -> AnyhowResult<()> {
let windows = app.windows();
let window = windows.get(MAIN_WINDOW_NAME)
.ok_or_else(|| anyhow::anyhow!("Main window not found"))?;
window.set_position(PhysicalPosition::new(self.x, self.y))?;
Ok(())
}
pub fn persist_to_filesystem(&self, app_data_root: &AppDataRoot) -> AnyhowResult<()> {
let filename = app_data_root.get_window_position_config_file();
let json = serde_json::to_string(self)?;
@@ -1,12 +1,13 @@
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::utils::best_window_size_heuristic::best_window_size_heuristic;
use crate::core::windows::main_window::constants::MAIN_WINDOW_NAME;
use crate::core::utils::window::get_window_size_heuristic::get_window_size_heuristic;
use crate::core::windows::main_window::constants::{MAIN_WINDOW_MIN_HEIGHT, MAIN_WINDOW_MIN_WIDTH, MAIN_WINDOW_NAME};
use errors::AnyhowResult;
use serde_derive::{Deserialize, Serialize};
use std::fs::OpenOptions;
use std::io::Write;
use tauri::{AppHandle, Manager, PhysicalSize, Window};
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
pub struct MainWindowSize {
pub width: u32,
@@ -26,7 +27,7 @@ impl MainWindowSize {
}
pub fn from_window(window: &Window) -> AnyhowResult<Self> {
let size = best_window_size_heuristic(window)?;
let size = get_window_size_heuristic(window)?;
Ok(Self {
width: size.width,
height: size.height,
@@ -43,14 +44,6 @@ impl MainWindowSize {
Ok(Some(size))
}
pub fn apply_to_main_window(&self, app: &AppHandle) -> AnyhowResult<()> {
let windows = app.windows();
let window = windows.get(MAIN_WINDOW_NAME)
.ok_or_else(|| anyhow::anyhow!("Main window not found"))?;
window.set_size(PhysicalSize::new(self.width, self.height))?;
Ok(())
}
pub fn persist_to_filesystem(&self, app_data_root: &AppDataRoot) -> AnyhowResult<()> {
let filename = app_data_root.get_window_size_config_file();
let json = serde_json::to_string(self)?;
@@ -64,6 +57,21 @@ impl MainWindowSize {
Ok(())
}
/// Window size is not wide enough to persist or apply.
pub fn is_not_wide_enough(&self) -> bool {
self.width < MAIN_WINDOW_MIN_WIDTH
}
/// Window size is not tall enough to persist or apply.
pub fn is_not_tall_enough(&self) -> bool {
self.height < MAIN_WINDOW_MIN_HEIGHT
}
/// Window size is not big enough (tall or wide) to persist or apply.
pub fn is_not_big_enough(&self) -> bool {
self.is_not_wide_enough() || self.is_not_tall_enough()
}
pub fn to_physical_size(&self) -> PhysicalSize<u32> {
PhysicalSize::new(self.width, self.height)
}
@@ -0,0 +1,2 @@
pub mod main_window_position;
pub mod main_window_size;
@@ -1,7 +1,7 @@
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::core::state::window::main_window_position::MainWindowPosition;
use crate::core::state::window::main_window_size::MainWindowSize;
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;
@@ -1,5 +1,5 @@
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::main_window_position::MainWindowPosition;
use crate::core::state::window::main_window_position::MainWindowPosition;
use errors::AnyhowResult;
use log::info;
use memory_store::clone_slot::CloneSlot;
@@ -1,6 +1,6 @@
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::main_window_size::MainWindowSize;
use crate::core::utils::best_window_size_heuristic::best_window_size_heuristic;
use crate::core::state::window::main_window_size::MainWindowSize;
use crate::core::utils::window::get_window_size_heuristic::get_window_size_heuristic;
use errors::AnyhowResult;
use log::info;
use memory_store::clone_slot::CloneSlot;
@@ -13,7 +13,7 @@ pub async fn persist_window_resize_task(
) -> AnyhowResult<()> {
let current_window_size = MainWindowSize::from_window(window)?;
let current_physical_size = best_window_size_heuristic(window)?;
let current_physical_size = get_window_size_heuristic(window)?;
let mut save_size_to_disk = true;
@@ -22,6 +22,10 @@ pub async fn persist_window_resize_task(
save_size_to_disk = false;
}
}
if current_window_size.is_not_big_enough() {
save_size_to_disk = false;
}
if save_size_to_disk {
info!("Saving window size configs to disk...");
@@ -0,0 +1,11 @@
use crate::core::events::generation_events::common::GenerationServiceProvider;
use enums::common::generation_provider::GenerationProvider;
// TODO(bt,2025-07-15): Get rid of GenerationServiceProvider
pub fn to_generation_service_provider(provider: GenerationProvider) -> GenerationServiceProvider {
match provider {
GenerationProvider::Artcraft => GenerationServiceProvider::Artcraft,
GenerationProvider::Fal => GenerationServiceProvider::Fal,
GenerationProvider::Sora => GenerationServiceProvider::Sora,
}
}
@@ -0,0 +1,2 @@
pub mod generation_provider;
pub mod task_type;
@@ -0,0 +1,11 @@
use crate::core::events::generation_events::common::GenerationAction;
use enums::tauri::tasks::task_type::TaskType;
pub fn to_generation_action(task_type: TaskType) -> GenerationAction {
match task_type {
TaskType::ImageGeneration => GenerationAction::GenerateImage,
TaskType::VideoGeneration => GenerationAction::GenerateVideo,
TaskType::BackgroundRemoval => GenerationAction::RemoveBackground,
TaskType::ObjectGeneration => GenerationAction::ImageTo3d,
}
}
@@ -1,7 +1,7 @@
pub mod best_window_size_heuristic;
pub mod clear_all_webview_cookies;
pub mod download_media_file_to_temp_dir;
pub mod download_url_to_temp_dir;
pub mod enum_conversion;
pub mod get_url_file_extension;
pub mod image;
pub mod large_file_transfer;
@@ -9,4 +9,4 @@ pub mod object_store;
pub mod save_base64_image_to_temp_dir;
pub mod simple_http_download;
pub mod simple_http_download_to_tempfile;
pub mod webview_unsafe;
pub mod window;
@@ -1,38 +0,0 @@
use errors::AnyhowResult;
use tauri::{AppHandle, Manager};
const MAIN_WINDOW_NAME: &str = "main";
pub fn webview_unsafe_for_app(app: &AppHandle) -> AnyhowResult<()> {
let windows = app.windows();
let maybe_window = windows.get(MAIN_WINDOW_NAME);
/*if let Some(window) = maybe_window {
let webview = window.get_webview(MAIN_WINDOW_NAME);
if let Some(webview) = webview {
webview_unsafe(&webview);
}
} else {
return Err(anyhow::anyhow!("Main window not found"));
}*/
Ok(())
}
/*pub fn webview_unsafe(webview: &Webview) {
let r = webview.with_webview(|webview| {
webview.controller();
#[cfg(target_os = "macos")]
unsafe {
let view: &objc2_web_kit::WKWebView = &*webview.inner().cast();
let controller: &objc2_web_kit::WKUserContentController = &*webview.controller().cast();
let window: &objc2_app_kit::NSWindow = &*webview.ns_window().cast();
//view.setPageZoom(40.);
//controller.removeAllUserScripts();
let bg_color = objc2_app_kit::NSColor::colorWithDeviceRed_green_blue_alpha(1.0, 0.2, 0.4, 1.);
window.setBackgroundColor(Some(&bg_color));
}
});
}*/
@@ -4,18 +4,18 @@ use tauri::{PhysicalSize, Window};
// A typical height difference when dev tools are open:
// Window inner size = PhysicalSize { width: 3704, height: 732 }
// Window outer size = PhysicalSize { width: 3704, height: 1476 }
const HEIGHT_DIFFERENCE_THRESHOLD: u32 = 300;
const DEV_TOOLS_HEIGHT_DIFFERENCE_THRESHOLD: u32 = 300;
/// Get the best window size
pub fn best_window_size_heuristic(window: &Window) -> AnyhowResult<PhysicalSize<u32>> {
pub fn get_window_size_heuristic(window: &Window) -> AnyhowResult<PhysicalSize<u32>> {
// NB: Ordinarily we'd use inner size, as the outer size includes the window decorations.
// However, dev tools contract the inner size, and in the event they're open, we should
// prefer the outer size.
let inner = window.inner_size()?;
let outer = window.outer_size()?;
let height_delta = outer.height.saturating_sub(inner.height);
if height_delta > HEIGHT_DIFFERENCE_THRESHOLD {
if height_delta > DEV_TOOLS_HEIGHT_DIFFERENCE_THRESHOLD {
// Dev tools are open, use the outer size
Ok(outer)
} else {
@@ -0,0 +1,3 @@
pub mod get_window_size_heuristic;
pub mod position_main_window;
pub mod resize_main_window;
@@ -0,0 +1,65 @@
use crate::core::state::window::main_window_position::MainWindowPosition;
use crate::core::windows::main_window::constants::MAIN_WINDOW_NAME;
use errors::AnyhowResult;
use tauri::{AppHandle, Manager, PhysicalPosition};
/// Try to position the main window at startup.
/// If it doesn't meet certain placement heuristics, abandon manual placement and let the OS
/// decide where to place the app.
pub fn position_main_window(
app: &AppHandle,
position: &MainWindowPosition,
) -> AnyhowResult<()> {
// NB: This non-negative position heuristic is good for MacBook (single screen).
// Not sure how this fares on Windows, Linux, or multiscreen setups.
// On Mac, this prevents the windows from being positioned off to the top-left,
// as (0,0) is the top left directly under the top menu bar.
if position.x < 0 {
// TODO(bt,2025-07-16): just in the case of x/width, allow slight placement off
// screen as long as the app is wide enough.
return Err(anyhow::anyhow!("X position must be non-negative"));
} else if position.y < 0 {
return Err(anyhow::anyhow!("Y position must be non-negative"));
}
let windows = app.windows();
let window = windows.get(MAIN_WINDOW_NAME)
.ok_or_else(|| anyhow::anyhow!("Main window not found"))?;
if let Ok(Some(monitor)) = window.current_monitor() {
// Reports the size of the monitor, e.g. PhysicalSize { width: 4112, height: 2658 }
// This is not impacted by `scale_factor`, even if that's a multiple like "2".
// i.e., positioning the app at `monitor.size().width/2` and `monitor.size().height/2`
// will place the **top left corner of the app** in roughly the center of the monitor.
let monitor_size = monitor.size();
println!("Current monitor - size: {:?}", monitor_size);
println!("Current monitor - scale factor: {:?}", monitor.scale_factor());
println!("Current monitor - position: {:?}", monitor.position());
let width_padding = monitor_size.width.div_ceil(6);
let height_padding = monitor_size.height.div_ceil(6);
let max_width = monitor_size.width.saturating_sub(width_padding);
let max_height = monitor_size.height.saturating_sub(height_padding);
let max_width = i32::try_from(max_width)?;
let max_height = i32::try_from(max_height)?;
// Prevent the window from being positioned off the screen to the bottom, right, and bottom right.
// This works on MacBook (single screen), but should be tested on Windows, Linux, and multiscreen setups.
if position.x > max_width {
return Err(anyhow::anyhow!("X position must be less than or equal to {} (monitor width {})",
max_width, monitor_size.width));
} else if position.y > max_height {
return Err(anyhow::anyhow!("Y position must be less than or equal to {} (monitor height {})",
max_height, monitor_size.height));
}
}
window.set_position(PhysicalPosition::new(position.x, position.y))?;
Ok(())
}
@@ -0,0 +1,63 @@
use crate::core::state::window::main_window_size::MainWindowSize;
use crate::core::windows::main_window::constants::{MAIN_WINDOW_MIN_HEIGHT, MAIN_WINDOW_MIN_WIDTH, MAIN_WINDOW_NAME};
use tauri::{AppHandle, Manager, PhysicalSize};
/// Try to resize the main window at startup.
/// If it doesn't meet certain sizing heuristics, abandon manual resize and let the OS
/// decide how to size the app.
pub fn resize_main_window(
app: &AppHandle,
size: &MainWindowSize,
) -> errors::AnyhowResult<()> {
let mut resize_width = size.width;
let mut resize_height = size.height;
let mut resize_was_corrected = false;
if resize_width < MAIN_WINDOW_MIN_WIDTH {
resize_width = MAIN_WINDOW_MIN_WIDTH;
resize_was_corrected = true;
}
if resize_height < MAIN_WINDOW_MIN_HEIGHT {
resize_height = MAIN_WINDOW_MIN_HEIGHT;
resize_was_corrected = true;
}
let windows = app.windows();
let window = windows.get(MAIN_WINDOW_NAME)
.ok_or_else(|| anyhow::anyhow!("Main window not found"))?;
if let Ok(Some(monitor)) = window.current_monitor() {
let scale_factor = monitor.scale_factor();
if scale_factor > 1.0 {
let min_width = MAIN_WINDOW_MIN_WIDTH as f64 * scale_factor;
let min_height = MAIN_WINDOW_MIN_HEIGHT as f64 * scale_factor;
// Is this safe? https://users.rust-lang.org/t/convert-an-f64-to-an-i32/50991/4
let min_width = min_width as u32;
let min_height = min_height as u32;
if resize_width < min_width {
resize_width = min_width;
resize_was_corrected = true;
}
if resize_height < min_height {
resize_height = min_height;
resize_was_corrected = true;
}
}
}
if resize_was_corrected {
println!("Resizing window to: {}x{} (corrected from {}x{})",
resize_width, resize_height, size.width, size.height);
} else {
println!("Resizing window to: {}x{} (no correction needed)", resize_width, resize_height);
}
window.set_size(PhysicalSize::new(resize_width, resize_height))?;
Ok(())
}
@@ -2,3 +2,11 @@
/// The name of the main application window.
/// This is the Artcraft application UX itself.
pub const MAIN_WINDOW_NAME : &str = "main";
// TODO: handle pixel density, eg. retina displays
/// This is the minimum width to resize the main window in an automated fashion
pub const MAIN_WINDOW_MIN_WIDTH : u32 = 800;
// TODO: handle pixel density, eg. retina displays
/// This is the minimum height to resize the main window in an automated fashion
pub const MAIN_WINDOW_MIN_HEIGHT : u32 = 600;
+25 -78
View File
@@ -15,15 +15,13 @@ 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::lifecycle::startup::handle_tauri_startup::handle_tauri_startup;
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;
use crate::services::fal::commands::fal_background_removal_command::fal_background_removal_command;
use crate::services::fal::commands::fal_hunyuan_image_to_3d_command::fal_hunyuan_image_to_3d_command;
use crate::services::fal::commands::fal_kling_image_to_video_command::fal_kling_image_to_video_command;
@@ -41,6 +39,7 @@ use crate::services::sora::state::sora_credential_manager::SoraCredentialManager
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::sora::threads::sora_task_polling_thread::sora_task_polling_thread;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use log::error;
use tauri_plugin_dialog;
use tauri_plugin_http;
@@ -85,36 +84,12 @@ pub fn run() {
let app_env_configs_2 = app_env_configs.clone();
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...");
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_upload::init())
.plugin(tauri_plugin_log::Builder::new()
.level(log::LevelFilter::Info)
.targets(vec![
Target::new(TargetKind::Stdout),
Target::new(TargetKind::LogDir {
file_name: Some(app_data_root.log_file_name_str().to_string())
}),
])
.build())
.setup(move |app| {
// TODO(bt): This is broken on windows
// log_environment_details();
@@ -127,59 +102,32 @@ pub fn run() {
// )?;
//}
let app = app.handle().clone();
let result = webview_unsafe_for_app(&app);
if let Err(err) = result {
eprintln!("Error setting webview unsafe: {:?}", err);
}
let handle = app.clone();
let root = app_data_root_2.clone();
let env_config = app_env_configs_2.clone();
let storyteller_creds = storyteller_creds_manager_2.clone();
let sora_creds = sora_creds_manager_2.clone();
let sora_tasks = sora_task_queue_2.clone();
let fal_creds = fal_creds_manager_2.clone();
let fal_tasks = fal_task_queue_2.clone();
// TODO(bt): Clean this up. We can just clone at the callsite. Also clean initialization
let app_2 = app.clone();
let app_3 = app.clone();
let app_4 = app.clone();
let app_5 = app.clone();
let app_env_configs_3 = app_env_configs_2.clone();
let app_data_root_3 = app_data_root_2.clone();
let app_data_root_4 = app_data_root_2.clone();
let app_data_root_5 = app_data_root_2.clone();
let sora_creds_manager_3 = sora_creds_manager_2.clone();
let storyteller_creds_manager_3 = storyteller_creds_manager.clone();
let storyteller_creds_manager_4 = storyteller_creds_manager.clone();
let fal_creds_manager_3 = fal_creds_manager_2.clone();
let fal_task_queue_3 = fal_task_queue_2.clone();
tauri::async_runtime::block_on(async move {
let result = handle_tauri_startup(
handle,
root,
env_config,
storyteller_creds,
sora_creds,
sora_tasks,
fal_creds,
fal_tasks,
).await;
match MainWindowSize::from_filesystem_configs(&app_data_root_3) {
Ok(None) => {}
Ok(Some(size)) => {
println!("Resizing window to: {:?}", size);
let result = size.apply_to_main_window(&app);
if let Err(err) = result {
eprintln!("Could not set window size: {:?}", err);
}
if let Err(err) = result {
error!("Failed to handle Tauri startup: {:?}", err);
panic!("Failed to handle Tauri startup: {:?}", err);
}
Err(err) => {
eprintln!("Failed to read window size from disk: {:?}", err);
}
}
match MainWindowPosition::from_filesystem_configs(&app_data_root_3) {
Ok(None) => {}
Ok(Some(pos)) => {
println!("Moving window to: {:?}", pos);
let result = pos.apply_to_main_window(&app);
if let Err(err) = result {
eprintln!("Could not set window position: {:?}", err);
}
}
Err(err) => {
eprintln!("Failed to read window position from disk: {:?}", err);
}
}
tauri::async_runtime::spawn(main_window_thread(app_3, app_data_root_3, storyteller_creds_manager_2));
tauri::async_runtime::spawn(sora_task_polling_thread(app_4, app_env_configs_2, app_data_root_4, sora_creds_manager_3, storyteller_creds_manager_3, sora_task_queue_2));
tauri::async_runtime::spawn(discord_presence_thread());
tauri::async_runtime::spawn(fal_task_polling_thread(app_5, app_env_configs_3, app_data_root_5, fal_creds_manager_3, fal_task_queue_3, storyteller_creds_manager_4));
});
Ok(())
})
@@ -188,7 +136,6 @@ pub fn run() {
.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)
@@ -145,10 +145,12 @@ pub async fn sora_image_remix_command(
status = CommandErrorStatus::BadRequest;
error_message = "Your Sora username is not yet created. Please visit the Sora.com website to create it first.";
},
InnerError::SoraError(SoraError::BadGateway(_) | SoraError::CloudFlareTimeout(_)) => {
error_type = SoraImageRemixErrorType::SoraIsHavingProblems;
status = CommandErrorStatus::ServerError;
error_message = "Sora is having problems. Please try again later.";
InnerError::SoraError(err) => {
if err.is_sora_having_downtime_issues() {
error_type = SoraImageRemixErrorType::SoraIsHavingProblems;
status = CommandErrorStatus::ServerError;
error_message = "Sora is having problems. Please try again later.";
}
}
_ => {},
}

Some files were not shown because too many files have changed in this diff Show More