mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-26 03:39:28 -04:00
Compare commits
66
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50a1d3801f | ||
|
|
0d67b81f5f | ||
|
|
24addb0a49 | ||
|
|
041cfc5460 | ||
|
|
693b8c36be | ||
|
|
8c2e37cad7 | ||
|
|
5a30046efa | ||
|
|
2d9773100e | ||
|
|
1ad16c1d87 | ||
|
|
a531448611 | ||
|
|
dba5b095c4 | ||
|
|
d4323da8de | ||
|
|
7eb2a3cdba | ||
|
|
708930dfb4 | ||
|
|
4f0916edb7 | ||
|
|
c36e88d8ce | ||
|
|
233f60acf6 | ||
|
|
a3e16e491a | ||
|
|
3d521fec79 | ||
|
|
6c8979123f | ||
|
|
8d4edcfafd | ||
|
|
b53e138dab | ||
|
|
dc1b038ffa | ||
|
|
418a9f92c0 | ||
|
|
d671f7f951 | ||
|
|
e97e5c239e | ||
|
|
a7270f4685 | ||
|
|
a6fb661fbe | ||
|
|
e5f5a06b6b | ||
|
|
48a6b1a046 | ||
|
|
7791b9b6cc | ||
|
|
ff30213af8 | ||
|
|
766f726dc1 | ||
|
|
76d7d4c100 | ||
|
|
2902db074e | ||
|
|
b8ee03f1b7 | ||
|
|
2cf300588e | ||
|
|
d2720d0a62 | ||
|
|
69ed3ec48b | ||
|
|
81f5797e2f | ||
|
|
a80c69ab1e | ||
|
|
da859fc2f8 | ||
|
|
5f832d5d17 | ||
|
|
612e58483c | ||
|
|
11b07b5833 | ||
|
|
a3f3bd8b3f | ||
|
|
a93e921b51 | ||
|
|
58d5a172c4 | ||
|
|
012b7a9567 | ||
|
|
18f4411f2a | ||
|
|
0ed4d58387 | ||
|
|
9782bd33bc | ||
|
|
2c9be7c262 | ||
|
|
a1f38b3695 | ||
|
|
d5566c3466 | ||
|
|
27d2ae47b3 | ||
|
|
926076441b | ||
|
|
64353f1be9 | ||
|
|
a8bb7f2720 | ||
|
|
ec1257ccf6 | ||
|
|
6038fa4fd3 | ||
|
|
8ae6867e05 | ||
|
|
9bc24ac216 | ||
|
|
590c2fbf96 | ||
|
|
212283e8d2 | ||
|
|
40d22b8ad0 |
@@ -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 }}
|
||||
@@ -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
@@ -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/
|
||||
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO generic_inference_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n job_type = ?,\n\n maybe_external_third_party = ?,\n maybe_external_third_party_id = ?,\n\n product_category = ?,\n inference_category = ?,\n\n maybe_model_type = NULL,\n maybe_model_token = NULL,\n\n maybe_input_source_token = NULL,\n maybe_input_source_token_type = NULL,\n\n maybe_download_url = NULL,\n maybe_cover_image_media_file_token = NULL,\n\n maybe_raw_inference_text = NULL,\n\n maybe_inference_args = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n priority_level = 0,\n is_keepalive_required = FALSE,\n max_duration_seconds = 0,\n\n is_debug_request = FALSE,\n maybe_routing_tag = NULL,\n\n status = ?\n ",
|
||||
"query": "\nINSERT INTO generic_inference_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n job_type = ?,\n\n maybe_external_third_party = ?,\n maybe_external_third_party_id = ?,\n\n product_category = ?,\n inference_category = ?,\n\n maybe_model_type = NULL,\n maybe_model_token = NULL,\n\n maybe_input_source_token = NULL,\n maybe_input_source_token_type = NULL,\n\n maybe_download_url = NULL,\n maybe_cover_image_media_file_token = NULL,\n \n maybe_prompt_token = ?,\n\n maybe_raw_inference_text = NULL,\n\n maybe_inference_args = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n priority_level = 0,\n is_keepalive_required = FALSE,\n max_duration_seconds = 0,\n\n is_debug_request = FALSE,\n maybe_routing_tag = NULL,\n\n status = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 13
|
||||
"Right": 14
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0fa824822cd94024dce21243f41e69e4a3e8685c4de676834a7c0ee14982c976"
|
||||
"hash": "002cdb3d086f3b4181e2997befa54bd8516cb1d3d3bb6e389862695318d5c299"
|
||||
}
|
||||
+12
@@ -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_caller,\n frontend_subscriber_id,\n frontend_subscriber_payload\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 9
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "01a0bf33e8331c19dc73f064c54eaa863bec158ef7713450a06c75e246f5f08c"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO prompts\nSET\n token = ?,\n prompt_type = ?,\n\n maybe_creator_user_token = ?,\n \n maybe_model_type = ?,\n maybe_generation_provider = ?,\n\n maybe_positive_prompt = ?,\n maybe_negative_prompt = ?,\n \n maybe_other_args = ?,\n\n creator_ip_address = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 9
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "08f52f15acf6afc7091a525e3dc88f706c71c3579e5e81aa9903807d85f469ef"
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n pci.media_token as `media_token: tokens::tokens::media_files::MediaFileToken`,\n pci.context_semantic_type as `context_semantic_type: enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType`,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension\n\nFROM prompt_context_items pci\nJOIN media_files m\nON\n pci.media_token = m.token\nWHERE\n pci.prompt_token = ?\nORDER BY pci.id ASC\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "media_token: tokens::tokens::media_files::MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "context_semantic_type: enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "public_bucket_directory_hash",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "maybe_public_bucket_prefix",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "maybe_public_bucket_extension",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "094eb099cde85835db0662c2700c6b82644b51bf9b3609391ed4d2945fe5e98f"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO prompts\nSET\n token = ?,\n prompt_type = ?,\n\n maybe_creator_user_token = ?,\n\n maybe_positive_prompt = ?,\n maybe_negative_prompt = ?,\n maybe_other_args = ?,\n\n creator_ip_address = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 7
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "13ca06bcbb4a576f8a2229a8554b7488d379baa381d8d5cb60972750e81b2173"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE generic_inference_jobs\nSET\n status = 'cancelled_by_system',\n retry_at = NULL,\n internal_debugging_failure_reason = 'reap_stale_fakeyou_jobs'\nWHERE\n job_type != 'fal_queue'\n AND status IN (\n 'pending',\n 'started',\n 'attempt_failed'\n )\n AND created_at > NOW() - INTERVAL 30 MINUTE\nLIMIT 5000\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "19708db99f2b3c6283d2115a511e0eb1c99fab396ad0effd9eb2dd79926e9c93"
|
||||
}
|
||||
+29
-7
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n p.token as `token: tokens::tokens::prompts::PromptToken`,\n\n p.prompt_type as `prompt_type: enums::by_table::prompts::prompt_type::PromptType`,\n\n p.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n\n p.maybe_positive_prompt,\n p.maybe_negative_prompt,\n p.maybe_other_args,\n\n p.creator_ip_address,\n p.created_at\n\nFROM prompts as p\nWHERE\n p.token = ?\n ",
|
||||
"query": "\nSELECT\n p.token as `token: tokens::tokens::prompts::PromptToken`,\n\n p.prompt_type as `prompt_type: enums::by_table::prompts::prompt_type::PromptType`,\n\n p.maybe_model_type as `maybe_model_type: enums::common::model_type::ModelType`,\n p.maybe_generation_provider as `maybe_generation_provider: enums::common::generation_provider::GenerationProvider`,\n\n p.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n\n p.maybe_positive_prompt,\n p.maybe_negative_prompt,\n p.maybe_other_args,\n\n p.creator_ip_address,\n p.created_at\n\nFROM prompts as p\nWHERE\n p.token = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -25,6 +25,26 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "maybe_model_type: enums::common::model_type::ModelType",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "maybe_generation_provider: enums::common::generation_provider::GenerationProvider",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -34,7 +54,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"ordinal": 5,
|
||||
"name": "maybe_positive_prompt",
|
||||
"type_info": {
|
||||
"type": "Blob",
|
||||
@@ -44,7 +64,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"ordinal": 6,
|
||||
"name": "maybe_negative_prompt",
|
||||
"type_info": {
|
||||
"type": "Blob",
|
||||
@@ -54,7 +74,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"ordinal": 7,
|
||||
"name": "maybe_other_args",
|
||||
"type_info": {
|
||||
"type": "Blob",
|
||||
@@ -64,7 +84,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"ordinal": 8,
|
||||
"name": "creator_ip_address",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -74,7 +94,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"ordinal": 9,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
@@ -94,9 +114,11 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "61744bbf83f7cf00348bcc21e868d9c506fec46da4a33ac50d63c6d9e00c0cec"
|
||||
"hash": "b46b0d022790f4e10f6e47126df0fdc55172bf8cda77d4e0b2afac9e6ebd1630"
|
||||
}
|
||||
+12
@@ -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"
|
||||
}
|
||||
+14
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n\n jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,\n \n jobs.maybe_external_third_party as `external_third_party: enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty`,\n jobs.maybe_external_third_party_id as `external_third_party_id`,\n\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n\n jobs.created_at,\n jobs.updated_at\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\nAND jobs.maybe_external_third_party_id = ?\n ",
|
||||
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n\n jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,\n \n jobs.maybe_external_third_party as `external_third_party: enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty`,\n jobs.maybe_external_third_party_id as `external_third_party_id`,\n\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n \n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n\n jobs.created_at,\n jobs.updated_at\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\nAND jobs.maybe_external_third_party_id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -75,6 +75,16 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "maybe_prompt_token: tokens::tokens::prompts::PromptToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
@@ -84,7 +94,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"ordinal": 9,
|
||||
"name": "updated_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
@@ -105,9 +115,10 @@
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "64dfacc1f0552d539f009fea91e902a2fe296c06ad539808596bbf2c8fc9eaa0"
|
||||
"hash": "fbe482c8987231c3b5d75f2e792d0fdd033de8a7eb821428fa034752c73edfb7"
|
||||
}
|
||||
Generated
+53
-54
@@ -656,12 +656,14 @@ dependencies = [
|
||||
"directories",
|
||||
"dirs",
|
||||
"discord-rich-presence",
|
||||
"enums",
|
||||
"errors",
|
||||
"fal_client",
|
||||
"filesys",
|
||||
"futures 0.3.31",
|
||||
"idempotency",
|
||||
"image",
|
||||
"images",
|
||||
"indexmap 2.9.0",
|
||||
"log",
|
||||
"memory_store",
|
||||
@@ -675,6 +677,7 @@ dependencies = [
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"serial_test",
|
||||
"sqlite_tasks",
|
||||
"storyteller_client",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
@@ -696,11 +699,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 +990,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 +1919,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 +2287,7 @@ dependencies = [
|
||||
"autocfg",
|
||||
"cfg-if 1.0.0",
|
||||
"crossbeam-utils 0.8.20",
|
||||
"memoffset 0.9.0",
|
||||
"memoffset",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
@@ -3667,7 +3669,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 +5990,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 +6372,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 +6382,7 @@ dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg_aliases 0.1.1",
|
||||
"libc",
|
||||
"memoffset 0.9.0",
|
||||
"memoffset",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6869,6 +6849,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"cloudflare_errors",
|
||||
"cookie 0.18.1",
|
||||
"errors",
|
||||
"idempotency",
|
||||
@@ -9645,11 +9626,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 +9661,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 +9673,6 @@ dependencies = [
|
||||
"chrono",
|
||||
"crc",
|
||||
"crossbeam-queue",
|
||||
"dotenvy",
|
||||
"either",
|
||||
"event-listener 2.5.3",
|
||||
"futures-channel",
|
||||
@@ -9706,9 +9706,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 +9719,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 +9745,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 +9789,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 +9819,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 +9830,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 +10000,7 @@ dependencies = [
|
||||
"artcraft_api_defs",
|
||||
"backoff",
|
||||
"chrono",
|
||||
"cloudflare_errors",
|
||||
"cookie 0.18.1",
|
||||
"enums",
|
||||
"enums_public",
|
||||
@@ -11589,7 +11588,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 +13173,7 @@ dependencies = [
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"hex",
|
||||
"nix 0.28.0",
|
||||
"nix",
|
||||
"ordered-stream",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
|
||||
@@ -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,56 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- Migration history:
|
||||
-- tasks_v1.sqlite - initial version
|
||||
-- tasks_v2.sqlite - added model_type (nullable)
|
||||
|
||||
CREATE TABLE tasks (
|
||||
-- Task auto-incrementing primary key.
|
||||
-- We don't use this.
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
|
||||
-- TaskStatus enum
|
||||
-- This is how the job system manages job states.
|
||||
-- e.g. pending, started, complete_success, dead, etc.
|
||||
task_status TEXT NOT NULL,
|
||||
|
||||
-- TaskType enum
|
||||
-- e.g. image_generation, video_generation, etc.
|
||||
task_type TEXT NOT NULL,
|
||||
|
||||
-- TaskModelType enum
|
||||
-- e.g. flux_1_dev, veo_2, etc.
|
||||
model_type TEXT,
|
||||
|
||||
-- GenerationProvider enum
|
||||
-- Together with `provider_job_id`, this creates a foreign key lookup to the job.
|
||||
-- e.g. 'artcraft', 'fal', 'sora', etc.
|
||||
provider TEXT,
|
||||
|
||||
-- The primary key for the job in the provider's system.
|
||||
provider_job_id TEXT,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- Tell the job system which caller initiated the task.
|
||||
-- e.g. 'canvas'
|
||||
frontend_caller TEXT,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- An arbitrary opaque identifier that frontend can set.
|
||||
frontend_subscriber_id TEXT,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- An opaque JSON payload set by the frontend subscriber that
|
||||
-- will be re-emitted back to the frontend.
|
||||
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);
|
||||
@@ -0,0 +1,5 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE prompts DROP COLUMN maybe_model_type;
|
||||
@@ -0,0 +1,6 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE prompts
|
||||
ADD COLUMN maybe_model_type VARCHAR(32) DEFAULT NULL;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE prompts DROP COLUMN maybe_generation_provider;
|
||||
@@ -0,0 +1,6 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE prompts
|
||||
ADD COLUMN maybe_generation_provider VARCHAR(16) DEFAULT NULL;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE prompts DROP COLUMN maybe_anonymous_visitor_token;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE prompts
|
||||
ADD COLUMN maybe_anonymous_visitor_token VARCHAR(32) DEFAULT NULL;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
DROP TABLE prompt_context_items;
|
||||
@@ -0,0 +1,30 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
-- An n:m join table between prompts and media_files.
|
||||
-- This associates mostly image media files with prompts, though we could conceivably
|
||||
-- also store 3D sets, etc.
|
||||
CREATE TABLE prompt_context_items (
|
||||
-- Not used for anything except replication.
|
||||
id BIGINT(20) NOT NULL AUTO_INCREMENT,
|
||||
|
||||
-- The semantic type of context.
|
||||
context_semantic_type VARCHAR(16) NOT NULL,
|
||||
|
||||
-- Foreign key, many-to-many
|
||||
-- There is no uniqueness constraint for (prompt_token, media_token) because a user
|
||||
-- might attach many duplicate images to the same prompt.
|
||||
prompt_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- Foreign key to the media token
|
||||
-- There is no uniqueness constraint for (prompt_token, media_token) because a user
|
||||
-- might attach many duplicate images to the same prompt.
|
||||
media_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- INDICES --
|
||||
PRIMARY KEY (id),
|
||||
KEY index_prompt_token (prompt_token),
|
||||
KEY index_media_token (media_token)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE generic_inference_jobs DROP COLUMN maybe_prompt_token;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE generic_inference_jobs
|
||||
ADD COLUMN maybe_prompt_token VARCHAR(32) DEFAULT NULL;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE media_files DROP COLUMN maybe_generation_provider;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE media_files
|
||||
ADD COLUMN maybe_generation_provider VARCHAR(16) DEFAULT NULL;
|
||||
@@ -21,6 +21,8 @@ CREATE TABLE generic_inference_jobs (
|
||||
-- The new enum for the type of job, which will eventually replace `inference_category` and `maybe_model_type`.
|
||||
-- This isn't fully supported yet in the inference-job, but we'll start populating it and adding logic around it.
|
||||
-- We'll start this out as nullable, then migrate the old rows with a default column value.
|
||||
--
|
||||
-- Fal jobs have the type `fal_queue`.
|
||||
job_type VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- There is an index on this column.
|
||||
@@ -103,6 +105,11 @@ CREATE TABLE generic_inference_jobs (
|
||||
-- And furthermore, not all download URLs have a cover image.
|
||||
maybe_cover_image_media_file_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- If we pre-constructed a prompt, this will be copied to the resultant media generated.
|
||||
-- This is especially important for Fal-related jobs and endpoints. We need this for keeping
|
||||
-- track of all the prompt details, context images, etc.
|
||||
maybe_prompt_token VARCHAR(30) DEFAULT NULL,
|
||||
|
||||
-- A migration flag we can control from the backend API service which will tell
|
||||
-- the worker where to store the results. If false, GCP. If true, AWS.
|
||||
store_in_aws BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
|
||||
@@ -61,6 +61,9 @@ CREATE TABLE media_files (
|
||||
-- * 'workflow' (DEPRECATED)
|
||||
origin_product_category VARCHAR(16) NOT NULL DEFAULT "unknown",
|
||||
|
||||
-- What service provided the generation (if any).
|
||||
maybe_generation_provider VARCHAR(16) DEFAULT NULL,
|
||||
|
||||
-- For inference that can be tied back to a model, the type of model.
|
||||
-- There are multiple indices on this column!
|
||||
-- DO NOT EXPOSE THIS TO USERS VIA THE API, as we may leak secrets in doing so.
|
||||
|
||||
+2
-4
@@ -15,9 +15,9 @@ CREATE TABLE batch_generations (
|
||||
|
||||
-- The type of record this is attached to
|
||||
-- Currently supported values:
|
||||
-- * 'model_weight'
|
||||
-- * 'model_weight' (deprecated)
|
||||
-- * 'comment' (deprecated)
|
||||
-- * 'media_file'
|
||||
-- * 'comment'
|
||||
entity_type VARCHAR(32) NOT NULL,
|
||||
|
||||
-- The token for the record.
|
||||
@@ -37,6 +37,4 @@ CREATE TABLE batch_generations (
|
||||
-- NB: Realistically, we probably don't need this index until we have a lot of entity types, and the entity_token
|
||||
-- entropy is probably sufficient enough as-is.
|
||||
-- KEY index_entity_type_and_token (entity_type, entity_token),
|
||||
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
@@ -0,0 +1,30 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
-- An n:m join table between prompts and media_files.
|
||||
-- This associates mostly image media files with prompts, though we could conceivably
|
||||
-- also store 3D sets, etc.
|
||||
CREATE TABLE prompt_context_items (
|
||||
-- Not used for anything except replication.
|
||||
id BIGINT(20) NOT NULL AUTO_INCREMENT,
|
||||
|
||||
-- Foreign key, many-to-many
|
||||
-- There is no uniqueness constraint for (prompt_token, media_token) because a user
|
||||
-- might attach many duplicate images to the same prompt.
|
||||
prompt_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- Foreign key to the media token
|
||||
-- There is no uniqueness constraint for (prompt_token, media_token) because a user
|
||||
-- might attach many duplicate images to the same prompt.
|
||||
media_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- The semantic type of context.
|
||||
context_semantic_type VARCHAR(16) NOT NULL,
|
||||
|
||||
-- INDICES --
|
||||
PRIMARY KEY (id),
|
||||
KEY index_prompt_token (prompt_token),
|
||||
KEY index_media_token (media_token)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
+15
-3
@@ -19,13 +19,25 @@ CREATE TABLE prompts (
|
||||
-- Original product to which the prompt applied.
|
||||
-- Prompts may or may not be compatible across products.
|
||||
-- Possible values:
|
||||
-- * 'stable_diffusion'
|
||||
-- * 'comfy_ui'
|
||||
-- * 'artcraft_app'
|
||||
-- * 'stable_diffusion' (legacy)
|
||||
-- * 'comfy_ui' (legacy)
|
||||
prompt_type VARCHAR(16) NOT NULL,
|
||||
|
||||
-- Whomever created the prompt
|
||||
-- What type of model was used to generate the result.
|
||||
-- NB: We're conflating prompts with outputs, but in a
|
||||
-- sense the model is inextricably part of the prompt.
|
||||
maybe_model_type VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- What service provided the generation.
|
||||
maybe_generation_provider VARCHAR(16) NOT NULL,
|
||||
|
||||
-- Whoever created the prompt
|
||||
maybe_creator_user_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- Whoever created the prompt (anonymous user tracking)
|
||||
maybe_anonymous_visitor_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- ========== PROMPT DETAILS ==========
|
||||
|
||||
-- The positive prompt (optional)
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use fal::prelude::fal_ai::flux_pro::kontext::max::{max, FluxProKontextMaxInput};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct FluxProKontextMaxArgs<'a, U: IntoUrl, R: IntoUrl> {
|
||||
// Request
|
||||
pub prompt: &'a str,
|
||||
pub image_url: U,
|
||||
pub num_images: FluxProKontextMaxNumImages,
|
||||
|
||||
// Fulfillment
|
||||
pub webhook_url: R,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum FluxProKontextMaxNumImages {
|
||||
One, // Default
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
pub async fn enqueue_flux_pro_kontext_max_edit_webhook<U: IntoUrl, R: IntoUrl>(
|
||||
args: FluxProKontextMaxArgs<'_, U, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
let num_images = match args.num_images {
|
||||
FluxProKontextMaxNumImages::One => 1,
|
||||
FluxProKontextMaxNumImages::Two => 2,
|
||||
FluxProKontextMaxNumImages::Three => 3,
|
||||
FluxProKontextMaxNumImages::Four => 4,
|
||||
};
|
||||
|
||||
let request = FluxProKontextMaxInput {
|
||||
prompt: args.prompt.to_string(),
|
||||
image_url: args.image_url.as_str().to_string(),
|
||||
num_images: Some(num_images),
|
||||
|
||||
// Maybe expose
|
||||
aspect_ratio: None,
|
||||
safety_tolerance: Some("5".to_string()), // NB: 5 is most tolerant
|
||||
output_format: Some("png".to_string()), // png or jpeg
|
||||
seed: None,
|
||||
|
||||
// Constants
|
||||
sync_mode: None, // Synchronous / slow
|
||||
};
|
||||
|
||||
let result = max(request)
|
||||
.with_api_key(&args.api_key.0)
|
||||
.queue_webhook(args.webhook_url)
|
||||
.await;
|
||||
|
||||
result.map_err(|err| classify_fal_error(err))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::requests::webhook::image::edit::enqueue_flux_pro_kontext_max_edit_webhook::{enqueue_flux_pro_kontext_max_edit_webhook, FluxProKontextMaxArgs, FluxProKontextMaxNumImages};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use test_data::web::image_urls::TALL_MOCHI_WITH_GLASSES_IMAGE_URL;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test() -> AnyhowResult<()> {
|
||||
// XXX: Don't commit secrets!
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = FluxProKontextMaxArgs {
|
||||
image_url: TALL_MOCHI_WITH_GLASSES_IMAGE_URL,
|
||||
prompt: "turn the glasses into sunglasses, make them sleek sunglasses with black rims, square shaped",
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
num_images: FluxProKontextMaxNumImages::One,
|
||||
};
|
||||
|
||||
let result = enqueue_flux_pro_kontext_max_edit_webhook(args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod enqueue_flux_pro_kontext_max_edit_webhook;
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::creds::open_ai_api_key::OpenAiApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use fal::prelude::fal_ai::gpt_image_1::edit_image::byok::{gpt_text_to_image, GptTextToImageRequest};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct GptTextToImageByokArgs<'a, V: IntoUrl> {
|
||||
// Request
|
||||
pub prompt: &'a str,
|
||||
pub image_size: GptTextToImageSize,
|
||||
pub num_images: GptTextToImageNumImages,
|
||||
pub quality: GptTextToImageQuality,
|
||||
|
||||
// Fulfillment
|
||||
pub api_key: &'a FalApiKey,
|
||||
pub openai_api_key: &'a OpenAiApiKey,
|
||||
pub webhook_url: V,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptTextToImageSize{
|
||||
Auto,
|
||||
Square,
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptTextToImageQuality {
|
||||
Auto,
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptTextToImageNumImages{
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
pub async fn enqueue_gpt_image_1_text_to_image_webhook<V: IntoUrl>(
|
||||
args: GptTextToImageByokArgs<'_, V>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
// auto, 1024x1024, 1536x1024, 1024x1536
|
||||
let image_size = match args.image_size {
|
||||
GptTextToImageSize::Auto => "auto",
|
||||
GptTextToImageSize::Square => "1024x1024",
|
||||
GptTextToImageSize::Horizontal => "1536x1024",
|
||||
GptTextToImageSize::Vertical => "1024x1536",
|
||||
};
|
||||
|
||||
let quality = match args.quality {
|
||||
GptTextToImageQuality::Auto => "auto",
|
||||
GptTextToImageQuality::Low => "low",
|
||||
GptTextToImageQuality::Medium => "medium",
|
||||
GptTextToImageQuality::High => "high",
|
||||
};
|
||||
|
||||
let num_images = match args.num_images {
|
||||
GptTextToImageNumImages::One => 1,
|
||||
GptTextToImageNumImages::Two => 2,
|
||||
GptTextToImageNumImages::Three => 3,
|
||||
GptTextToImageNumImages::Four => 4,
|
||||
};
|
||||
|
||||
let request = GptTextToImageRequest {
|
||||
prompt: args.prompt.to_string(),
|
||||
image_size: image_size.to_string(),
|
||||
num_images,
|
||||
quality: quality.to_string(),
|
||||
openai_api_key: args.openai_api_key.0.to_string(),
|
||||
};
|
||||
|
||||
let result = gpt_text_to_image(request)
|
||||
.with_api_key(&args.api_key.0)
|
||||
.queue_webhook(args.webhook_url)
|
||||
.await;
|
||||
|
||||
result.map_err(|err| classify_fal_error(err))
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::creds::open_ai_api_key::OpenAiApiKey;
|
||||
use crate::requests::webhook::image::enqueue_gpt_image_1_text_to_image_webhook::{enqueue_gpt_image_1_text_to_image_webhook, GptTextToImageByokArgs, GptTextToImageNumImages, GptTextToImageQuality, GptTextToImageSize};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_gpt_image_1_edit_image() -> AnyhowResult<()> {
|
||||
// XXX: Don't commit secrets!
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
|
||||
let fal_api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/openai_api_key.txt")?;
|
||||
|
||||
let open_ai_api_key = OpenAiApiKey::from_str(&secret);
|
||||
|
||||
let args = GptTextToImageByokArgs {
|
||||
prompt: "put the man and the ghost on the grassy hill. the man is scared of the friendly ghost.",
|
||||
api_key: &fal_api_key,
|
||||
openai_api_key: &open_ai_api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
image_size: GptTextToImageSize::Horizontal,
|
||||
num_images: GptTextToImageNumImages::One,
|
||||
quality: GptTextToImageQuality::High,
|
||||
};
|
||||
|
||||
let result = enqueue_gpt_image_1_text_to_image_webhook(args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use fal::prelude::fal_ai::flux_pro::v1::fill::fill;
|
||||
use fal::prelude::fal_ai::flux_pro::v1::fill::FluxProFillInput;
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct FluxPro1InfillArgs<'a, U: IntoUrl, R: IntoUrl, L: IntoUrl> {
|
||||
// Request
|
||||
pub prompt: &'a str,
|
||||
pub image_url: U,
|
||||
pub mask_url: R,
|
||||
pub num_images: FluxPro1InfillNumImages,
|
||||
|
||||
// Fulfillment
|
||||
pub webhook_url: L,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum FluxPro1InfillNumImages {
|
||||
One, // Default
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
pub async fn enqueue_flux_pro_1_infill_webhook<U: IntoUrl, R: IntoUrl, L: IntoUrl>(
|
||||
args: FluxPro1InfillArgs<'_, U, R, L>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
let num_images = match args.num_images {
|
||||
FluxPro1InfillNumImages::One => 1,
|
||||
FluxPro1InfillNumImages::Two => 2,
|
||||
FluxPro1InfillNumImages::Three => 3,
|
||||
FluxPro1InfillNumImages::Four => 4,
|
||||
};
|
||||
|
||||
let request = FluxProFillInput {
|
||||
prompt: args.prompt.to_string(),
|
||||
image_url: args.image_url.as_str().to_string(),
|
||||
mask_url: args.mask_url.as_str().to_string(),
|
||||
num_images: Some(num_images),
|
||||
|
||||
// Maybe expose
|
||||
safety_tolerance: Some("5".to_string()), // NB: 5 is most tolerant
|
||||
output_format: Some("png".to_string()), // png or jpeg
|
||||
seed: None,
|
||||
|
||||
// Constants
|
||||
sync_mode: None, // Synchronous / slow
|
||||
};
|
||||
|
||||
let result = fill(request)
|
||||
.with_api_key(&args.api_key.0)
|
||||
.queue_webhook(args.webhook_url)
|
||||
.await;
|
||||
|
||||
result.map_err(|err| classify_fal_error(err))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::requests::webhook::image::infill::enqueue_flux_pro_1_infill_webhook::{enqueue_flux_pro_1_infill_webhook, FluxPro1InfillArgs, FluxPro1InfillNumImages};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use test_data::web::image_urls::{TALL_MOCHI_WITH_GLASSES_GLASSES_MASK_IMAGE_URL, TALL_MOCHI_WITH_GLASSES_IMAGE_URL};
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test() -> AnyhowResult<()> {
|
||||
// XXX: Don't commit secrets!
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = FluxPro1InfillArgs {
|
||||
image_url: TALL_MOCHI_WITH_GLASSES_IMAGE_URL,
|
||||
mask_url: TALL_MOCHI_WITH_GLASSES_GLASSES_MASK_IMAGE_URL,
|
||||
prompt: "slick sunglasses, cool glasses, reflection in glasses lenses",
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
num_images: FluxPro1InfillNumImages::One,
|
||||
};
|
||||
|
||||
let result = enqueue_flux_pro_1_infill_webhook(args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod enqueue_flux_pro_1_infill_webhook;
|
||||
@@ -1,6 +1,9 @@
|
||||
pub mod edit;
|
||||
pub mod enqueue_flux_1_dev_text_to_image_webhook;
|
||||
pub mod enqueue_flux_1_schnell_text_to_image_webhook;
|
||||
pub mod enqueue_flux_pro_11_text_to_image_webhook;
|
||||
pub mod enqueue_flux_pro_11_ultra_text_to_image_webhook;
|
||||
pub mod enqueue_gpt_image_1_edit_image_webhook;
|
||||
pub mod enqueue_gpt_image_1_text_to_image_webhook;
|
||||
pub mod infill;
|
||||
pub mod remove_background_rembg_webhook;
|
||||
|
||||
+4
-4
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+6
-23
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use crate::error::storyteller_error::StorytellerError;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
use crate::utils::basic_json_post_request::basic_json_post_request;
|
||||
use artcraft_api_defs::generate::image::edit::flux_pro_kontext_max_edit_image::{FluxProKontextMaxEditImageRequest, FluxProKontextMaxEditImageResponse, FLUX_PRO_KONTEXT_MAX_EDIT_IMAGE_PATH};
|
||||
|
||||
|
||||
pub async fn flux_pro_kontext_max_edit_image(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: FluxProKontextMaxEditImageRequest,
|
||||
) -> Result<FluxProKontextMaxEditImageResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
FLUX_PRO_KONTEXT_MAX_EDIT_IMAGE_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
pub mod flux_pro_kontext_max_edit_image;
|
||||
pub mod gpt_image_1_edit_image;
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use crate::error::storyteller_error::StorytellerError;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
use crate::utils::basic_json_post_request::basic_json_post_request;
|
||||
use artcraft_api_defs::generate::image::generate_gpt_image_1_text_to_image::{GenerateGptImage1TextToImageRequest, GenerateGptImage1TextToImageResponse, GPT_IMAGE_1_TEXT_TO_IMAGE_PATH};
|
||||
|
||||
|
||||
pub async fn generate_gpt_image_1_text_to_image(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: GenerateGptImage1TextToImageRequest,
|
||||
) -> Result<GenerateGptImage1TextToImageResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
GPT_IMAGE_1_TEXT_TO_IMAGE_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use crate::error::storyteller_error::StorytellerError;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
use crate::utils::basic_json_post_request::basic_json_post_request;
|
||||
use artcraft_api_defs::generate::image::inpaint::flux_pro_1_inpaint_image::{FluxPro1InpaintImageRequest, FluxPro1InpaintImageResponse, FLUX_PRO_1_INPAINT_PATH};
|
||||
|
||||
pub async fn flux_pro_1_inpaint_image(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: FluxPro1InpaintImageRequest,
|
||||
) -> Result<FluxPro1InpaintImageResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
FLUX_PRO_1_INPAINT_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod flux_pro_1_inpaint_image;
|
||||
@@ -3,4 +3,6 @@ pub mod generate_flux_1_dev_text_to_image;
|
||||
pub mod generate_flux_1_schnell_text_to_image;
|
||||
pub mod generate_flux_pro_11_text_to_image;
|
||||
pub mod generate_flux_pro_11_ultra_text_to_image;
|
||||
pub mod generate_gpt_image_1_text_to_image;
|
||||
pub mod inpaint;
|
||||
pub mod remove_image_background;
|
||||
@@ -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;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod get_media_file;
|
||||
pub mod upload_image_media_file_from_bytes;
|
||||
pub mod upload_image_media_file_from_file;
|
||||
pub mod upload_new_engine_asset_from_file;
|
||||
pub mod upload_video_media_file_from_file;
|
||||
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
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::shared_response_types::media_file_cover_image_details::MediaFileCoverImageDetails;
|
||||
use crate::shared_response_types::media_links::MediaLinks;
|
||||
use crate::shared_response_types::simple_entity_stats::SimpleEntityStats;
|
||||
use crate::shared_response_types::user_details_light::UserDetailsLight;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
|
||||
use crate::utils::filter_bad_response::filter_bad_response;
|
||||
use crate::utils::http_get_anonymous::http_get_anonymous;
|
||||
use chrono::{DateTime, Utc};
|
||||
use enums::by_table::media_files::media_file_animation_type::MediaFileAnimationType;
|
||||
use enums::by_table::media_files::media_file_class::MediaFileClass;
|
||||
use enums::by_table::media_files::media_file_engine_category::MediaFileEngineCategory;
|
||||
use enums::by_table::media_files::media_file_type::MediaFileType;
|
||||
use enums::by_table::model_weights::weights_category::WeightsCategory;
|
||||
use enums::common::visibility::Visibility;
|
||||
use enums::no_table::style_transfer::style_transfer_name::StyleTransferName;
|
||||
use enums_public::by_table::model_weights::public_weights_types::PublicWeightsType;
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
use log::debug;
|
||||
use reqwest::multipart::{Form, Part};
|
||||
use reqwest::Client;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
use tokens::tokens::batch_generations::BatchGenerationToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::model_weights::ModelWeightToken;
|
||||
use tokens::tokens::prompts::PromptToken;
|
||||
use uuid::uuid;
|
||||
|
||||
pub struct UploadImageBytesArgs<'a> {
|
||||
pub api_host: &'a ApiHost,
|
||||
pub maybe_creds: Option<&'a StorytellerCredentialSet>,
|
||||
|
||||
// NB: Bytes need to be owned for the request.
|
||||
pub image_bytes: Vec<u8>,
|
||||
pub image_type: ImageType,
|
||||
|
||||
/// If true, we should hide the image from the user's gallery.
|
||||
pub is_intermediate_system_file: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum ImageType {
|
||||
Png,
|
||||
Jpeg,
|
||||
Gif,
|
||||
}
|
||||
|
||||
impl ImageType {
|
||||
pub fn mime_type(&self) -> &str {
|
||||
match self {
|
||||
ImageType::Png => "image/png",
|
||||
ImageType::Jpeg => "image/jpeg",
|
||||
ImageType::Gif => "image/gif",
|
||||
}
|
||||
}
|
||||
pub fn file_extension(&self) -> &str {
|
||||
match self {
|
||||
ImageType::Png => "png",
|
||||
ImageType::Jpeg => "jpg",
|
||||
ImageType::Gif => "gif",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Upload an image media file from a file.
|
||||
/// NB: File type is not checked, so caller needs to enforce.
|
||||
/// NB: We need owned bytes for the request.
|
||||
pub async fn upload_image_media_file_from_bytes(
|
||||
args: UploadImageBytesArgs<'_>
|
||||
) -> Result<UploadImageMediaFileSuccessResponse, StorytellerError> {
|
||||
|
||||
let url = get_route(args.api_host);
|
||||
|
||||
debug!("Requesting {:?}", &url);
|
||||
|
||||
let client = Client::builder()
|
||||
.gzip(true)
|
||||
.build()
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
|
||||
let part = Part::bytes(args.image_bytes)
|
||||
.file_name(format!("image.{}", args.image_type.file_extension()))
|
||||
.mime_str(args.image_type.mime_type())
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
|
||||
let mut form = Form::new()
|
||||
.text("uuid_idempotency_token", generate_random_uuid())
|
||||
.part("file", part);
|
||||
|
||||
if args.is_intermediate_system_file {
|
||||
form = form.text("is_intermediate_system_file", "true");
|
||||
}
|
||||
|
||||
let mut request_builder = client.post(url)
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.header("Accept", APPLICATION_JSON);
|
||||
|
||||
if let Some(creds) = args.maybe_creds {
|
||||
if let Some(header) = &creds.maybe_as_cookie_header() {
|
||||
request_builder = request_builder.header("Cookie", header);
|
||||
}
|
||||
}
|
||||
|
||||
let response = request_builder
|
||||
.multipart(form)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
let response = filter_bad_response(response).await?;
|
||||
let response_body = &response.text().await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
let media_file = serde_json::from_str(&response_body)
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
Ok(media_file)
|
||||
}
|
||||
|
||||
fn get_route(api_host: &ApiHost) -> String {
|
||||
let api_hostname_and_scheme = api_host.to_api_hostname_and_scheme();
|
||||
format!("{}/v1/media_files/upload/image", api_hostname_and_scheme)
|
||||
}
|
||||
|
||||
// TODO(bt,2025-04-22): Share API definitions between client and server in common crate.
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct UploadImageMediaFileSuccessResponse {
|
||||
pub success: bool,
|
||||
pub media_file_token: MediaFileToken,
|
||||
}
|
||||
+21
-7
@@ -33,14 +33,24 @@ use tokens::tokens::model_weights::ModelWeightToken;
|
||||
use tokens::tokens::prompts::PromptToken;
|
||||
use uuid::uuid;
|
||||
|
||||
pub struct UploadImageFromFileArgs<'a, P: AsRef<Path>> {
|
||||
pub api_host: &'a ApiHost,
|
||||
pub maybe_creds: Option<&'a StorytellerCredentialSet>,
|
||||
|
||||
// NB: Path needs to be owned for the request.
|
||||
pub path: P,
|
||||
|
||||
/// If true, we should hide the image from the user's gallery.
|
||||
pub is_intermediate_system_file: bool,
|
||||
}
|
||||
|
||||
|
||||
/// Upload an image media file from a file.
|
||||
pub async fn upload_image_media_file_from_file<P: AsRef<Path>>(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
path: P,
|
||||
args: UploadImageFromFileArgs<'_, P>,
|
||||
) -> Result<UploadImageMediaFileSuccessResponse, StorytellerError> {
|
||||
|
||||
let url = get_route(api_host);
|
||||
let url = get_route(args.api_host);
|
||||
|
||||
debug!("Requesting {:?}", &url);
|
||||
|
||||
@@ -51,17 +61,21 @@ pub async fn upload_image_media_file_from_file<P: AsRef<Path>>(
|
||||
.build()
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
|
||||
let form = Form::new()
|
||||
let mut form = Form::new()
|
||||
.text("uuid_idempotency_token", generate_random_uuid())
|
||||
.file("file", path)
|
||||
.file("file", args.path)
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
|
||||
if args.is_intermediate_system_file {
|
||||
form = form.text("is_intermediate_system_file", "true");
|
||||
}
|
||||
|
||||
let mut request_builder = client.post(url)
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.header("Accept", APPLICATION_JSON);
|
||||
|
||||
if let Some(creds) = maybe_creds {
|
||||
if let Some(creds) = args.maybe_creds {
|
||||
if let Some(header) = &creds.maybe_as_cookie_header() {
|
||||
request_builder = request_builder.header("Cookie", header);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use crate::error::client_error::ClientError;
|
||||
use crate::error::storyteller_error::StorytellerError;
|
||||
use crate::media_files::upload_image_media_file_from_file::upload_image_media_file_from_file;
|
||||
use crate::media_files::upload_image_media_file_from_file::{upload_image_media_file_from_file, UploadImageFromFileArgs};
|
||||
use crate::media_files::upload_new_engine_asset_from_file::upload_new_engine_asset_from_file;
|
||||
use crate::media_files::upload_video_media_file_from_file::upload_video_media_file_from_file;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
@@ -49,7 +49,13 @@ pub async fn upload_media_file_from_file<P: AsRef<Path>>(
|
||||
}
|
||||
}
|
||||
Some(FileExtension::Png) | Some(FileExtension::Jpg) | Some(FileExtension::Gif) | Some(FileExtension::Webp) => {
|
||||
match upload_image_media_file_from_file(api_host, maybe_creds, path).await {
|
||||
let result = upload_image_media_file_from_file(UploadImageFromFileArgs {
|
||||
api_host,
|
||||
maybe_creds,
|
||||
path,
|
||||
is_intermediate_system_file: false,
|
||||
}).await;
|
||||
match result {
|
||||
Ok(result) => Ok(UploadMediaFileSuccessResponse {
|
||||
success: result.success,
|
||||
media_file_token: result.media_file_token,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -28,14 +28,17 @@ 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 }
|
||||
idempotency = { workspace = true }
|
||||
images = { workspace = true }
|
||||
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 +61,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 +76,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"
|
||||
|
||||
+93
-16
@@ -1,8 +1,9 @@
|
||||
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_gpt_image_1::handle_gpt_image_1;
|
||||
use crate::core::commands::enqueue::image::gpt_image_1::handle_gpt_image_1_sora::handle_gpt_image_1_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 +15,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 +37,28 @@ pub struct EnqueueTextToImageRequest {
|
||||
|
||||
/// The model to use.
|
||||
pub model: Option<ImageModel>,
|
||||
|
||||
/// Aspect ratio.
|
||||
pub aspect_ratio: Option<TextToImageSize>,
|
||||
|
||||
/// The number of images to generate.
|
||||
pub number_images: Option<u32>,
|
||||
}
|
||||
|
||||
// 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 +86,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 +102,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 +151,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 +167,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 +221,23 @@ 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_gpt_image_1(
|
||||
request,
|
||||
app,
|
||||
app_data_root,
|
||||
app_env_configs,
|
||||
provider_priority_store,
|
||||
storyteller_creds_manager,
|
||||
sora_creds_manager,
|
||||
sora_task_queue,
|
||||
).await;
|
||||
}
|
||||
_ => {
|
||||
// Fall-through
|
||||
|
||||
+84
-24
@@ -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, GenerateFlux1DevTextToImageNumImages, GenerateFlux1DevTextToImageRequest};
|
||||
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::{GenerateFlux1SchnellTextToImageAspectRatio, GenerateFlux1SchnellTextToImageNumImages, GenerateFlux1SchnellTextToImageRequest};
|
||||
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::{GenerateFluxPro11TextToImageAspectRatio, GenerateFluxPro11TextToImageNumImages, GenerateFluxPro11TextToImageRequest};
|
||||
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::{GenerateFluxPro11UltraTextToImageAspectRatio, GenerateFluxPro11UltraTextToImageNumImages, 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,
|
||||
@@ -56,19 +58,33 @@ pub async fn handle_image_artcraft(
|
||||
return Err(InternalImageError::NoModelSpecified);
|
||||
}
|
||||
Some(ImageModel::GptImage1) => {
|
||||
return Err(InternalImageError::AnyhowError(anyhow!("wrong logic: artcraft is handling sora images")));
|
||||
return Err(InternalImageError::AnyhowError(anyhow!("wrong logic: an earlier branch should have handled gpt-image-1 images for Artcraft")));
|
||||
}
|
||||
Some(ImageModel::Recraft3) => {
|
||||
return Err(InternalImageError::AnyhowError(anyhow!("not yet implemented in Artcraft")));
|
||||
}
|
||||
Some(ImageModel::Flux1Dev) => {
|
||||
info!("enqueue Flux 1 Dev");
|
||||
selected_model = ImageModel::Flux1Dev;
|
||||
selected_model = GenerationModel::Flux1Dev;
|
||||
let request = GenerateFlux1DevTextToImageRequest {
|
||||
uuid_idempotency_token,
|
||||
prompt: request.prompt,
|
||||
aspect_ratio: None,
|
||||
num_images: None,
|
||||
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,
|
||||
}),
|
||||
num_images: request.number_images
|
||||
.and_then(|num| match num {
|
||||
1 => Some(GenerateFlux1DevTextToImageNumImages::One),
|
||||
2 => Some(GenerateFlux1DevTextToImageNumImages::Two),
|
||||
3 => Some(GenerateFlux1DevTextToImageNumImages::Three),
|
||||
4 => Some(GenerateFlux1DevTextToImageNumImages::Four),
|
||||
_ => None, // NB: use service defaults
|
||||
}),
|
||||
};
|
||||
let result = generate_flux_1_dev_text_to_image(
|
||||
&app_env_configs.storyteller_host,
|
||||
@@ -88,12 +104,26 @@ pub async fn handle_image_artcraft(
|
||||
}
|
||||
Some(ImageModel::Flux1Schnell) => {
|
||||
info!("enqueue Flux 1 Schnell");
|
||||
selected_model = ImageModel::Flux1Schnell;
|
||||
selected_model = GenerationModel::Flux1Schnell;
|
||||
let request = GenerateFlux1SchnellTextToImageRequest {
|
||||
uuid_idempotency_token,
|
||||
prompt: request.prompt,
|
||||
aspect_ratio: None,
|
||||
num_images: None,
|
||||
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::Square => GenerateFlux1SchnellTextToImageAspectRatio::SquareHd,
|
||||
TextToImageSize::Auto => GenerateFlux1SchnellTextToImageAspectRatio::SquareHd,
|
||||
}),
|
||||
num_images: request.number_images
|
||||
.and_then(|num| match num {
|
||||
1 => Some(GenerateFlux1SchnellTextToImageNumImages::One),
|
||||
2 => Some(GenerateFlux1SchnellTextToImageNumImages::Two),
|
||||
3 => Some(GenerateFlux1SchnellTextToImageNumImages::Three),
|
||||
4 => Some(GenerateFlux1SchnellTextToImageNumImages::Four),
|
||||
_ => None, // NB: use service defaults
|
||||
}),
|
||||
};
|
||||
let result = generate_flux_1_schnell_text_to_image(
|
||||
&app_env_configs.storyteller_host,
|
||||
@@ -113,12 +143,26 @@ pub async fn handle_image_artcraft(
|
||||
}
|
||||
Some(ImageModel::FluxPro11) => {
|
||||
info!("enqueue Flux Pro 1.1");
|
||||
selected_model = ImageModel::FluxPro11;
|
||||
selected_model = GenerationModel::FluxPro11;
|
||||
let request = GenerateFluxPro11TextToImageRequest {
|
||||
uuid_idempotency_token,
|
||||
prompt: request.prompt,
|
||||
aspect_ratio: None,
|
||||
num_images: None,
|
||||
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::Square => GenerateFluxPro11TextToImageAspectRatio::Square,
|
||||
TextToImageSize::Auto => GenerateFluxPro11TextToImageAspectRatio::Square,
|
||||
}),
|
||||
num_images: request.number_images
|
||||
.and_then(|num| match num {
|
||||
1 => Some(GenerateFluxPro11TextToImageNumImages::One),
|
||||
2 => Some(GenerateFluxPro11TextToImageNumImages::Two),
|
||||
3 => Some(GenerateFluxPro11TextToImageNumImages::Three),
|
||||
4 => Some(GenerateFluxPro11TextToImageNumImages::Four),
|
||||
_ => None, // NB: use service defaults
|
||||
}),
|
||||
};
|
||||
let result = generate_flux_pro_11_text_to_image(
|
||||
&app_env_configs.storyteller_host,
|
||||
@@ -138,12 +182,26 @@ 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 request = GenerateFluxPro11UltraTextToImageRequest {
|
||||
uuid_idempotency_token,
|
||||
prompt: request.prompt,
|
||||
aspect_ratio: None,
|
||||
num_images: None,
|
||||
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::Square => GenerateFluxPro11UltraTextToImageAspectRatio::Square,
|
||||
TextToImageSize::Auto => GenerateFluxPro11UltraTextToImageAspectRatio::Square,
|
||||
}),
|
||||
num_images: request.number_images
|
||||
.and_then(|num| match num {
|
||||
1 => Some(GenerateFluxPro11UltraTextToImageNumImages::One),
|
||||
2 => Some(GenerateFluxPro11UltraTextToImageNumImages::Two),
|
||||
3 => Some(GenerateFluxPro11UltraTextToImageNumImages::Three),
|
||||
4 => Some(GenerateFluxPro11UltraTextToImageNumImages::Four),
|
||||
_ => None, // NB: use service defaults
|
||||
}),
|
||||
};
|
||||
let result = generate_flux_pro_11_ultra_text_to_image(
|
||||
&app_env_configs.storyteller_host,
|
||||
@@ -163,8 +221,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()),
|
||||
})
|
||||
}
|
||||
+16
-10
@@ -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;
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize};
|
||||
use crate::core::commands::enqueue::image::gpt_image_1::handle_gpt_image_1_artcraft::handle_gpt_image_1_artcraft;
|
||||
use crate::core::commands::enqueue::image::gpt_image_1::handle_gpt_image_1_sora::handle_gpt_image_1_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_edit_artcraft::handle_gpt_image_1_edit_artcraft;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_sora::handle_gpt_image_1_edit_sora;
|
||||
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, 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::core::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
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_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;
|
||||
use openai_sora_client::recipes::simple_image_gen_with_session_auto_renew::{simple_image_gen_with_session_auto_renew, SimpleImageGenAutoRenewRequest};
|
||||
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
|
||||
use std::time::Duration;
|
||||
use tauri::AppHandle;
|
||||
|
||||
pub async fn handle_gpt_image_1(
|
||||
request: EnqueueTextToImageRequest,
|
||||
app: &AppHandle,
|
||||
app_data_root: &AppDataRoot,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
provider_priority_store: &ProviderPriorityStore,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
sora_task_queue: &SoraTaskQueue,
|
||||
) -> Result<TaskEnqueueSuccess, InternalImageError> {
|
||||
|
||||
let priority = provider_priority_store.get_priority()?;
|
||||
|
||||
// TODO: Check if providers are available before proceeding.
|
||||
|
||||
info!("Providers by priority: {:?}", priority);
|
||||
|
||||
for provider in priority.iter() {
|
||||
match provider {
|
||||
Provider::Fal => {
|
||||
// Fallthrough
|
||||
// If in the future we support OpenAI API keys, it's worth considering whether we
|
||||
// send those requests ourselves, or if we use FAL as an intermediary. FAL makes
|
||||
// the API nicer to deal with, but the user needs an additional key.
|
||||
}
|
||||
Provider::Artcraft => {
|
||||
info!("Dispatching gpt-image-1 via Artcraft...");
|
||||
return handle_gpt_image_1_artcraft(
|
||||
request,
|
||||
app_env_configs,
|
||||
storyteller_creds_manager
|
||||
).await;
|
||||
}
|
||||
Provider::Sora => {
|
||||
info!("Dispatching gpt-image-1 via Sora...");
|
||||
return handle_gpt_image_1_sora(
|
||||
request,
|
||||
app,
|
||||
sora_creds_manager,
|
||||
sora_task_queue,
|
||||
).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(InternalImageError::NoProviderAvailable)
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
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_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize};
|
||||
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
|
||||
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, 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::core::state::provider_priority::ProviderPriorityStore;
|
||||
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 artcraft_api_defs::generate::image::generate_gpt_image_1_text_to_image::{GenerateGptImage1TextToImageImageQuality, GenerateGptImage1TextToImageImageSize, GenerateGptImage1TextToImageNumImages, GenerateGptImage1TextToImageRequest};
|
||||
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};
|
||||
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
|
||||
use openai_sora_client::recipes::simple_image_gen_with_session_auto_renew::{simple_image_gen_with_session_auto_renew, SimpleImageGenAutoRenewRequest};
|
||||
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
|
||||
use std::time::Duration;
|
||||
use storyteller_client::generate::image::edit::gpt_image_1_edit_image::gpt_image_1_edit_image;
|
||||
use storyteller_client::generate::image::generate_gpt_image_1_text_to_image::generate_gpt_image_1_text_to_image;
|
||||
use tauri::AppHandle;
|
||||
|
||||
pub async fn handle_gpt_image_1_artcraft(
|
||||
request: EnqueueTextToImageRequest,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
) -> Result<TaskEnqueueSuccess, InternalImageError> {
|
||||
|
||||
let creds = match storyteller_creds_manager.get_credentials()? {
|
||||
Some(creds) => creds,
|
||||
None => {
|
||||
return Err(InternalImageError::NeedsStorytellerCredentials);
|
||||
},
|
||||
};
|
||||
|
||||
info!("Calling Artcraft gpt-image-1 ...");
|
||||
|
||||
let uuid_idempotency_token = generate_random_uuid();
|
||||
|
||||
let image_quality = Some(GenerateGptImage1TextToImageImageQuality::High);
|
||||
|
||||
//let image_quality = request.image_quality
|
||||
// .map(|quality| match quality {
|
||||
// EditImageQuality::Auto => GenerateGptImage1TextToImageImageQuality::Auto,
|
||||
// EditImageQuality::High => GenerateGptImage1TextToImageImageQuality::High,
|
||||
// EditImageQuality::Medium => GenerateGptImage1TextToImageImageQuality::Medium,
|
||||
// EditImageQuality::Low => GenerateGptImage1TextToImageImageQuality::Low,
|
||||
// });
|
||||
|
||||
let image_size = request.aspect_ratio
|
||||
.map(|size| match size {
|
||||
TextToImageSize::Auto => GenerateGptImage1TextToImageImageSize::Square,
|
||||
TextToImageSize::Square => GenerateGptImage1TextToImageImageSize::Square,
|
||||
TextToImageSize::Tall => GenerateGptImage1TextToImageImageSize::Vertical,
|
||||
TextToImageSize::Wide => GenerateGptImage1TextToImageImageSize::Horizontal,
|
||||
});
|
||||
|
||||
let num_images = match request.number_images {
|
||||
None => None,
|
||||
Some(1) => Some(GenerateGptImage1TextToImageNumImages::One),
|
||||
Some(2) => Some(GenerateGptImage1TextToImageNumImages::Two),
|
||||
Some(3) => Some(GenerateGptImage1TextToImageNumImages::Three),
|
||||
Some(4) => Some(GenerateGptImage1TextToImageNumImages::Four),
|
||||
// TODO: Error
|
||||
//Some(other) => {
|
||||
// return Err(InternalImageError::InvalidNumberOfRequestedImages {
|
||||
// min: 1,
|
||||
// max: 4,
|
||||
// requested: other,
|
||||
// });
|
||||
//},
|
||||
_ => Some(GenerateGptImage1TextToImageNumImages::One), // Default to one image if invalid number
|
||||
};
|
||||
|
||||
let request = GenerateGptImage1TextToImageRequest {
|
||||
uuid_idempotency_token,
|
||||
prompt: request.prompt.clone(),
|
||||
image_size,
|
||||
num_images,
|
||||
image_quality,
|
||||
};
|
||||
|
||||
let result = generate_gpt_image_1_text_to_image(
|
||||
&app_env_configs.storyteller_host,
|
||||
Some(&creds),
|
||||
request,
|
||||
).await;
|
||||
|
||||
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(InternalImageError::StorytellerError(err));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(TaskEnqueueSuccess {
|
||||
provider: GenerationProvider::Artcraft,
|
||||
model: Some(GenerationModel::GptImage1),
|
||||
provider_job_id: Some(job_id.to_string()),
|
||||
task_type: TaskType::ImageGeneration,
|
||||
})
|
||||
}
|
||||
+34
-8
@@ -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;
|
||||
@@ -15,12 +19,12 @@ use tauri::AppHandle;
|
||||
|
||||
const SORA_SIMPLE_IMAGE_GEN_TIMEOUT : Duration = Duration::from_millis(1000 * 30); // 30 seconds
|
||||
|
||||
pub async fn handle_image_sora(
|
||||
app: &AppHandle,
|
||||
pub async fn handle_gpt_image_1_sora(
|
||||
request: EnqueueTextToImageRequest,
|
||||
app: &AppHandle,
|
||||
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,29 @@ 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 num_images = match request.number_images {
|
||||
None => NumImages::One,
|
||||
Some(1) => NumImages::One,
|
||||
Some(2) => NumImages::Two,
|
||||
Some(3) => NumImages::Two, // NB: There is no option for three images!
|
||||
Some(4) => NumImages::Four,
|
||||
_ => NumImages::One,
|
||||
};
|
||||
|
||||
let result =
|
||||
simple_image_gen_with_session_auto_renew(SimpleImageGenAutoRenewRequest {
|
||||
prompt: prompt.to_string(),
|
||||
num_images: NumImages::One,
|
||||
image_size: ImageSize::Square,
|
||||
num_images,
|
||||
image_size: size,
|
||||
credentials: &creds,
|
||||
request_timeout: Some(SORA_SIMPLE_IMAGE_GEN_TIMEOUT),
|
||||
}).await;
|
||||
@@ -80,5 +101,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,3 @@
|
||||
pub (crate) mod handle_gpt_image_1_artcraft;
|
||||
pub (crate) mod handle_gpt_image_1_sora;
|
||||
pub (super) mod handle_gpt_image_1;
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
+46
-11
@@ -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::image_edit::gpt_image_1::handle_gpt_image_1_edit::handle_gpt_image_1_edit;
|
||||
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,11 +37,13 @@ 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;
|
||||
use storyteller_client::utils::api_host::ApiHost;
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
use enums::tauri::ux::tauri_command_caller::TauriCommandCaller;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
@@ -55,6 +56,21 @@ pub struct EnqueueImageBgRemovalCommand {
|
||||
|
||||
/// Base64-encoded image
|
||||
pub base64_image: Option<String>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// Name of the frontend caller.
|
||||
/// We'll use this to selectively trigger events.
|
||||
pub frontend_caller: Option<TauriCommandCaller>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// A frontend-defined identifier that we'll send back to the frontend
|
||||
/// as a Tauri event on task completion.
|
||||
pub frontend_subscriber_id: Option<String>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// A frontend-defined payload that we'll send back to the frontend
|
||||
/// as a Tauri event on task completion.
|
||||
pub frontend_subscriber_payload: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
@@ -89,6 +105,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 +119,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 +145,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 +165,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 +182,21 @@ pub async fn handle_request(
|
||||
storyteller_creds_manager,
|
||||
fal_creds_manager,
|
||||
fal_task_queue,
|
||||
).await
|
||||
).await?;
|
||||
|
||||
let result = success_event
|
||||
.insert_into_task_database_with_frontend_payload(
|
||||
task_database,
|
||||
request.frontend_caller,
|
||||
request.frontend_subscriber_id.as_deref(),
|
||||
request.frontend_subscriber_payload.as_deref()
|
||||
)
|
||||
.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)
|
||||
}
|
||||
|
||||
+4
-5
@@ -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::image_edit::gpt_image_1::handle_gpt_image_1_edit_artcraft::handle_gpt_image_1_edit_artcraft;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_sora::handle_gpt_image_1_edit_sora;
|
||||
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()?;
|
||||
|
||||
|
||||
+20
-11
@@ -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;
|
||||
@@ -38,7 +39,7 @@ 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 storyteller_client::generate::image::remove_image_background::remove_image_background;
|
||||
use storyteller_client::media_files::upload_image_media_file_from_file::upload_image_media_file_from_file;
|
||||
use storyteller_client::media_files::upload_image_media_file_from_file::{upload_image_media_file_from_file, UploadImageFromFileArgs};
|
||||
use tauri::AppHandle;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -118,9 +123,13 @@ async fn upload_image_from_base64_bytes(
|
||||
|
||||
info!("Uploading image media file from temp file: {:?}", temp_file.path());
|
||||
|
||||
let result =
|
||||
upload_image_media_file_from_file(&app_env_configs.storyteller_host, Some(&creds), temp_file)
|
||||
.await
|
||||
let result =
|
||||
upload_image_media_file_from_file(UploadImageFromFileArgs {
|
||||
api_host: &app_env_configs.storyteller_host,
|
||||
maybe_creds: Some(&creds),
|
||||
path: temp_file,
|
||||
is_intermediate_system_file: true, // NB: Probably not essential to keep this.
|
||||
}).await
|
||||
.map_err(|err| {
|
||||
error!("Failed to upload image media file: {:?}", err);
|
||||
InternalBgRemovalError::StorytellerError(err)
|
||||
|
||||
+2
-2
@@ -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,
|
||||
// }
|
||||
//}
|
||||
}
|
||||
+46
-17
@@ -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::image_edit::gpt_image_1::handle_gpt_image_1_edit::handle_gpt_image_1_edit;
|
||||
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,13 +36,14 @@ 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;
|
||||
use storyteller_client::utils::api_host::ApiHost;
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::core::commands::enqueue::image_edit::flux_kontext::handle_flux_kontext_edit::handle_flux_kontext_edit;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct EnqueueContextualEditImageCommand {
|
||||
@@ -130,6 +132,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 +149,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 +177,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,18 +197,19 @@ 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(
|
||||
Some(ContextualImageEditModel::FluxProKontextMax) => {
|
||||
handle_flux_kontext_edit(
|
||||
request,
|
||||
app,
|
||||
app_data_root,
|
||||
@@ -215,8 +220,32 @@ pub async fn handle_request(
|
||||
fal_task_queue,
|
||||
sora_creds_manager,
|
||||
sora_task_queue,
|
||||
).await
|
||||
).await?
|
||||
}
|
||||
// TODO(bt,2025-07-05): Flux Kontext, etc.
|
||||
Some(ContextualImageEditModel::GptImage1) => {
|
||||
handle_gpt_image_1_edit(
|
||||
request,
|
||||
app,
|
||||
app_data_root,
|
||||
app_env_configs,
|
||||
provider_priority_store,
|
||||
storyteller_creds_manager,
|
||||
fal_creds_manager,
|
||||
fal_task_queue,
|
||||
sora_creds_manager,
|
||||
sora_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.
|
||||
}
|
||||
|
||||
Ok(success_event)
|
||||
}
|
||||
|
||||
@@ -30,6 +30,12 @@ pub enum InternalContextualEditImageError {
|
||||
requested: u32,
|
||||
},
|
||||
|
||||
// TODO: Maybe just reuse the other error enum variant
|
||||
/// An invalid number of input images was requested for Flux Kontext.
|
||||
InvalidNumberOfInputImagesForFluxKontext {
|
||||
message: String,
|
||||
},
|
||||
|
||||
/// No sora credentials are set.
|
||||
NeedsSoraCredentials,
|
||||
|
||||
@@ -84,6 +90,11 @@ impl InternalContextualEditImageError {
|
||||
error_type = EnqueueContextualEditImageErrorType::BadRequest;
|
||||
error_message = format!("Invalid number of input images ({}). Must be between {} and {}", requested, min, max);
|
||||
}
|
||||
InternalContextualEditImageError::InvalidNumberOfInputImagesForFluxKontext { message } => {
|
||||
status = CommandErrorStatus::BadRequest;
|
||||
error_type = EnqueueContextualEditImageErrorType::BadRequest;
|
||||
error_message = message.clone();
|
||||
}
|
||||
InternalContextualEditImageError::SoraError(_) => {}
|
||||
InternalContextualEditImageError::AnyhowError(_) => {}
|
||||
InternalContextualEditImageError::StorytellerError(_) => {}
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
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::flux_kontext::handle_flux_kontext_edit_artcraft::handle_flux_kontext_edit_artcraft;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_artcraft::handle_gpt_image_1_edit_artcraft;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_sora::handle_gpt_image_1_edit_sora;
|
||||
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::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 log::info;
|
||||
use tauri::AppHandle;
|
||||
|
||||
pub async fn handle_flux_kontext_edit(
|
||||
request: &EnqueueContextualEditImageCommand,
|
||||
app: &AppHandle,
|
||||
app_data_root: &AppDataRoot,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
provider_priority_store: &ProviderPriorityStore,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
sora_task_queue: &SoraTaskQueue,
|
||||
) -> Result<TaskEnqueueSuccess, InternalContextualEditImageError> {
|
||||
|
||||
let priority = provider_priority_store.get_priority()?;
|
||||
|
||||
// TODO: Check if providers are available before proceeding.
|
||||
|
||||
info!("Providers by priority: {:?}", priority);
|
||||
|
||||
for provider in priority.iter() {
|
||||
match provider {
|
||||
Provider::Fal => {
|
||||
// Fallthrough (TODO)
|
||||
}
|
||||
Provider::Sora => {
|
||||
// Fallthrough
|
||||
}
|
||||
Provider::Artcraft => {
|
||||
info!("Dispatching gpt-image-1 (edit) via Artcraft...");
|
||||
return handle_flux_kontext_edit_artcraft(
|
||||
request,
|
||||
app,
|
||||
app_data_root,
|
||||
app_env_configs,
|
||||
storyteller_creds_manager
|
||||
).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(InternalContextualEditImageError::NoProviderAvailable)
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
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_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize, EnqueueContextualEditImageCommand};
|
||||
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
|
||||
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, 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;
|
||||
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::ProviderPriorityStore;
|
||||
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 anyhow::anyhow;
|
||||
use artcraft_api_defs::generate::image::edit::flux_pro_kontext_max_edit_image::{FluxProKontextMaxEditImageNumImages, FluxProKontextMaxEditImageRequest};
|
||||
use artcraft_api_defs::generate::image::edit::gpt_image_1_edit_image::{GptImage1EditImageImageQuality, GptImage1EditImageImageSize, GptImage1EditImageNumImages, GptImage1EditImageRequest};
|
||||
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 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::edit::enqueue_flux_pro_kontext_max_edit_webhook::FluxProKontextMaxNumImages;
|
||||
use fal_client::requests::webhook::image::enqueue_gpt_image_1_edit_image_webhook::GptEditImageQuality;
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
use log::{error, info};
|
||||
use storyteller_client::generate::image::edit::flux_pro_kontext_max_edit_image::flux_pro_kontext_max_edit_image;
|
||||
use storyteller_client::generate::image::edit::gpt_image_1_edit_image::gpt_image_1_edit_image;
|
||||
use storyteller_client::generate::image::generate_flux_1_dev_text_to_image::generate_flux_1_dev_text_to_image;
|
||||
use storyteller_client::generate::image::generate_flux_1_schnell_text_to_image::generate_flux_1_schnell_text_to_image;
|
||||
use storyteller_client::generate::image::generate_flux_pro_11_text_to_image::generate_flux_pro_11_text_to_image;
|
||||
use storyteller_client::generate::image::generate_flux_pro_11_ultra_text_to_image::generate_flux_pro_11_ultra_text_to_image;
|
||||
use tauri::AppHandle;
|
||||
|
||||
pub async fn handle_flux_kontext_edit_artcraft(
|
||||
request: &EnqueueContextualEditImageCommand,
|
||||
app: &AppHandle,
|
||||
app_data_root: &AppDataRoot,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
) -> Result<TaskEnqueueSuccess, InternalContextualEditImageError> {
|
||||
|
||||
let creds = match storyteller_creds_manager.get_credentials()? {
|
||||
Some(creds) => creds,
|
||||
None => {
|
||||
return Err(InternalContextualEditImageError::NeedsStorytellerCredentials);
|
||||
},
|
||||
};
|
||||
|
||||
info!("Calling Artcraft flux kontext max edit (edit) ...");
|
||||
|
||||
let uuid_idempotency_token = generate_random_uuid();
|
||||
|
||||
let num_images = match request.image_count {
|
||||
None => None,
|
||||
Some(1) => Some(FluxProKontextMaxEditImageNumImages::One),
|
||||
Some(2) => Some(FluxProKontextMaxEditImageNumImages::Two),
|
||||
Some(3) => Some(FluxProKontextMaxEditImageNumImages::Three),
|
||||
Some(4) => Some(FluxProKontextMaxEditImageNumImages::Four),
|
||||
Some(other) => {
|
||||
return Err(InternalContextualEditImageError::InvalidNumberOfRequestedImages {
|
||||
min: 1,
|
||||
max: 4,
|
||||
requested: other,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
let mut maybe_media_token = None;
|
||||
|
||||
let list_count = request.image_media_tokens
|
||||
.as_ref()
|
||||
.map(|tokens| tokens.len())
|
||||
.unwrap_or(0);
|
||||
|
||||
if request.scene_image_media_token.is_some() && list_count > 0 {
|
||||
return Err(InternalContextualEditImageError::InvalidNumberOfInputImagesForFluxKontext {
|
||||
message: "Cannot specify both scene_image_media_token and image_media_tokens for Flux Kontext Max".to_string()
|
||||
});
|
||||
}
|
||||
|
||||
if list_count > 1 {
|
||||
return Err(InternalContextualEditImageError::InvalidNumberOfInputImagesForFluxKontext {
|
||||
message: "Cannot specify more than one image in image_media_tokens for Flux Kontext Max".to_string()
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(media_token) = &request.scene_image_media_token {
|
||||
maybe_media_token = Some(media_token.clone());
|
||||
}
|
||||
|
||||
let maybe_list_media_token = request.image_media_tokens
|
||||
.as_ref()
|
||||
.map(|tokens| tokens.first())
|
||||
.flatten();
|
||||
|
||||
if let Some(media_token) = maybe_list_media_token {
|
||||
maybe_media_token = Some(media_token.clone());
|
||||
}
|
||||
|
||||
let media_token = match maybe_media_token {
|
||||
Some(token) => token,
|
||||
None => {
|
||||
return Err(InternalContextualEditImageError::InvalidNumberOfInputImagesForFluxKontext {
|
||||
message: "No input image specified for Flux Kontext Max edit".to_string(),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
let request = FluxProKontextMaxEditImageRequest {
|
||||
uuid_idempotency_token,
|
||||
prompt: Some(request.prompt.clone()),
|
||||
image_media_token: media_token,
|
||||
num_images,
|
||||
};
|
||||
|
||||
let result = flux_pro_kontext_max_edit_image(
|
||||
&app_env_configs.storyteller_host,
|
||||
Some(&creds),
|
||||
request,
|
||||
).await;
|
||||
|
||||
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(TaskEnqueueSuccess {
|
||||
provider: GenerationProvider::Artcraft,
|
||||
model: Some(GenerationModel::FluxProKontextMax),
|
||||
provider_job_id: Some(job_id.to_string()),
|
||||
task_type: TaskType::ImageGeneration,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub (super) mod handle_flux_kontext_edit;
|
||||
mod handle_flux_kontext_edit_artcraft;
|
||||
+9
-9
@@ -1,8 +1,8 @@
|
||||
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::image_edit::gpt_image_1::handle_gpt_image_1_edit_artcraft::handle_gpt_image_1_edit_artcraft;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_sora::handle_gpt_image_1_edit_sora;
|
||||
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};
|
||||
@@ -16,7 +16,7 @@ use tauri::AppHandle;
|
||||
|
||||
pub(super) const MAX_IMAGES: usize = 10;
|
||||
|
||||
pub async fn handle_gpt_image_1(
|
||||
pub async fn handle_gpt_image_1_edit(
|
||||
request: &EnqueueContextualEditImageCommand,
|
||||
app: &AppHandle,
|
||||
app_data_root: &AppDataRoot,
|
||||
@@ -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()?;
|
||||
|
||||
@@ -44,8 +44,8 @@ pub async fn handle_gpt_image_1(
|
||||
// the API nicer to deal with, but the user needs an additional key.
|
||||
}
|
||||
Provider::Artcraft => {
|
||||
info!("Dispatching gpt-image-1 via Artcraft...");
|
||||
return handle_gpt_image_1_artcraft(
|
||||
info!("Dispatching gpt-image-1 (edit) via Artcraft...");
|
||||
return handle_gpt_image_1_edit_artcraft(
|
||||
request,
|
||||
app,
|
||||
app_data_root,
|
||||
@@ -54,8 +54,8 @@ pub async fn handle_gpt_image_1(
|
||||
).await;
|
||||
}
|
||||
Provider::Sora => {
|
||||
info!("Dispatching gpt-image-1 via Sora...");
|
||||
return handle_gpt_image_1_sora(
|
||||
info!("Dispatching gpt-image-1 (edit) via Sora...");
|
||||
return handle_gpt_image_1_edit_sora(
|
||||
request,
|
||||
app,
|
||||
app_data_root,
|
||||
+16
-11
@@ -2,10 +2,10 @@ 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_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::image_edit::gpt_image_1::handle_gpt_image_1_edit::MAX_IMAGES;
|
||||
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,14 +33,16 @@ 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(
|
||||
pub async fn handle_gpt_image_1_edit_artcraft(
|
||||
request: &EnqueueContextualEditImageCommand,
|
||||
app: &AppHandle,
|
||||
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,
|
||||
@@ -49,7 +51,7 @@ pub async fn handle_gpt_image_1_artcraft(
|
||||
},
|
||||
};
|
||||
|
||||
info!("Calling Artcraft gpt-image-1 ...");
|
||||
info!("Calling Artcraft gpt-image-1 (edit) ...");
|
||||
|
||||
let uuid_idempotency_token = generate_random_uuid();
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
+12
-9
@@ -2,11 +2,11 @@ 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_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::image_edit::gpt_image_1::handle_gpt_image_1_edit::MAX_IMAGES;
|
||||
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;
|
||||
@@ -46,15 +48,14 @@ const SORA_IMAGE_REMIX_TIMEOUT: Duration = Duration::from_millis(1000 * 30); //
|
||||
|
||||
const DEFAULT_ASPECT_RATIO : EditImageSize = EditImageSize::Square;
|
||||
|
||||
pub async fn handle_gpt_image_1_sora(
|
||||
pub async fn handle_gpt_image_1_edit_sora(
|
||||
request: &EnqueueContextualEditImageCommand,
|
||||
app: &AppHandle,
|
||||
app_data_root: &AppDataRoot,
|
||||
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,3 +1,3 @@
|
||||
pub mod handle_gpt_image_1;
|
||||
pub mod handle_gpt_image_1_artcraft;
|
||||
pub mod handle_gpt_image_1_sora;
|
||||
pub mod handle_gpt_image_1_edit;
|
||||
pub mod handle_gpt_image_1_edit_artcraft;
|
||||
pub mod handle_gpt_image_1_edit_sora;
|
||||
@@ -1,4 +1,4 @@
|
||||
pub mod enqueue_contextual_edit_image_command;
|
||||
pub mod errors;
|
||||
pub mod gpt_image_1;
|
||||
pub mod success_event;
|
||||
mod flux_kontext;
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, EnqueueTextToImageSuccessResponse};
|
||||
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_edit::handle_gpt_image_1_edit;
|
||||
use crate::core::commands::enqueue::image_inpaint::errors::InternalImageInpaintError;
|
||||
use crate::core::commands::enqueue::image_inpaint::flux_pro_1_inpaint::handle_flux_pro_1_inpaint::handle_flux_pro_1_inpaint;
|
||||
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;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
|
||||
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
|
||||
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
|
||||
use crate::core::events::sendable_event_trait::SendableEvent;
|
||||
use crate::core::model::contextual_image_edit_models::ContextualImageEditModel;
|
||||
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::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;
|
||||
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 enums::tauri::ux::tauri_command_caller::TauriCommandCaller;
|
||||
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};
|
||||
use openai_sora_client::recipes::image_upload_from_file_with_session_auto_renew::{image_upload_from_file_with_session_auto_renew, ImageUploadFromFileAutoRenewRequest};
|
||||
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
|
||||
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;
|
||||
use storyteller_client::utils::api_host::ApiHost;
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Deserialize, Debug, Copy, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ImageInpaintModel {
|
||||
#[serde(rename = "flux_pro_1")]
|
||||
FluxPro1,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct EnqueueInpaintImageCommand {
|
||||
/// REQUIRED (Option<T> is just for error messages).
|
||||
/// The model to use.
|
||||
pub model: Option<ImageInpaintModel>,
|
||||
|
||||
/// REQUIRED (Option<T> is just for error messages).
|
||||
/// The source image to edit.
|
||||
pub image_media_token: Option<MediaFileToken>,
|
||||
|
||||
/// REQUIRED: Supply this *XOR* `mask_image_raw_bytes`.
|
||||
/// The mask to focus the edit (already uploaded).
|
||||
pub mask_image_media_token: Option<MediaFileToken>,
|
||||
|
||||
/// REQUIRED: Supply this *XOR* `mask_image_media_token`.
|
||||
/// The mask to focus the edit (raw bytes).
|
||||
pub mask_image_raw_bytes: Option<Vec<u8>>,
|
||||
|
||||
/// REQUIRED.
|
||||
/// The user's image generation prompt.
|
||||
pub prompt: String,
|
||||
|
||||
/// Number of images to generate.
|
||||
pub image_count: Option<u32>,
|
||||
|
||||
/// If true, force the dimensions of the source image and mask image to match.
|
||||
pub require_matching_dimensions: Option<bool>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// Name of the frontend caller.
|
||||
/// We'll use this to selectively trigger events.
|
||||
pub frontend_caller: Option<TauriCommandCaller>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// A frontend-defined identifier that we'll send back to the frontend
|
||||
/// as a Tauri event on task completion.
|
||||
pub frontend_subscriber_id: Option<String>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// A frontend-defined payload that we'll send back to the frontend
|
||||
/// as a Tauri event on task completion.
|
||||
pub frontend_subscriber_payload: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum EnqueueInpaintImageErrorType {
|
||||
/// Caller didn't specify a model
|
||||
ModelNotSpecified,
|
||||
|
||||
/// No model available for image generation
|
||||
NoProviderAvailable,
|
||||
|
||||
/// No source image was supplied.
|
||||
NoSourceImageSpecified,
|
||||
|
||||
/// No mask image was supplied.
|
||||
NoMaskImageSpecified,
|
||||
|
||||
/// Too many mask images were supplied.
|
||||
MultipleMaskImagesSpecified,
|
||||
|
||||
/// Bad mask image was supplied.
|
||||
BadMaskImage,
|
||||
|
||||
/// Generic bad request error
|
||||
BadRequest,
|
||||
|
||||
/// Generic server error
|
||||
ServerError,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct EnqueueImageInpaintSuccessResponse {
|
||||
}
|
||||
|
||||
impl SerializeMarker for EnqueueImageInpaintSuccessResponse {}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn enqueue_image_inpaint_command(
|
||||
app: AppHandle,
|
||||
request: EnqueueInpaintImageCommand,
|
||||
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>,
|
||||
sora_creds_manager: State<'_, SoraCredentialManager>,
|
||||
sora_task_queue: State<'_, SoraTaskQueue>,
|
||||
) -> ResponseOrErrorType<EnqueueImageInpaintSuccessResponse, EnqueueInpaintImageErrorType> {
|
||||
|
||||
info!("enqueue_image_inpaint_command called; model: {:?}, image_media_token: {:?}, prompt: {:?}",
|
||||
&request.model, &request.image_media_token, &request.prompt);
|
||||
|
||||
let result = handle_request(
|
||||
&request,
|
||||
&app,
|
||||
&app_data_root,
|
||||
&app_env_configs,
|
||||
&provider_priority_store,
|
||||
&task_database,
|
||||
&storyteller_creds_manager,
|
||||
&fal_creds_manager,
|
||||
&fal_task_queue,
|
||||
&sora_creds_manager,
|
||||
&sora_task_queue,
|
||||
).await;
|
||||
|
||||
match result {
|
||||
Err(err) => {
|
||||
error!("Error enqueuing inpaint image: {:?}", err);
|
||||
|
||||
// TODO: Derive from err. Make service provider optional.
|
||||
let event = GenerationEnqueueFailureEvent {
|
||||
action: GenerationAction::GenerateImage,
|
||||
service: GenerationServiceProvider::Artcraft, // FIXME: This is wrong.
|
||||
model: None,
|
||||
reason: None,
|
||||
};
|
||||
|
||||
if let Err(err) = event.send(&app) {
|
||||
error!("Failed to emit event: {:?}", err); // Fail open.
|
||||
}
|
||||
|
||||
Err(err.to_tauri_response())
|
||||
}
|
||||
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(EnqueueImageInpaintSuccessResponse {}.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_request(
|
||||
request: &EnqueueInpaintImageCommand,
|
||||
app: &AppHandle,
|
||||
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<TaskEnqueueSuccess, InternalImageInpaintError> {
|
||||
let success_event= match request.model {
|
||||
None => {
|
||||
return Err(InternalImageInpaintError::NoModelSpecified)
|
||||
}
|
||||
Some(ImageInpaintModel::FluxPro1) => {
|
||||
handle_flux_pro_1_inpaint(
|
||||
request,
|
||||
app,
|
||||
app_data_root,
|
||||
app_env_configs,
|
||||
provider_priority_store,
|
||||
storyteller_creds_manager,
|
||||
fal_creds_manager,
|
||||
fal_task_queue,
|
||||
sora_creds_manager,
|
||||
sora_task_queue,
|
||||
).await?
|
||||
}
|
||||
};
|
||||
|
||||
let result = success_event
|
||||
.insert_into_task_database_with_frontend_payload(
|
||||
task_database,
|
||||
request.frontend_caller,
|
||||
request.frontend_subscriber_id.as_deref(),
|
||||
request.frontend_subscriber_payload.as_deref()
|
||||
)
|
||||
.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)
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageErrorType;
|
||||
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
|
||||
use errors::AnyhowError;
|
||||
use serde::Serialize;
|
||||
use storyteller_client::error::storyteller_error::StorytellerError;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum InternalImageInpaintError {
|
||||
/// Request did not specify a model.
|
||||
NoModelSpecified,
|
||||
|
||||
/// No provider is configured for the model type.
|
||||
NoProviderAvailable,
|
||||
|
||||
/// A source image was not provided
|
||||
NoSourceImageSpecified,
|
||||
|
||||
/// A mask image was not provided
|
||||
NoMaskImageSpecified,
|
||||
|
||||
/// Both mask image media token and raw bytes were supplied.
|
||||
MaskMediaTokenAndBytesSupplied,
|
||||
|
||||
/// Mask image has an unknown MIME type.
|
||||
CouldNotDetermineMaskMimeType,
|
||||
|
||||
/// Could not encode the mask image to PNG.
|
||||
CouldNotEncodeMask,
|
||||
|
||||
/// No storyteller credentials are set.
|
||||
NeedsStorytellerCredentials,
|
||||
|
||||
/// An invalid number of images was requested.
|
||||
InvalidNumberOfRequestedImages {
|
||||
min: u32,
|
||||
max: u32,
|
||||
requested: u32,
|
||||
},
|
||||
|
||||
/// An invalid number of input images was requested.
|
||||
InvalidNumberOfInputImages {
|
||||
min: u32,
|
||||
max: u32,
|
||||
requested: u32,
|
||||
},
|
||||
|
||||
AnyhowError(AnyhowError),
|
||||
StorytellerError(StorytellerError),
|
||||
}
|
||||
|
||||
impl From<AnyhowError> for InternalImageInpaintError {
|
||||
fn from(value: AnyhowError) -> Self {
|
||||
Self::AnyhowError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StorytellerError> for InternalImageInpaintError {
|
||||
fn from(value: StorytellerError) -> Self {
|
||||
Self::StorytellerError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl InternalImageInpaintError {
|
||||
pub fn to_tauri_response<E: Serialize>(&self) -> CommandErrorResponseWrapper<EnqueueInpaintImageErrorType, E> {
|
||||
let mut status = CommandErrorStatus::ServerError;
|
||||
let mut error_type = EnqueueInpaintImageErrorType::ServerError;
|
||||
let mut error_message = "A server error occurred. Please try again. If it continues, please tell our staff about the problem.".to_string();
|
||||
|
||||
match self {
|
||||
InternalImageInpaintError::NoModelSpecified => {
|
||||
status = CommandErrorStatus::BadRequest;
|
||||
error_type = EnqueueInpaintImageErrorType::ModelNotSpecified;
|
||||
error_message = "No model specified for image generation".to_string();
|
||||
}
|
||||
InternalImageInpaintError::NoProviderAvailable => {
|
||||
status = CommandErrorStatus::ServerError;
|
||||
error_type = EnqueueInpaintImageErrorType::NoProviderAvailable;
|
||||
error_message = "No configured provider available for image generation".to_string();
|
||||
}
|
||||
InternalImageInpaintError::NoSourceImageSpecified => {
|
||||
status = CommandErrorStatus::BadRequest;
|
||||
error_type = EnqueueInpaintImageErrorType::NoSourceImageSpecified;
|
||||
error_message = "No source image was provided".to_string();
|
||||
}
|
||||
InternalImageInpaintError::NoMaskImageSpecified => {
|
||||
status = CommandErrorStatus::BadRequest;
|
||||
error_type = EnqueueInpaintImageErrorType::NoMaskImageSpecified;
|
||||
error_message = "No mask image was provided".to_string();
|
||||
}
|
||||
InternalImageInpaintError::MaskMediaTokenAndBytesSupplied => {
|
||||
status = CommandErrorStatus::BadRequest;
|
||||
error_type = EnqueueInpaintImageErrorType::MultipleMaskImagesSpecified;
|
||||
error_message = "multiple mask images provided".to_string();
|
||||
}
|
||||
InternalImageInpaintError::CouldNotDetermineMaskMimeType => {
|
||||
status = CommandErrorStatus::BadRequest;
|
||||
error_type = EnqueueInpaintImageErrorType::BadMaskImage;
|
||||
error_message = "bad mask image (mime)".to_string();
|
||||
}
|
||||
InternalImageInpaintError::CouldNotEncodeMask => {
|
||||
status = CommandErrorStatus::BadRequest;
|
||||
error_type = EnqueueInpaintImageErrorType::BadMaskImage;
|
||||
error_message = "bad mask image (encode)".to_string();
|
||||
}
|
||||
InternalImageInpaintError::InvalidNumberOfRequestedImages { min, max, requested } => {
|
||||
status = CommandErrorStatus::BadRequest;
|
||||
error_type = EnqueueInpaintImageErrorType::BadRequest;
|
||||
error_message = format!("Invalid number of images requested ({}). Must be between {} and {}", requested, min, max);
|
||||
|
||||
}
|
||||
InternalImageInpaintError::InvalidNumberOfInputImages { min, max, requested } => {
|
||||
status = CommandErrorStatus::BadRequest;
|
||||
error_type = EnqueueInpaintImageErrorType::BadRequest;
|
||||
error_message = format!("Invalid number of input images ({}). Must be between {} and {}", requested, min, max);
|
||||
}
|
||||
InternalImageInpaintError::AnyhowError(_) => {}
|
||||
InternalImageInpaintError::StorytellerError(_) => {}
|
||||
InternalImageInpaintError::NeedsStorytellerCredentials => {}
|
||||
}
|
||||
|
||||
CommandErrorResponseWrapper {
|
||||
status,
|
||||
error_message: Some(error_message.to_string()),
|
||||
error_type: Some(error_type),
|
||||
error_details: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
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_edit_artcraft::handle_gpt_image_1_edit_artcraft;
|
||||
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_sora::handle_gpt_image_1_edit_sora;
|
||||
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
|
||||
use crate::core::commands::enqueue::image_inpaint::errors::InternalImageInpaintError;
|
||||
use crate::core::commands::enqueue::image_inpaint::flux_pro_1_inpaint::handle_flux_pro_1_inpaint_artcraft::handle_flux_pro_1_inpaint_artcraft;
|
||||
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::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 log::info;
|
||||
use tauri::AppHandle;
|
||||
|
||||
pub(super) const MAX_IMAGES: usize = 10;
|
||||
|
||||
pub async fn handle_flux_pro_1_inpaint(
|
||||
request: &EnqueueInpaintImageCommand,
|
||||
app: &AppHandle,
|
||||
app_data_root: &AppDataRoot,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
provider_priority_store: &ProviderPriorityStore,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
fal_creds_manager: &FalCredentialManager,
|
||||
fal_task_queue: &FalTaskQueue,
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
sora_task_queue: &SoraTaskQueue,
|
||||
) -> Result<TaskEnqueueSuccess, InternalImageInpaintError> {
|
||||
|
||||
let priority = provider_priority_store.get_priority()?;
|
||||
|
||||
// TODO: Check if providers are available before proceeding.
|
||||
|
||||
info!("Providers by priority: {:?}", priority);
|
||||
|
||||
for provider in priority.iter() {
|
||||
match provider {
|
||||
Provider::Fal => {
|
||||
// Fallthrough (for now)
|
||||
}
|
||||
Provider::Sora => {
|
||||
// Fallthrough
|
||||
}
|
||||
Provider::Artcraft => {
|
||||
info!("Dispatching flux pro 1 inpaint via Artcraft...");
|
||||
return handle_flux_pro_1_inpaint_artcraft(
|
||||
request,
|
||||
app,
|
||||
app_data_root,
|
||||
app_env_configs,
|
||||
storyteller_creds_manager
|
||||
).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(InternalImageInpaintError::NoProviderAvailable)
|
||||
}
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
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_bg_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalCommand;
|
||||
use crate::core::commands::enqueue::image_bg_removal::errors::InternalBgRemovalError;
|
||||
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize, EnqueueContextualEditImageCommand};
|
||||
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
|
||||
use crate::core::commands::enqueue::image_inpaint::errors::InternalImageInpaintError;
|
||||
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, 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;
|
||||
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::ProviderPriorityStore;
|
||||
use crate::core::utils::save_base64_image_to_temp_dir::save_base64_image_to_temp_dir;
|
||||
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 anyhow::anyhow;
|
||||
use artcraft_api_defs::generate::image::edit::gpt_image_1_edit_image::{GptImage1EditImageImageQuality, GptImage1EditImageImageSize, GptImage1EditImageNumImages, GptImage1EditImageRequest};
|
||||
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::inpaint::flux_pro_1_inpaint_image::{FluxPro1InpaintImageNumImages, FluxPro1InpaintImageRequest};
|
||||
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;
|
||||
use images::encoding::image_bytes_to_png_bytes::image_bytes_to_png_bytes;
|
||||
use images::encoding::image_bytes_to_png_bytes_with_dimensions::image_bytes_to_png_bytes_with_dimensions;
|
||||
use images::mask_images::normalize_image_bytes_to_flux_mask::normalize_image_bytes_to_flux_mask;
|
||||
use log::{error, info};
|
||||
use mimetypes::mimetype_info::mimetype_info::MimetypeInfo;
|
||||
use storyteller_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use storyteller_client::generate::image::edit::gpt_image_1_edit_image::gpt_image_1_edit_image;
|
||||
use storyteller_client::generate::image::generate_flux_1_dev_text_to_image::generate_flux_1_dev_text_to_image;
|
||||
use storyteller_client::generate::image::generate_flux_1_schnell_text_to_image::generate_flux_1_schnell_text_to_image;
|
||||
use storyteller_client::generate::image::generate_flux_pro_11_text_to_image::generate_flux_pro_11_text_to_image;
|
||||
use storyteller_client::generate::image::generate_flux_pro_11_ultra_text_to_image::generate_flux_pro_11_ultra_text_to_image;
|
||||
use storyteller_client::generate::image::inpaint::flux_pro_1_inpaint_image::flux_pro_1_inpaint_image;
|
||||
use storyteller_client::media_files::get_media_file::get_media_file;
|
||||
use storyteller_client::media_files::upload_image_media_file_from_bytes::{upload_image_media_file_from_bytes, ImageType, UploadImageBytesArgs};
|
||||
use storyteller_client::media_files::upload_image_media_file_from_file::upload_image_media_file_from_file;
|
||||
use tauri::AppHandle;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
pub async fn handle_flux_pro_1_inpaint_artcraft(
|
||||
request: &EnqueueInpaintImageCommand,
|
||||
app: &AppHandle,
|
||||
app_data_root: &AppDataRoot,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
) -> Result<TaskEnqueueSuccess, InternalImageInpaintError> {
|
||||
|
||||
let creds = match storyteller_creds_manager.get_credentials()? {
|
||||
Some(creds) => creds,
|
||||
None => {
|
||||
return Err(InternalImageInpaintError::NeedsStorytellerCredentials);
|
||||
},
|
||||
};
|
||||
|
||||
let image_media_token = match &request.image_media_token {
|
||||
Some(token) => token.clone(),
|
||||
None => {
|
||||
return Err(InternalImageInpaintError::NoSourceImageSpecified);
|
||||
},
|
||||
};
|
||||
|
||||
//let image_media_file = get_media_file(
|
||||
// &app_env_configs.storyteller_host,
|
||||
// &image_media_token,
|
||||
//).await.map_err(|err| {
|
||||
// error!("Failed to get media file for image: {:?}", err);
|
||||
// InternalImageInpaintError::StorytellerError(err)
|
||||
//})?;
|
||||
|
||||
let mask_media_token = get_mask(
|
||||
request,
|
||||
app_env_configs,
|
||||
&creds,
|
||||
).await?;
|
||||
|
||||
info!("Calling Artcraft flux pro 1 inpaint ...");
|
||||
|
||||
let uuid_idempotency_token = generate_random_uuid();
|
||||
|
||||
let num_images = match request.image_count {
|
||||
None => None,
|
||||
Some(1) => Some(FluxPro1InpaintImageNumImages::One),
|
||||
Some(2) => Some(FluxPro1InpaintImageNumImages::Two),
|
||||
Some(3) => Some(FluxPro1InpaintImageNumImages::Three),
|
||||
Some(4) => Some(FluxPro1InpaintImageNumImages::Four),
|
||||
Some(other) => {
|
||||
return Err(InternalImageInpaintError::InvalidNumberOfRequestedImages {
|
||||
min: 1,
|
||||
max: 4,
|
||||
requested: other,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
let request = FluxPro1InpaintImageRequest {
|
||||
uuid_idempotency_token,
|
||||
prompt: Some(request.prompt.clone()),
|
||||
image_media_token,
|
||||
mask_media_token,
|
||||
num_images,
|
||||
};
|
||||
|
||||
let result = flux_pro_1_inpaint_image(
|
||||
&app_env_configs.storyteller_host,
|
||||
Some(&creds),
|
||||
request,
|
||||
).await;
|
||||
|
||||
let job_id = match result {
|
||||
Ok(enqueued) => {
|
||||
// TODO(bt,2025-07-05): Enqueue job token?
|
||||
info!("Successfully enqueued Artcraft flux pro 1 inpaint. Job token: {}",
|
||||
enqueued.inference_job_token);
|
||||
enqueued.inference_job_token
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to use Artcraft flux pro 1 inpaint: {:?}", err);
|
||||
return Err(InternalImageInpaintError::StorytellerError(err));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(TaskEnqueueSuccess {
|
||||
provider: GenerationProvider::Artcraft,
|
||||
model: Some(GenerationModel::FluxProKontextMax),
|
||||
provider_job_id: Some(job_id.to_string()),
|
||||
task_type: TaskType::ImageGeneration,
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_mask(
|
||||
request: &EnqueueInpaintImageCommand,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
storyteller_creds: &StorytellerCredentialSet,
|
||||
) -> Result<MediaFileToken, InternalImageInpaintError> {
|
||||
|
||||
if request.mask_image_media_token.is_some() && request.mask_image_raw_bytes.is_some() {
|
||||
return Err(InternalImageInpaintError::MaskMediaTokenAndBytesSupplied);
|
||||
}
|
||||
|
||||
if let Some(token) = request.mask_image_media_token.as_ref() {
|
||||
return Ok(token.clone());
|
||||
};
|
||||
|
||||
let image_bytes = request.mask_image_raw_bytes.as_ref()
|
||||
.ok_or(InternalImageInpaintError::NoMaskImageSpecified)?;
|
||||
|
||||
let image_bytes = normalize_image_bytes_to_flux_mask(image_bytes)
|
||||
.map_err(|err| {
|
||||
error!("Failed to convert image bytes to png: {:?}", err);
|
||||
InternalImageInpaintError::CouldNotEncodeMask
|
||||
})?;
|
||||
|
||||
info!("Uploading image media file from bytes...");
|
||||
|
||||
let result = upload_image_media_file_from_bytes(UploadImageBytesArgs {
|
||||
api_host: &app_env_configs.storyteller_host,
|
||||
maybe_creds: Some(&storyteller_creds),
|
||||
image_bytes: image_bytes.0,
|
||||
image_type: ImageType::Png,
|
||||
is_intermediate_system_file: true,
|
||||
}).await
|
||||
.map_err(|err| {
|
||||
error!("Failed to upload image media file: {:?}", err);
|
||||
InternalImageInpaintError::StorytellerError(err)
|
||||
})?;
|
||||
|
||||
Ok(result.media_file_token)
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
pub (super) mod handle_flux_pro_1_inpaint;
|
||||
mod handle_flux_pro_1_inpaint_artcraft;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user