Compare commits

...
Author SHA1 Message Date
CrossPr0duct 6c96f21bd7 Merge branch 'main' into feature/bugfixes 2025-06-11 13:42:28 -04:00
CrossPr0duct 245409de08 Fixed one off issue. 2025-06-11 13:25:27 -04:00
CrossPr0duct 90d0f15176 Wired ContextMenu debugging small issue. 2025-06-11 13:06:07 -04:00
CrossPr0duct 257db9b892 render order is now correct. 2025-06-10 20:29:52 -04:00
CrossPr0duct 70348a922a Z indicies for paint seem to be ignored. 2025-06-10 17:39:10 -04:00
CrossPr0duct 95e724e4bd Fixed outline problem 2025-06-10 12:37:20 -04:00
BFlat df0b98ec63 drag and drop gallery 3d objects to 3d scene feature (#666)
- can now drag and drop 3d models onto the editor scene from the gallery
modal
2025-06-10 12:48:44 +07:00
BFlat f214577af1 2D canvas gallery image drop feature (#665)
- now you can drag and drop onto the 2D canvas from the gallery modal
2025-06-10 12:24:28 +07:00
ArEnScandCrossPr0duct 3efe9db4ca Create Menus and Bug Fixes (#664)
Removed:
Save and Load Scene for later.
Added undo and redo as button.
Hide Generate Image
Added a menu ( still needs logic code )
Fixed Opacity on brush.

---------

Co-authored-by: CrossPr0duct <alicextensor@gmail.com>
2025-06-09 17:36:16 -04:00
CrossPr0duct 21ea7d05e2 Fixed brush 2025-06-09 17:07:16 -04:00
CrossPr0duct e8305bd112 Add in redo and undo and hide a few components. 2025-06-09 14:55:07 -04:00
Brandon Thomas 8208206e5a missing 2025-06-09 12:20:21 -04:00
Tanish Manku 4f33e74e3a Make 3D state save and load on tab change (#661) 2025-06-06 22:50:47 -07:00
Brandon Thomas f75b210dcd end-to-end fal in artcraft backend (#663) 2025-06-06 21:37:06 -04:00
Brandon ThomasandMichael Chung 7a07f196d1 Fix Safari issue (#662)
Co-authored-by: Michael Chung <michaelchung@mac.home>
2025-06-06 20:26:30 -04:00
a609282d8a Feature/page draw (#659)
Co-authored-by: CrossPr0duct <alicextensor@gmail.com>
Co-authored-by: BFlat <bombayapirat@gmail.com>
2025-06-06 12:03:09 -04:00
BFlat 0a32e06cd5 reusable model selector (#660)
- reusable model selector component
- uses zustand with separated states for each tab

Image model list:
- GPT Image 1 (GPT-4o)
- Flux Pro Ultra
- Recraft 3
- Flux.1 Kontext

Video model list:
- Kling 2.0
- Runway
2025-06-06 17:31:36 +07:00
Brandon Thomas f1563feb91 codegen db 2025-06-05 09:24:06 -04:00
Brandon Thomas cc73ffab5d mark fal jobs complete (#658) 2025-06-05 09:19:38 -04:00
Brandon Thomas d374d9339c codegen db 2025-06-05 00:20:34 -04:00
Brandon Thomas 53681eac3f hunyuan2 image-to-3d (#657) 2025-06-05 00:15:13 -04:00
Brandon Thomas f22369ea4a update artcraft website with download links 2025-06-04 22:01:22 -04:00
Brandon Thomas f7060c0278 classify cloudflare errors 2025-06-04 20:33:27 -04:00
Brandon Thomas 2af8fbd9e1 npm install verbose flags 2025-06-04 20:10:05 -04:00
BFlat a4eb74292f login and popup modals (#652)
- moved artcraft login to first step before sora login
- made sora login skippable
- added pop up to block generation if sora session is not found
2025-06-04 19:47:27 -04:00
Tanish Manku 78b8c56066 Make 2D, 3D, image, video conditionally render (#654) 2025-06-04 14:47:39 -07:00
Brandon Thomas 221aa347bf forgot to commit these 2025-06-04 09:31:34 -04:00
Brandon Thomas 14a27ee803 fal endpoints: kling video 1.6 (#656) 2025-06-04 09:27:26 -04:00
Brandon Thomas 36e35dab0b fal on the backend (#653) 2025-06-03 14:59:28 -04:00
Brandon Thomas cb868c7275 add migrations and materialized final schema 2025-06-02 18:00:04 +05:30
Brandon Thomas b756f81421 vendor in fal-rs so we can hack in webhook support (then upstream it) (#650) 2025-06-02 01:43:18 -04:00
Brandon Thomas 0a1ad3d814 less flux-y generated images as background 2025-06-01 19:13:09 -04:00
Brandon Thomas 9df8c981cd fix api docs 2025-06-01 16:16:50 -04:00
Brandon Thomas e7622318c4 deprecate old endpoints (don't remove yet) (#649) 2025-06-01 15:51:37 -04:00
Brandon Thomas 352c7517cc openart client (maybe we won't do this just yet) (#648) 2025-05-30 19:39:41 -04:00
bflatastic 010a2f043a Remove old unused SettingsModal component 2025-05-30 16:09:25 +07:00
bflatastic cbb32f8275 Fix vertical line color in AssetModal and SettingsModal 2025-05-30 16:08:45 +07:00
bflatastic 0aecc97ede Update TopBar component: correct label for video tab from "Prompt to Video" to "Image to Video" 2025-05-30 15:44:43 +07:00
bflatastic 72a1bb07e0 Update command in windows_frontend_dev.ps1 to use 'npx' for executing nx dev 2025-05-30 13:05:06 +07:00
Brandon Thomas 6a01e4bba0 we don't need to spam the logs with the challenge html 2025-05-30 00:22:01 -04:00
Brandon Thomas 90d741323c check cloudflare challenges 2025-05-30 00:03:48 -04:00
Brandon Thomas 066d1c8bfb emit version to logging, add old sora cookie name 2025-05-29 21:38:07 -04:00
Brandon Thomas 3536736f53 better logging of sora session issues 2025-05-29 20:42:33 -04:00
Brandon Thomas 76d1f65164 more better alerts (#647) 2025-05-29 05:28:16 -04:00
Brandon Thomas ab72551ee7 better events (#646) 2025-05-29 05:03:32 -04:00
Brandon Thomas 9cf87cda5b start phasing fields out from our OpenAI client. These are a liability
It's smarter to just dump the fields we don't care about. That is less
of a chance that we got things wrong or things become incongruent at
runtime in the field.
2025-05-28 20:26:07 -04:00
Brandon Thomas 5163f357af fix nullable type in sora api 2025-05-28 12:15:08 -04:00
BFlat 42dfa9263d artcraft site landing update (#645) 2025-05-28 15:14:35 +07:00
Brandon Thomas f4d9385668 Fix package.lock (#644) 2025-05-28 01:20:09 -04:00
Brandon Thomas 189d6a6df8 refactor response types 2025-05-27 23:37:34 -04:00
Brandon Thomas 89cdc8492f tabs refactor (#643) 2025-05-27 22:54:04 -04:00
BFlat 46d2944823 background gallery for image tab (#638)
- reduced image size
- a more performant css only animation
2025-05-27 22:25:27 -04:00
ArEnScandMichael Chung 216e091257 Fixing build issue with button-link. (#642)
Co-authored-by: Michael Chung <michaelchung@mac.home>
2025-05-27 19:27:24 -04:00
Brandon Thomas f8bfa3ab2b fix nx builds (#641) 2025-05-27 18:58:47 -04:00
Brandon Thomas 569622bcc0 make frontend/clean_modules.sh take the nuclear option 2025-05-27 16:52:12 -04:00
Brandon Thomas 7aad79d16b timestamp when build finishes 2025-05-27 15:14:45 -04:00
Brandon Thomas d70f8ac41e fix circular deps in nx (#639) 2025-05-27 14:25:57 -04:00
ArEnSc 7097144e19 Removals (#636)
Co-authored-by: CrossPr0duct <alicextensor@gmail.com>
Co-authored-by: Michael Chung <michaelchung@mac.home>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Tanish Manku <tanish2k09.dev@gmail.com>
Good work every one =)
2025-05-27 12:00:35 -04:00
Brandon Thomas 709af560a5 enqueue gpt-4o images too 2025-05-26 23:46:05 -04:00
Brandon Thomas 0f1cf93252 add fal image generation end-to-end (recraft, etc.) (#637) 2025-05-26 21:33:21 -04:00
bflatastic 4a10581e9a fix drag and drop logic for 3D and 2D tabs in PageEditor component 2025-05-26 23:31:47 +07:00
bflatastic 447a70b890 differentiate tabs to drop amd set up place for drag and drop on 2d 2025-05-26 23:26:13 +07:00
Brandon Thomas f4e998d945 cleanup deps (#635) 2025-05-26 01:38:27 -04:00
Brandon Thomas a954859ac2 cleanup dead dependencies 2025-05-25 23:55:15 -04:00
Brandon Thomas 2d8e835401 standardize actix versions in root Cargo.toml 2025-05-25 23:47:12 -04:00
Brandon Thomas c23380af90 codegen db 2025-05-25 23:22:10 -04:00
Brandon Thomas c34f75d071 cleanup hakari packages 2025-05-25 23:15:32 -04:00
Brandon Thomas 8a886e9033 cleanup scripts (2) 2025-05-25 22:57:01 -04:00
Brandon Thomas 3101df8211 cleanup scripts 2025-05-25 22:53:40 -04:00
Brandon Thomas a4c264ebbf cleanup packages, fixup storyteller-web 2025-05-25 22:50:45 -04:00
Brandon Thomas 3d807fdeb2 refactor database crates 2025-05-25 22:33:18 -04:00
Brandon Thomas b3a62cce64 refactor packages 2025-05-25 22:09:59 -04:00
Brandon Thomas 42ccc25e4d fix video enqueue 2025-05-25 21:31:56 -04:00
Brandon Thomas 6595518b35 add detailed build info in an about settings pane (#634) 2025-05-25 14:45:50 -04:00
ArEnSc 618cbc8f0e Feature/clean (#633)
Clean up the TS work unify and the app
2025-05-24 23:31:25 -04:00
BFlat b31a152c1a text to image tab (commented out) (#632) 2025-05-24 10:57:28 +07:00
BFlat 0e2169b902 updated topbar menu icons with tooltip (#631)
- animated on hover
- to accommodate more pages
2025-05-23 23:20:09 +07:00
Michael Chung 0ee9b7c442 fix: remove DialogTitle from DialogEditText 2025-05-23 11:33:48 -04:00
bflatastic 17495276e7 Refactor GalleryModal refresh logic and add refresh button
- Simplified the refresh logic by extracting it into a separate `refreshGallery` function.
- Added a refresh button with a tooltip to allow users to manually refresh the gallery items.
- Updated the useEffect to call the new refresh function when the modal opens, ensuring the gallery is always up-to-date.
2025-05-23 20:47:29 +07:00
bflatastic 2d78fd5a6f library always refreshed on open 2025-05-23 20:39:40 +07:00
BFlat 98e513e917 gallery dnd code refactor (#630)
- cleaner code + reusable image drop function for later 2d image drop
implementation
2025-05-23 09:21:43 +07:00
ArEnScandMichael Chung 97eb1c65ca Fix cors allow all urls for now, since there isn't really a way to ha… (#628)
…ck the app lol.

Co-authored-by: Michael Chung <michaelchung@mac.home>
2025-05-22 18:43:59 -04:00
bflatastic ba82a438dd image not dropping at correct position fix 2025-05-23 02:11:54 +07:00
bflatastic de7b990ec5 bug fix: select mode library modal not closing on X click 2025-05-23 01:35:50 +07:00
BFlat 7b90c59675 library modal signals bug fix (#627)
- fixed weird behavior with library modal not opening
- image drag and drop more accurate position (production drag and drop
still doesnt work - looking into it)
2025-05-23 01:31:49 +07:00
Brandon Thomas 11f1a5f41a sora aspect ratios (#626) 2025-05-22 00:09:59 -04:00
BFlat b92cd58f81 Enhance GalleryModal UX with Filters, Drag-and-Drop, Pagination, and UI Improvements (#625)
**✨ Features & Enhancements:**
- GalleryModal Filters: Replaced tab-based navigation with a more
scalable filter system.
- Image Fit Toggle: Allows toggling image fit display mode for better
previewing.
- Pagination & Infinite Scroll: Improved item loading performance using
soft refresh and infinite scroll logic.
- Drag-and-drop setup for gallery items
- Prevented dragging for video items
- Added support for image drop in PageEditor (3D)
- "My Library" Label Updates
2025-05-21 23:29:06 +07:00
Brandon Thomas 914c26b4c6 classify gateway timeout 2025-05-21 11:04:32 -04:00
Brandon Thomas fe8939a0f6 incomplete runwayml implementation (pausing for now) (#624) 2025-05-20 23:39:34 -04:00
Tanish Manku c809ff88fa Add save/load support for character poses (#623) 2025-05-20 20:13:04 -04:00
bflatastic 86675568a8 Update modal components for improved drag functionality and styling
- Increased the height of the drag handle in AssetModal and GalleryModal for better usability.
- Adjusted z-index properties for elements within AssetModal to ensure proper layering.
- Enhanced the modal's drag behavior to allow overflow beyond viewport edges, improving user experience when repositioning.
2025-05-21 02:32:20 +07:00
bflatastic a8927cc78d Refactor expand button in modal component for improved styling and functionality
- Updated the ExpandButton component to accept size props for better responsiveness.
- Replaced SVG icons with FontAwesome icons for a more consistent design.
- Adjusted the GalleryModal layout to include a gap between buttons for enhanced usability.
2025-05-21 02:17:17 +07:00
bflatastic 0e8dd19ecf Enhance modal functionality with draggable support and expand button
- Added draggable functionality to the modal, allowing users to reposition it.
- Introduced an expand button to toggle full-screen mode for the modal.
- Updated modal backdrop to support interaction options and improved styling.
- Adjusted AssetModal and GalleryModal components to utilize new modal features.
2025-05-21 02:06:51 +07:00
bflatastic b32a2e79b9 promptbox height bug fix 2025-05-20 22:47:38 +07:00
bflatastic a662c5809d lightbox videos fix 2025-05-20 22:31:27 +07:00
Michael Chung 7498720674 Fixing the gallery modal to show videos and images. 2025-05-20 11:14:36 -04:00
ArEnScandMichael Chung c8a4490b52 Feature/add to scene video 2 (#621)
Co-authored-by: Michael Chung <michaelchung@michaels-mbp-2.home>
2025-05-20 09:22:46 -04:00
Brandon Thomas c4e30e6dc7 swap out the background (#620) 2025-05-19 23:01:33 -04:00
ArEnScandMichael Chung 12070f14ad Feature/add to scene video 2 (#619)
Adding videos to scene via signal.

---------

Co-authored-by: Michael Chung <michaelchung@michaels-mbp-2.home>
2025-05-19 02:19:31 -04:00
BFlat 77a103de06 gallery videos tab (#617)
- 16/9 ratio grid items
- lightbox for videos will only show download button
2025-05-19 10:58:34 +07:00
BFlat 82f5805f24 image-to-video generation page with background gallery and UI components (#616) 2025-05-19 09:51:52 +07:00
f7c9ebfaa3 Feature/video addition (#615)
Co-authored-by: Michael Chung <michaelchung@Michaels-MacBook-Pro-2.local>
Co-authored-by: Michael Chung <michaelchung@michaels-mbp-2.home>
2025-05-18 20:38:12 -04:00
Brandon Thomas 76cd00cc13 add fal hunyuan image-to-3d (#614) 2025-05-18 15:37:55 -04:00
Brandon Thomas cb64f45b48 refactor artcraft crates to organize by service (module reorg) (#613) 2025-05-18 00:53:00 -04:00
Brandon Thomas 6751ab0b60 fal video integration (#612) 2025-05-17 19:47:40 -04:00
ArEnScandMichael Chung 9827a3efc5 Feature/add scene (#611)
Co-authored-by: Michael Chung <michaelchung@michaels-mbp-2.home>
2025-05-15 22:09:38 -04:00
Brandon Thomas 93b53d84cd remove some dead code + fix missing "dev" assets (#610) 2025-05-15 18:32:03 -04:00
Brandon Thomas a7794f6338 don't need to build for editor2d anymore 2025-05-15 17:20:56 -04:00
ArEnScandMichael Chung ef03e292c3 Add scene to 2d Lightbox modal. (#609)
Co-authored-by: Michael Chung <michaelchung@michaels-mbp-2.home>
2025-05-14 12:11:19 -04:00
BFlat 0e8c1fa3e7 Update landing2 mobile videos fix 2025-05-14 12:07:07 +07:00
BFlat af08834de7 artcraft landing 2 (#608) 2025-05-14 02:28:05 +07:00
Brandon Thomas e728806c04 more csp fixes 2025-05-13 14:18:29 -04:00
Brandon Thomas 3e605e7b18 add google analytics to CSP 2025-05-13 14:03:31 -04:00
Brandon Thomas d244524abc fal api integration in tauri + frontend controls + background removal (#607) 2025-05-13 13:12:49 -04:00
bflatastic 8e455e27a9 fixed: Artcraft login in onboarding shown when already logged into Artcraft 2025-05-13 11:55:44 +07:00
Brandon Thomas e0df1644a5 switch order of 2d/3d 2025-05-12 19:11:29 -04:00
Brandon Thomas 736d033ebf status alerts on 2d side 2025-05-12 18:54:42 -04:00
Brandon Thomas af3f0baa63 handle sora logged out state after app starts 2025-05-12 18:44:54 -04:00
3086 changed files with 137868 additions and 38289 deletions
@@ -0,0 +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 ",
"describe": {
"columns": [],
"parameters": {
"Right": 13
},
"nullable": []
},
"hash": "0fa824822cd94024dce21243f41e69e4a3e8685c4de676834a7c0ee14982c976"
}
@@ -0,0 +1,113 @@
{
"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 ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "status: enums::common::job_status_plus::JobStatusPlus",
"type_info": {
"type": "String",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | ENUM",
"char_set": 224,
"max_size": 76
}
},
{
"ordinal": 2,
"name": "external_third_party: enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 3,
"name": "external_third_party_id",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 256
}
},
{
"ordinal": 4,
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 5,
"name": "maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 6,
"name": "creator_ip_address",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 7,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 8,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false,
true,
true,
true,
true,
false,
false,
false
]
},
"hash": "64dfacc1f0552d539f009fea91e902a2fe296c06ad539808596bbf2c8fc9eaa0"
}
@@ -48,7 +48,7 @@
"name": "maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
@@ -1,12 +1,12 @@
{
"db_name": "MySQL",
"query": "\n INSERT INTO media_files\n SET\n token = ?,\n\n media_class = ?,\n media_type = ?,\n\n is_user_upload = ?,\n is_intermediate_system_file = ?,\n\n origin_category = ?, \n origin_product_category = ?, \n maybe_origin_model_type = ?, \n maybe_origin_model_token = ?, \n maybe_origin_filename = ?,\n\n is_batch_generated = ?,\n maybe_batch_token = ?,\n\n maybe_mime_type = ?,\n file_size_bytes = ?,\n maybe_duration_millis = ?,\n maybe_audio_encoding = ?, \n maybe_video_encoding = ?, \n maybe_frame_width = ?, \n maybe_frame_height = ?,\n maybe_prompt_token = ?,\n checksum_sha2 = ?,\n\n maybe_title = ?,\n maybe_text_transcript = ?,\n\n maybe_scene_source_media_file_token = ?,\n\n public_bucket_directory_hash = ?, \n maybe_public_bucket_prefix = ?, \n maybe_public_bucket_extension = ?, \n\n maybe_creator_user_token = ?, \n maybe_creator_anonymous_visitor_token = ?, \n\n creator_ip_address = ?, \n creator_set_visibility = ?, \n\n maybe_creator_file_synthetic_id = ?, \n maybe_creator_category_synthetic_id = ?,\n\n extra_file_modification_info = ?,\n\n maybe_mod_user_token = ?, \n is_generated_on_prem = ?, \n generated_by_worker = ?, \n generated_by_cluster = ?\n ",
"query": "\n INSERT INTO media_files\n SET\n token = ?,\n\n media_class = ?,\n media_type = ?,\n\n is_user_upload = ?,\n is_intermediate_system_file = ?,\n\n origin_category = ?, \n origin_product_category = ?, \n maybe_origin_model_type = ?, \n maybe_origin_model_token = ?, \n maybe_origin_filename = ?,\n\n is_batch_generated = ?,\n maybe_batch_token = ?,\n\n maybe_mime_type = ?,\n file_size_bytes = ?,\n maybe_duration_millis = ?,\n maybe_audio_encoding = ?, \n maybe_video_encoding = ?, \n maybe_frame_width = ?, \n maybe_frame_height = ?,\n maybe_prompt_token = ?,\n checksum_sha2 = ?,\n \n maybe_engine_category = ?,\n\n maybe_title = ?,\n maybe_text_transcript = ?,\n\n maybe_scene_source_media_file_token = ?,\n\n public_bucket_directory_hash = ?, \n maybe_public_bucket_prefix = ?, \n maybe_public_bucket_extension = ?, \n\n maybe_creator_user_token = ?, \n maybe_creator_anonymous_visitor_token = ?, \n\n creator_ip_address = ?, \n creator_set_visibility = ?, \n\n maybe_creator_file_synthetic_id = ?, \n maybe_creator_category_synthetic_id = ?,\n\n extra_file_modification_info = ?,\n\n maybe_mod_user_token = ?, \n is_generated_on_prem = ?, \n generated_by_worker = ?, \n generated_by_cluster = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 38
"Right": 39
},
"nullable": []
},
"hash": "c58f414b5e4d0f0420d1894e2f40311e5613919f49f3ac91f81f5fc2bef1bea8"
"hash": "b4470e607c005421dd8504b9f1d695080a581936ac6ce00b6f437b2764e741be"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nUPDATE generic_inference_jobs\nSET\n status = ?,\n on_success_result_entity_type = ?,\n on_success_result_entity_token = ?,\n failure_reason = NULL,\n internal_debugging_failure_reason = NULL,\n success_execution_millis = NULL,\n success_inference_execution_millis = NULL,\n retry_at = NULL,\n successfully_completed_at = NOW()\nWHERE token = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
},
"hash": "f68a8917e639418a0bb952bef606c96299001e143c745ee30df99b72d1235d35"
}
+13 -4
View File
@@ -1,6 +1,15 @@
{
"files.exclude": {
"**/*~": true,
"frontend_old/**": true
}
"files.exclude": {
"**/*~": true,
".sqlx/**": true,
"_elasticsearch/**": true,
"_sql/**": true,
"frontend_old/**": true,
"includes/**": true,
"target/**": true,
"test_data/**": true,
"tmp/**": true,
"virtualenv/**": true,
"windows_dlls/**": true,
}
}
Generated
+555 -2877
View File
File diff suppressed because it is too large Load Diff
+65 -32
View File
@@ -4,13 +4,6 @@ resolver = "2"
# This block defines the workspace members.
# When we add a library or binary crate, it needs an entry here.
members = [
# Types: common strongly-typed tokens, ids, payloads, etc.
"crates/_types/composite_identifiers",
"crates/_types/enums",
"crates/_types/enums_public",
"crates/_types/reusable_types", # TODO(bt, 2022-12-21): Deprecate
"crates/_types/tokens",
# CLI tools for operations, etc.
"crates/cli/devenv/dev-database-seed",
"crates/cli/devenv/dev-upload-media-file",
@@ -30,13 +23,17 @@ members = [
"crates/lib/actix_cors_configs",
"crates/lib/actix_helpers",
"crates/lib/api_clients/cloud_storage",
"crates/lib/api_clients/fal_client",
"crates/lib/api_clients/google_drive_common",
"crates/lib/api_clients/google_sign_in",
"crates/lib/api_clients/openai_sora_client",
"crates/lib/api_clients/openart_client",
"crates/lib/api_clients/runwayml_client",
"crates/lib/api_clients/storyteller_client",
"crates/lib/api_clients/thumbnail_generator",
"crates/lib/bootstrap",
"crates/lib/bucket_paths",
"crates/lib/build_metadata",
"crates/lib/caching/memory_caching",
"crates/lib/caching/memory_store",
"crates/lib/caching/redis_caching",
@@ -50,10 +47,10 @@ members = [
"crates/lib/deprecated/http_server_common", # TODO(bt, 2023-03-20): Move useful pieces to actix_helpers
"crates/lib/dimensions",
"crates/lib/easyenv", # NB: Vendored version of https://github.com/echelon/easyenv.rs that we'll improve.
"crates/lib/elasticsearch_schema",
"crates/lib/email_sender",
"crates/lib/errors",
"crates/lib/files/archives",
"crates/lib/files/file_to_base64_url",
"crates/lib/files/filesys",
"crates/lib/files/hashing",
"crates/lib/files/images",
@@ -63,16 +60,13 @@ members = [
"crates/lib/idempotency",
"crates/lib/image_algorithms",
"crates/lib/jobs_common",
"crates/lib/jwt_light",
"crates/lib/logging",
"crates/lib/machine_learning/ml_models",
"crates/lib/machine_learning/ml_weights_registry",
"crates/lib/machine_learning/prompt_engineering",
"crates/lib/markdown",
"crates/lib/migration",
"crates/lib/mysql_queries",
"crates/lib/password",
"crates/lib/redis_common",
"crates/lib/redis_schema",
"crates/lib/server_environment",
"crates/lib/shared_service_components",
"crates/lib/sqlx_mysql_helpers",
@@ -84,7 +78,22 @@ members = [
"crates/lib/user_input_common",
"crates/lib/web_scrapers",
#"crates/lib/api_clients/fakeyou_client",
#"crates/lib/sqlite_queries",
#"crates/lib/machine_learning/ml_models",
# Schema (database): queries, database layouts, Redis keys, etc.
"crates/schema/database/elasticsearch_schema",
"crates/schema/database/mysql_queries",
"crates/schema/database/redis_common",
"crates/schema/database/redis_schema",
#"crates/schema/database/sqlite_queries",
# Schema (public): common strongly-typed tokens, ids, payloads, etc.
"crates/schema/public/api/artcraft_api_defs",
"crates/schema/public/composite_identifiers",
"crates/schema/public/enums",
"crates/schema/public/enums_public",
"crates/schema/public/reusable_types", # TODO(bt, 2022-12-21): Deprecate
"crates/schema/public/tokens",
# Services: API servers, jobs, workers, etc.
"crates/service/job/analytics_job",
@@ -105,19 +114,23 @@ members = [
"crates/service/plugins/user_traits_component",
# Vendored code
"crates/vendor/candle/candle-core",
"crates/vendor/candle/candle-datasets",
"crates/vendor/candle/candle-examples",
"crates/vendor/candle/candle-flash-attn",
"crates/vendor/candle/candle-kernels",
"crates/vendor/candle/candle-metal-kernels",
"crates/vendor/candle/candle-nn",
"crates/vendor/candle/candle-onnx",
"crates/vendor/candle/candle-pyo3",
"crates/vendor/candle/candle-transformers",
"crates/vendor/candle/tensor-tools",
"crates/vendor/fal-rs",
"crates/vendor/fal-rs/fal-derive",
"crates/vendor/fal-rs/generate",
"crates/vendor/limitation",
"crates/vendor/tauri-plugin-http",
# NB(bt,2025-05-25): Too many failures on Mac. Disable for now.
#"crates/vendor/candle/candle-core",
#"crates/vendor/candle/candle-datasets",
#"crates/vendor/candle/candle-examples",
#"crates/vendor/candle/candle-flash-attn",
#"crates/vendor/candle/candle-kernels",
#"crates/vendor/candle/candle-metal-kernels",
#"crates/vendor/candle/candle-nn",
#"crates/vendor/candle/candle-onnx",
#"crates/vendor/candle/candle-pyo3",
#"crates/vendor/candle/candle-transformers",
#"crates/vendor/candle/tensor-tools",
# NB(bt,2023-12-12): Newrelic keeps breaking with upgrades. Kill for now.
#"crates/vendor/newrelic-telemetry-sdk-rust",
@@ -157,35 +170,55 @@ workspace-build-acceleration = { path = "crates/workspace-build-acceleration" }
# External libs
# Centrally pinned versions so that we can uniformly upgrade/downgrade versions across the workspace
# If a crate needs to use an older/newer version, the dependency should be managed in that crate's own Cargo.toml
anyhow = "1.0.97"
actix = "0.13.5"
actix-cors = "0.6.4" # TODO(bt): Carefully upgrade this. CORS broke last time.
actix-http = "3.7.0"
actix-multipart = "0.6.2"
actix-rt = "2.10.0"
actix-service = "2.0.3"
actix-utils = "3.0.1"
actix-web = "4.7.0" # TODO(bt): We're behind and need to bump versions.
actix-web-actors = "4.3.1+deprecated"
actix-web-lab = "0.20.2"
anyhow = "1.0.98"
base64 = "0.22.1"
chrono = { version = "0.4.40", features = ["serde"] }
bytes = "1.10.1"
chrono = { version = "0.4.41", features = ["serde"] }
cookie = "0.18.1"
futures = { version = "0.3.31" }
hex-literal = "1.0.0"
indexmap = "2.9.0"
log = "0.4.27"
once_cell = "1.21.3"
rand = "0.9.0"
rand = "0.9.1"
regex = "1.11.1"
reqwest = { version = "0.12.15", features = ["json", "cookies"] }
serde = { version = "1.0.219", features = ["derive"] }
serde_derive = "1.0.219"
serde_json = "1.0.140"
serde_yaml = "0.9.1"
serde_yaml = "0.9.34+deprecated"
sha3 = "0.10.8"
tauri = { version = "2.4.1", features = ["unstable", "devtools"] } # NB: "devtools" prevents App Store distribution
tempdir = "0.3.7"
tempfile = "3.20.0"
thiserror = "2.0.12"
toml = "0.8.20"
tokio = { version = "1.45.1", features = ["macros"] }
toml = "0.8.22"
url = { version = "2.5.4", features = ["serde"] }
# Internal libs
artcraft_api_defs = { path = "crates/schema/public/api/artcraft_api_defs" }
build_metadata = { path = "crates/lib/build_metadata" }
crockford = { path = "crates/lib/data/crockford" }
enums = { path = "crates/_types/enums" }
enums_public = { path = "crates/_types/enums_public" }
enums = { path = "crates/schema/public/enums" }
enums_public = { path = "crates/schema/public/enums_public" }
errors = { path = "crates/lib/errors" }
fal_client = { path = "crates/lib/api_clients/fal_client" }
file_to_base64_url = { path = "crates/lib/files/file_to_base64_url" }
filesys = { path = "crates/lib/files/filesys" }
idempotency = { path = "crates/lib/idempotency" }
images = { path = "crates/lib/files/images" }
jwt_light = { path = "crates/lib/jwt_light" }
memory_caching = { path = "crates/lib/caching/memory_caching" }
memory_store = { path = "crates/lib/caching/memory_store" }
mimetypes = { path = "crates/lib/files/mimetypes" }
@@ -194,4 +227,4 @@ primitives = { path = "crates/lib/data/primitives" }
shared_service_components = { path = "crates/lib/shared_service_components" }
storyteller_client = { path = "crates/lib/api_clients/storyteller_client" }
subprocess_common = { path = "crates/lib/subprocess_common" }
tokens = { path = "crates/_types/tokens" }
tokens = { path = "crates/schema/public/tokens" }
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE generic_inference_jobs DROP COLUMN maybe_external_third_party_id;
ALTER TABLE generic_inference_jobs DROP COLUMN maybe_external_third_party;
@@ -0,0 +1,9 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE generic_inference_jobs
ADD COLUMN maybe_external_third_party VARCHAR(16) DEFAULT NULL;
ALTER TABLE generic_inference_jobs
ADD COLUMN maybe_external_third_party_id VARCHAR(64) DEFAULT NULL;
@@ -0,0 +1,9 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
DROP INDEX `index_maybe_external_third_party_id`
ON `generic_inference_jobs`;
DROP INDEX `index_maybe_external_third_party_and_id`
ON `generic_inference_jobs`;
@@ -0,0 +1,11 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE generic_inference_jobs
ADD UNIQUE INDEX index_maybe_external_third_party_and_id (maybe_external_third_party, maybe_external_third_party_id),
ALGORITHM=INPLACE, LOCK=NONE;
ALTER TABLE generic_inference_jobs
ADD INDEX index_maybe_external_third_party_id (maybe_external_third_party_id),
ALGORITHM=INPLACE, LOCK=NONE;
@@ -39,8 +39,20 @@ CREATE TABLE generic_inference_jobs (
-- Examples (may not be up to date):
-- * text_to_speech
-- * voice_conversion
-- * image_generation
-- * video_generation
-- * background_removal
inference_category VARCHAR(32) NOT NULL,
-- If the job is externally enqueued, this is the third-party service
-- responsible for the job. Typically, we won't run an inference job for
-- jobs being run on a third party.
maybe_external_third_party VARCHAR(16) DEFAULT NULL,
-- If the job is externally enqueued, this is the ID the third-party
-- service uses.
maybe_external_third_party_id VARCHAR(64) DEFAULT NULL,
-- There is an index on this column.
-- NB: See notes on inference_category. This is becoming a problematic field. We're using
-- this to load the container spin-up arguments and dependencies, but the former is being
@@ -287,11 +299,13 @@ CREATE TABLE generic_inference_jobs (
PRIMARY KEY (id),
UNIQUE KEY (token),
UNIQUE KEY (uuid_idempotency_token),
UNIQUE KEY (maybe_external_third_party, maybe_external_third_party_id),
KEY index_job_type (job_type),
KEY index_product_category (product_category),
KEY index_inference_category (inference_category),
KEY index_maybe_model_type_and_maybe_model_token (maybe_model_type, maybe_model_token),
KEY index_maybe_model_type (maybe_model_type),
KEY index_maybe_external_third_party_id (maybe_external_third_party_id),
KEY fk_maybe_model_token (maybe_model_token),
KEY fk_maybe_input_source_token (maybe_input_source_token),
KEY fk_maybe_input_source_token_and_type (maybe_input_source_token, maybe_input_source_token_type),
@@ -16,23 +16,23 @@ path = "src/main.rs"
normal = ["workspace-build-acceleration"]
[dependencies]
# Workspace hack (faster compile times)
workspace-build-acceleration = { workspace = true }
# Internal
bucket_paths = { path = "../../../lib/bucket_paths" }
cloud_storage = { path = "../../../lib/api_clients/cloud_storage" }
config = { path = "../../../lib/deprecated/config" }
easyenv = { path = "../../../lib/easyenv" }
enums = { path = "../../../_types/enums" }
enums = { path = "../../../schema/public/enums" }
errors = { path = "../../../lib/errors" }
filesys = { path = "../../../lib/files/filesys" }
hashing = { path = "../../../lib/files/hashing" }
idempotency = { path = "../../../lib/idempotency" }
mimetypes = { path = "../../../lib/files/mimetypes" }
mysql_queries = { path = "../../../lib/mysql_queries" }
mysql_queries = { path = "../../../schema/database/mysql_queries" }
storyteller_root = { path = "../../../lib/storyteller_root" }
tokens = { path = "../../../_types/tokens" }
# Workspace hack (faster compile times)
workspace-build-acceleration = { version = "0.1", path = "../../../workspace-build-acceleration" }
tokens = { path = "../../../schema/public/tokens" }
# External
log = "0.4.14"
@@ -16,20 +16,20 @@ path = "src/main.rs"
normal = ["workspace-build-acceleration"]
[dependencies]
# Workspace hack (faster compile times)
workspace-build-acceleration = { workspace = true }
# Internal
bucket_paths = { path = "../../../lib/bucket_paths" }
cloud_storage = { path = "../../../lib/api_clients/cloud_storage" }
config = { path = "../../../lib/deprecated/config" }
easyenv = { path = "../../../lib/easyenv" }
enums = { path = "../../../_types/enums" }
enums = { path = "../../../schema/public/enums" }
errors = { path = "../../../lib/errors" }
filesys = { path = "../../../lib/files/filesys" }
mimetypes = { path = "../../../lib/files/mimetypes" }
mysql_queries = { path = "../../../lib/mysql_queries" }
tokens = { path = "../../../_types/tokens" }
# Workspace hack (faster compile times)
workspace-build-acceleration = { version = "0.1", path = "../../../workspace-build-acceleration" }
mysql_queries = { path = "../../../schema/database/mysql_queries" }
tokens = { path = "../../../schema/public/tokens" }
# External
clap = "4.4.8"
+2 -3
View File
@@ -16,14 +16,13 @@ path = "src/main.rs"
normal = ["workspace-build-acceleration"]
[dependencies]
# Workspace hack (faster compile times)
workspace-build-acceleration = { workspace = true }
# Internal
errors = { path = "../../../lib/errors" }
web_scrapers = { path = "../../../lib/web_scrapers" }
# Workspace hack (faster compile times)
workspace-build-acceleration = { version = "0.1", path = "../../../workspace-build-acceleration" }
# External
clap = "4.4.8"
tokio = { version = "1.38.0", features = ["macros"] }
+4 -4
View File
@@ -17,15 +17,15 @@ normal = ["workspace-build-acceleration"]
[dependencies]
# Workspace hack (faster compile times)
workspace-build-acceleration = { version = "0.1", path = "../../../workspace-build-acceleration" }
workspace-build-acceleration = { workspace = true }
# Internal
datetimes = { path = "../../../lib/data/datetimes" }
easyenv = { path = "../../../lib/easyenv" }
enums = { path = "../../../_types/enums" }
enums = { path = "../../../schema/public/enums" }
errors = { path = "../../../lib/errors" }
mysql_queries = { path = "../../../lib/mysql_queries" }
tokens = { path = "../../../_types/tokens" }
mysql_queries = { path = "../../../schema/database/mysql_queries" }
tokens = { path = "../../../schema/public/tokens" }
# External
anyhow = "1.0.86"
+3 -7
View File
@@ -16,20 +16,16 @@ path = "src/main.rs"
normal = ["workspace-build-acceleration"]
[dependencies]
# Workspace hack (faster compile times)
workspace-build-acceleration = { workspace = true }
# Internal
easyenv = { path = "../../../lib/easyenv" }
errors = { path = "../../../lib/errors" }
# Workspace hack (faster compile times)
workspace-build-acceleration = { version = "0.1", path = "../../../workspace-build-acceleration" }
# External
anyhow = "1.0.86"
# TODO/NB: Commenting out clap4 as it prevents windows builds
# NB: clap4 = { package = "clap", version = "4.0" }
dotenv = "0.15.0"
log = "0.4.14"
# Serialization
serde = "1.0.125"
serde_derive = "1.0.125"
+6 -6
View File
@@ -16,17 +16,17 @@ path = "src/main.rs"
normal = ["workspace-build-acceleration"]
[dependencies]
# Workspace hack (faster compile times)
workspace-build-acceleration = { workspace = true }
# Internal
bucket_paths = { path = "../../../lib/bucket_paths" }
easyenv = { path = "../../../lib/easyenv" }
enums = { path = "../../../_types/enums" }
enums = { path = "../../../schema/public/enums" }
errors = { path = "../../../lib/errors" }
mysql_queries = { path = "../../../lib/mysql_queries" }
mysql_queries = { path = "../../../schema/database/mysql_queries" }
storyteller_root = { path = "../../../lib/storyteller_root" }
tokens = { path = "../../../_types/tokens" }
# Workspace hack (faster compile times)
workspace-build-acceleration = { version = "0.1", path = "../../../workspace-build-acceleration" }
tokens = { path = "../../../schema/public/tokens" }
# External
clap = "4.4.8"
@@ -16,20 +16,20 @@ path = "src/main.rs"
normal = ["workspace-build-acceleration"]
[dependencies]
# Workspace hack (faster compile times)
workspace-build-acceleration = { workspace = true }
# Internal
bucket_paths = { path = "../../../lib/bucket_paths" }
config = { path = "../../../lib/deprecated/config" }
easyenv = { path = "../../../lib/easyenv" }
elasticsearch_schema = { path = "../../../lib/elasticsearch_schema" }
enums = { path = "../../../_types/enums" }
elasticsearch_schema = { path = "../../../schema/database/elasticsearch_schema" }
enums = { path = "../../../schema/public/enums" }
errors = { path = "../../../lib/errors" }
mysql_queries = { path = "../../../lib/mysql_queries" }
mysql_queries = { path = "../../../schema/database/mysql_queries" }
primitives = { path = "../../../lib/data/primitives" }
storyteller_root = { path = "../../../lib/storyteller_root" }
tokens = { path = "../../../_types/tokens" }
# Workspace hack (faster compile times)
workspace-build-acceleration = { version = "0.1", path = "../../../workspace-build-acceleration" }
tokens = { path = "../../../schema/public/tokens" }
# External
clap = "4.4.8"
@@ -16,14 +16,14 @@ path = "src/main.rs"
normal = ["workspace-build-acceleration"]
[dependencies]
# Workspace hack (faster compile yytimes)
workspace-build-acceleration = { workspace = true }
# Internal
config = { path = "../../../lib/deprecated/config" }
easyenv = { path = "../../../lib/easyenv" }
errors = { path = "../../../lib/errors" }
# Workspace hack (faster compile yytimes)
workspace-build-acceleration = { version = "0.1", path = "../../../workspace-build-acceleration" }
# External
log = "0.4.20"
once_cell = "1.18.0"
@@ -16,13 +16,13 @@ path = "src/main.rs"
normal = ["workspace-build-acceleration"]
[dependencies]
# Workspace hack (faster compile times)
workspace-build-acceleration = { workspace = true }
# Internal
errors = { path = "../../../lib/errors" }
logging = { path = "../../../lib/logging" }
# Workspace hack (faster compile times)
workspace-build-acceleration = { version = "0.1", path = "../../../workspace-build-acceleration" }
# External
clap = { version = "4.4.8", features = ["derive"] }
walkdir = "2.4.0"
@@ -16,21 +16,21 @@ path = "src/main.rs"
normal = ["workspace-build-acceleration"]
[dependencies]
# Workspace hack (faster compile times)
workspace-build-acceleration = { workspace = true }
# Internal
bucket_paths = { path = "../../../lib/bucket_paths" }
cloud_storage = { path = "../../../lib/api_clients/cloud_storage" }
config = { path = "../../../lib/deprecated/config" }
easyenv = { path = "../../../lib/easyenv" }
enums = { path = "../../../_types/enums" }
enums = { path = "../../../schema/public/enums" }
errors = { path = "../../../lib/errors" }
filesys = { path = "../../../lib/files/filesys" }
hashing = { path = "../../../lib/files/hashing" }
mysql_queries = { path = "../../../lib/mysql_queries" }
mysql_queries = { path = "../../../schema/database/mysql_queries" }
storyteller_root = { path = "../../../lib/storyteller_root" }
tokens = { path = "../../../_types/tokens" }
# Workspace hack (faster compile times)
workspace-build-acceleration = { version = "0.1", path = "../../../workspace-build-acceleration" }
tokens = { path = "../../../schema/public/tokens" }
# External
clap = "4.4.8"
+9 -10
View File
@@ -23,23 +23,24 @@ tauri-build = { version = "2.1.1", features = [] }
workspace-build-acceleration = { workspace = true }
# Internal
crockford = { workspace = true }
artcraft_api_defs = { workspace = true }
build_metadata = { workspace = true }
errors = { workspace = true }
fal_client = { workspace = true }
filesys = { workspace = true }
memory_caching = { workspace = true }
idempotency = { workspace = true }
memory_store = { workspace = true }
mimetypes = { workspace = true }
openai_sora_client = { workspace = true }
primitives = { workspace = true }
storyteller_client = { workspace = true }
subprocess_common = { workspace = true }
tokens = { workspace = true }
# External
anyhow = { workspace = true }
base64 = { workspace = true }
bytes = "1.10"
bytes = "1.10.1"
chrono = { workspace = true }
const_format = "0.2.34"
directories = "6.0.0"
dirs = "6.0.0"
futures = "0.3"
@@ -52,14 +53,14 @@ reqwest = { workspace = true, features = ["multipart", "stream"], default-featur
serde = { workspace = true }
serde_derive = { workspace = true }
serde_json = { workspace = true }
serde_yaml = { workspace = true }
tauri = { workspace = true }
tauri-plugin-log = "2.3.1"
tauri-plugin-upload = "2.2.1"
tempdir = "0.3.7"
tempfile = "3.18.0"
tempdir = { workspace = true }
tempfile = { workspace = true }
tokio = { version = "1.42", features = ["rt", "rt-multi-thread", "fs"] }
tokio-util = { version = "0.7", features = ["codec"] }
url = { workspace = true }
# NB: We need to vendor this in so that we can access the cookies inside via the private API
tauri-plugin-http = { path = "../../vendor/tauri-plugin-http" }
@@ -70,9 +71,7 @@ discord-rich-presence = { git = "https://github.com/vionya/discord-rich-presence
tauri-plugin-dialog = "2"
[dev-dependencies]
libloading = "0.8.6"
serial_test = "3.2.0"
assertor = "0.0.3"
[profile.dev]
incremental = true # Compile your binary in smaller steps.
@@ -10,19 +10,12 @@
"log:default",
"upload:allow-download",
"upload:default",
"http:allow-fetch",
{
"identifier": "http:default",
"allow": [
{
"url": "https://*.storyteller.ai"
},
{
"url": "https://*.fakeyou.com"
}
],
"deny": [
{
"url": "https://private.tauri.app"
"url": "https://*"
}
]
},
@@ -1,5 +1,5 @@
use crate::state::app_preferences::app_preferences_manager::AppPreferencesManager;
use crate::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
use crate::core::state::app_preferences::app_preferences_manager::AppPreferencesManager;
use crate::core::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
use errors::AnyhowResult;
use log::{error, info};
use serde_derive::Serialize;
@@ -1,6 +1,6 @@
use crate::state::app_preferences::app_preferences_manager::AppPreferencesManager;
use crate::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::app_preferences::app_preferences_manager::AppPreferencesManager;
use crate::core::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use anyhow::anyhow;
use errors::AnyhowResult;
use log::{error, info};
@@ -0,0 +1,184 @@
use crate::core::commands::enqueue::image::handle_fal::handle_fal;
use crate::core::commands::enqueue::image::handle_sora::handle_sora;
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use crate::core::commands::response::shorthand::Response;
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
use crate::core::events::sendable_event_trait::SendableEvent;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use crate::core::utils::download_media_file_to_temp_dir::download_media_file_to_temp_dir;
use crate::core::utils::get_url_file_extension::get_url_file_extension;
use crate::core::utils::save_base64_image_to_temp_dir::save_base64_image_to_temp_dir;
use crate::core::utils::simple_http_download::simple_http_download;
use crate::core::utils::simple_http_download_to_tempfile::simple_http_download_to_tempfile;
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
use crate::services::sora::state::read_sora_credentials_from_disk::read_sora_credentials_from_disk;
use crate::services::sora::state::sora_credential_holder::SoraCredentialHolder;
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 base64::prelude::BASE64_STANDARD;
use base64::{DecodeError, Engine};
use errors::{AnyhowError, AnyhowResult};
use fal_client::error::fal_error_plus::FalErrorPlus;
use fal_client::requests::queue::image_gen::enqueue_flux_pro_ultra_text_to_image::{enqueue_flux_pro_ultra_text_to_image, FluxProUltraTextToImageArgs};
use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs};
use filesys::file_read_bytes::file_read_bytes;
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
use image::{DynamicImage, EncodableLayout, ImageReader};
use log::{debug, error, info, warn};
use mimetypes::mimetype_info::mimetype_info::MimetypeInfo;
use openai_sora_client::credentials::SoraCredentials;
use openai_sora_client::creds::credential_migration::CredentialMigrationRef;
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::requests::image_gen::sora_image_gen_remix::{sora_image_gen_remix, SoraImageGenRemixRequest};
use openai_sora_client::requests::upload::upload_media_from_bytes::sora_media_upload_from_bytes;
use openai_sora_client::requests::upload::upload_media_from_file::sora_media_upload_from_file;
use openai_sora_client::sora_error::SoraError;
use reqwest::Url;
use serde_derive::{Deserialize, Serialize};
use std::fs::read_to_string;
use std::io::{Cursor, Read, Write};
use std::ops::Add;
use std::path::PathBuf;
use std::time::Duration;
use storyteller_client::error::api_error::ApiError;
use storyteller_client::error::api_error::ApiError::InternalServerError;
use storyteller_client::media_files::get_media_file::{get_media_file, GetMediaFileSuccessResponse};
use storyteller_client::media_files::upload_image_media_file_from_file::upload_image_media_file_from_file;
use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, Emitter, Manager, State};
use tempfile::NamedTempFile;
#[derive(Deserialize)]
pub struct EnqueueTextToImageRequest {
/// Text prompt for the image generation. Required.
pub prompt: Option<String>,
/// The model to use.
pub model: Option<EnqueueTextToImageModel>,
}
#[derive(Deserialize, Debug, Copy, Clone)]
#[serde(rename_all = "snake_case")]
pub enum EnqueueTextToImageModel {
#[serde(rename = "flux_pro_ultra")]
FluxProUltra,
#[serde(rename = "gpt_image_1")]
GptImage1,
#[serde(rename = "recraft_3")]
Recraft3,
}
#[derive(Serialize)]
pub struct EnqueueTextToImageSuccessResponse {
}
impl SerializeMarker for EnqueueTextToImageSuccessResponse {}
#[derive(Serialize, Debug)]
#[serde(rename_all = "snake_case")]
pub enum EnqueueTextToImageErrorType {
/// Caller didn't specify a model
ModelNotSpecified,
/// Generic server error
ServerError,
/// No Fal API key available
NeedsFalApiKey,
/// Fal had an API error
FalError,
}
#[tauri::command]
pub async fn enqueue_text_to_image_command(
app: AppHandle,
request: EnqueueTextToImageRequest,
app_data_root: State<'_, AppDataRoot>,
fal_creds_manager: State<'_, FalCredentialManager>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
fal_task_queue: State<'_, FalTaskQueue>,
sora_creds_manager: State<'_, SoraCredentialManager>,
sora_task_queue: State<'_, SoraTaskQueue>,
) -> Response<EnqueueTextToImageSuccessResponse, EnqueueTextToImageErrorType, ()> {
info!("enqueue_text_to_image called");
let result = handle_request(
&app,
request,
&app_data_root,
&fal_creds_manager,
&storyteller_creds_manager,
&fal_task_queue,
&sora_creds_manager,
&sora_task_queue,
).await;
match result {
Err(err) => {
error!("error: {:?}", err);
let mut status = CommandErrorStatus::ServerError;
let mut error_type = EnqueueTextToImageErrorType::ServerError;
let mut error_message = "A server error occurred. Please try again. If it continues, please tell our staff about the problem.";
match err {
InternalImageError::NoModelSpecified => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueTextToImageErrorType::ModelNotSpecified;
error_message = "No model specified for image generation";
}
InternalImageError::NeedsFalApiKey => {
status = CommandErrorStatus::Unauthorized;
error_type = EnqueueTextToImageErrorType::NeedsFalApiKey;
error_message = "You need to set a FAL api key";
},
_ => {}, // Fall-through
}
Err(CommandErrorResponseWrapper {
status,
error_message: Some(error_message.to_string()),
error_type: Some(error_type),
error_details: None,
})
}
Ok(()) => {
Ok(EnqueueTextToImageSuccessResponse {}.into())
}
}
}
pub async fn handle_request(
app: &AppHandle,
request: EnqueueTextToImageRequest,
app_data_root: &AppDataRoot,
fal_creds_manager: &FalCredentialManager,
storyteller_creds_manager: &StorytellerCredentialManager,
fal_task_queue: &FalTaskQueue,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<(), InternalImageError> {
match request.model {
None => {
return Err(InternalImageError::NoModelSpecified);
}
Some(EnqueueTextToImageModel::FluxProUltra) | Some(EnqueueTextToImageModel::Recraft3) => {
handle_fal(&app, request, fal_creds_manager, fal_task_queue).await?;
},
Some(EnqueueTextToImageModel::GptImage1) => {
handle_sora(&app, request, sora_creds_manager, sora_task_queue).await?;
}
};
Ok(())
}
@@ -0,0 +1,111 @@
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageModel, EnqueueTextToImageRequest};
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use anyhow::anyhow;
use fal_client::creds::fal_api_key::FalApiKey;
use fal_client::requests::queue::image_gen::enqueue_flux_pro_ultra_text_to_image::{enqueue_flux_pro_ultra_text_to_image, FluxProUltraTextToImageArgs};
use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs};
use log::{error, info, warn};
use tauri::AppHandle;
pub async fn handle_fal(
app: &AppHandle,
request: EnqueueTextToImageRequest,
fal_creds_manager: &FalCredentialManager,
fal_task_queue: &FalTaskQueue,
) -> Result<(), InternalImageError> {
let api_key = match fal_creds_manager.get_key()? {
Some(key) => key,
None => {
error!("No FAL API key is set. Can't perform action.");
let event =
GenerationEnqueueFailureEvent::no_fal_api_key(GenerationAction::GenerateImage);
if let Err(err) = event.send(&app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
return Err(InternalImageError::NeedsFalApiKey);
},
};
let api_key = fal_creds_manager.get_key_required()
.map_err(|err| {
error!("EnqueueTextToImage FAL api key required: {:?}", err);
InternalImageError::NeedsFalApiKey
})?;
let prompt = request.prompt.as_deref().unwrap_or("");
let result = match request.model {
None => {
return Err(InternalImageError::NoModelSpecified);
}
Some(EnqueueTextToImageModel::GptImage1) => {
return Err(InternalImageError::AnyhowError(anyhow!("wrong logic: fal is handling sora images")));
}
Some(EnqueueTextToImageModel::FluxProUltra) => {
info!("enqueue Flux Pro Ultra text-to-image with prompt: {}", prompt);
enqueue_flux_pro_ultra_text_to_image(FluxProUltraTextToImageArgs {
prompt,
api_key: &api_key,
}).await
}
Some(EnqueueTextToImageModel::Recraft3) => {
info!("enqueue Recraft v3 text-to-image with prompt: {}", prompt);
enqueue_recraft3_text_to_image(Recraft3TextToImageArgs {
prompt,
api_key: &api_key,
}).await
}
};
match result {
Ok(enqueued) => {
info!("Successfully enqueued text to image");
let event = GenerationEnqueueSuccessEvent {
action: GenerationAction::GenerateImage,
service: GenerationServiceProvider::Fal,
model: None,
};
if let Err(err) = event.send(app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
if let Err(err) = fal_task_queue.insert(&enqueued) {
error!("Failed to enqueue task: {:?}", err);
return Err(InternalImageError::AnyhowError(anyhow!("Failed to enqueue task: {:?}", err)));
}
}
Err(err) => {
error!("Failed to enqueue text to image: {:?}", err);
let event = GenerationEnqueueFailureEvent {
action: GenerationAction::GenerateImage,
service: GenerationServiceProvider::Fal,
model: None,
reason: None,
};
if let Err(err) = event.send(app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
return Err(InternalImageError::FalError(err));
}
}
Ok(())
}
@@ -0,0 +1,104 @@
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageModel, EnqueueTextToImageRequest};
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use anyhow::anyhow;
use fal_client::requests::queue::image_gen::enqueue_flux_pro_ultra_text_to_image::{enqueue_flux_pro_ultra_text_to_image, FluxProUltraTextToImageArgs};
use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs};
use log::{error, info, warn};
use openai_sora_client::creds::sora_credential_set::SoraCredentialSet;
use openai_sora_client::recipes::image_remix_with_session_auto_renew::{image_remix_with_session_auto_renew, ImageRemixAutoRenewRequest};
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, SoraImageGenResponse};
use openai_sora_client::sora_error::SoraError;
use std::time::Duration;
use tauri::AppHandle;
const SORA_SIMPLE_IMAGE_GEN_TIMEOUT : Duration = Duration::from_millis(1000 * 30); // 30 seconds
pub async fn handle_sora(
app: &AppHandle,
request: EnqueueTextToImageRequest,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<(), InternalImageError> {
let mut creds = sora_creds_manager
.get_credentials_required()
.map_err(|err| {
error!("EnqueueTextToImage Sora creds required: {:?}", err);
InternalImageError::NeedsFalApiKey
})?;
let credential_updated = maybe_upgrade_or_renew_session(&mut creds)
.await
.map_err(|err| {
error!("Failed to upgrade or renew session: {:?}", err);
err
})?;
if credential_updated {
info!("Storing updated credentials");
sora_creds_manager.set_credentials(&creds)?;
}
let prompt = request.prompt.as_deref().unwrap_or("");
let result =
simple_image_gen_with_session_auto_renew(SimpleImageGenAutoRenewRequest {
prompt: prompt.to_string(),
num_images: NumImages::One,
image_size: ImageSize::Square,
credentials: &creds,
request_timeout: Some(SORA_SIMPLE_IMAGE_GEN_TIMEOUT),
}).await;
let (response, maybe_new_creds) =
match result {
Ok((response, maybe_new_creds)) => (response, maybe_new_creds),
Err(err) => {
let event = GenerationEnqueueFailureEvent {
action: GenerationAction::GenerateImage,
service: GenerationServiceProvider::Sora,
model: None,
reason: None,
};
if let Err(err) = event.send(app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
return Err(InternalImageError::SoraError(err));
}
};
if let Some(new_creds) = maybe_new_creds {
info!("Storing updated credentials.");
sora_creds_manager.set_credentials(&new_creds)?;
}
info!("New Sora Task ID: {:?} ", response.task_id);
sora_task_queue.insert(&response.task_id)?;
let event = GenerationEnqueueSuccessEvent {
action: GenerationAction::GenerateImage,
service: GenerationServiceProvider::Sora,
model: None,
};
if let Err(err) = event.send(app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
Ok(())
}
@@ -0,0 +1,54 @@
use base64::DecodeError;
use errors::AnyhowError;
use fal_client::error::fal_error_plus::FalErrorPlus;
use openai_sora_client::sora_error::SoraError;
use storyteller_client::error::api_error::ApiError;
#[derive(Debug)]
pub enum InternalImageError {
NoModelSpecified,
NeedsFalApiKey,
NeedsSoraCredentials,
FalError(FalErrorPlus),
SoraError(SoraError),
AnyhowError(AnyhowError),
StorytellerApiError(ApiError),
DecodeError(DecodeError),
IoError(std::io::Error),
}
impl From<AnyhowError> for InternalImageError {
fn from(value: AnyhowError) -> Self {
Self::AnyhowError(value)
}
}
impl From<FalErrorPlus> for InternalImageError {
fn from(value: FalErrorPlus) -> Self {
Self::FalError(value)
}
}
impl From<SoraError> for InternalImageError {
fn from(value: SoraError) -> Self {
Self::SoraError(value)
}
}
impl From<ApiError> for InternalImageError {
fn from(value: ApiError) -> Self {
Self::StorytellerApiError(value)
}
}
impl From<DecodeError> for InternalImageError {
fn from(value: DecodeError) -> Self {
Self::DecodeError(value)
}
}
impl From<std::io::Error> for InternalImageError {
fn from(value: std::io::Error) -> Self {
Self::IoError(value)
}
}
@@ -0,0 +1,4 @@
pub mod enqueue_text_to_image_command;
pub mod handle_fal;
pub mod handle_sora;
pub mod internal_image_error;
@@ -0,0 +1,2 @@
pub mod image;
pub mod object;
@@ -0,0 +1,179 @@
use crate::core::commands::enqueue::image::handle_fal::handle_fal;
use crate::core::commands::enqueue::image::handle_sora::handle_sora;
use crate::core::commands::enqueue::object::handle_object_fal::handle_object_fal;
use crate::core::commands::enqueue::object::internal_object_error::InternalObjectError;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use crate::core::commands::response::shorthand::Response;
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
use crate::core::events::sendable_event_trait::SendableEvent;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use crate::core::utils::download_media_file_to_temp_dir::download_media_file_to_temp_dir;
use crate::core::utils::get_url_file_extension::get_url_file_extension;
use crate::core::utils::save_base64_image_to_temp_dir::save_base64_image_to_temp_dir;
use crate::core::utils::simple_http_download::simple_http_download;
use crate::core::utils::simple_http_download_to_tempfile::simple_http_download_to_tempfile;
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
use crate::services::sora::state::read_sora_credentials_from_disk::read_sora_credentials_from_disk;
use crate::services::sora::state::sora_credential_holder::SoraCredentialHolder;
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 base64::prelude::BASE64_STANDARD;
use base64::{DecodeError, Engine};
use errors::{AnyhowError, AnyhowResult};
use fal_client::error::fal_error_plus::FalErrorPlus;
use fal_client::requests::queue::image_gen::enqueue_flux_pro_ultra_text_to_image::{enqueue_flux_pro_ultra_text_to_image, FluxProUltraTextToImageArgs};
use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs};
use filesys::file_read_bytes::file_read_bytes;
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
use image::{DynamicImage, EncodableLayout, ImageReader};
use log::{debug, error, info, warn};
use mimetypes::mimetype_info::mimetype_info::MimetypeInfo;
use openai_sora_client::credentials::SoraCredentials;
use openai_sora_client::creds::credential_migration::CredentialMigrationRef;
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::requests::image_gen::sora_image_gen_remix::{sora_image_gen_remix, SoraImageGenRemixRequest};
use openai_sora_client::requests::upload::upload_media_from_bytes::sora_media_upload_from_bytes;
use openai_sora_client::requests::upload::upload_media_from_file::sora_media_upload_from_file;
use openai_sora_client::sora_error::SoraError;
use reqwest::Url;
use serde_derive::{Deserialize, Serialize};
use std::fs::read_to_string;
use std::io::{Cursor, Read, Write};
use std::ops::Add;
use std::path::PathBuf;
use std::time::Duration;
use storyteller_client::error::api_error::ApiError;
use storyteller_client::error::api_error::ApiError::InternalServerError;
use storyteller_client::media_files::get_media_file::{get_media_file, GetMediaFileSuccessResponse};
use storyteller_client::media_files::upload_image_media_file_from_file::upload_image_media_file_from_file;
use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, Emitter, Manager, State};
use tempfile::NamedTempFile;
use tokens::tokens::media_files::MediaFileToken;
use crate::core::commands::enqueue::object::handle_object_artcraft::handle_object_artcraft;
#[derive(Deserialize)]
pub struct EnqueueImageTo3dObjectRequest {
/// Image media file; the image to remove the background from.
/// TODO: In the future we may support base64 images, URLs, or file paths here.
pub image_media_token: Option<MediaFileToken>,
/// The model to use.
pub model: Option<EnqueueImageTo3dObjectModel>,
}
#[derive(Deserialize, Debug, Copy, Clone)]
#[serde(rename_all = "snake_case")]
pub enum EnqueueImageTo3dObjectModel {
#[serde(rename = "hunyuan_3d_2")]
Hunyuan3d2,
}
#[derive(Serialize)]
pub struct EnqueueImageTo3dObjectSuccessResponse {
}
impl SerializeMarker for EnqueueImageTo3dObjectSuccessResponse {}
#[derive(Serialize, Debug)]
#[serde(rename_all = "snake_case")]
pub enum EnqueueImageTo3dObjectErrorType {
/// Caller didn't specify a model
ModelNotSpecified,
/// Generic server error
ServerError,
/// No Fal API key available
NeedsFalApiKey,
/// Fal had an API error
FalError,
/// Needs to be logged into Artcraft
NeedsStorytellerCredentials,
}
#[tauri::command]
pub async fn enqueue_image_to_3d_object_command(
app: AppHandle,
request: EnqueueImageTo3dObjectRequest,
app_data_root: State<'_, AppDataRoot>,
fal_creds_manager: State<'_, FalCredentialManager>,
fal_task_queue: State<'_, FalTaskQueue>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
sora_task_queue: State<'_, SoraTaskQueue>,
) -> Response<EnqueueImageTo3dObjectSuccessResponse, EnqueueImageTo3dObjectErrorType, ()> {
info!("enqueue_image_to_3d_object_command called");
let result = handle_request(
&app,
request,
&app_data_root,
&fal_creds_manager,
&storyteller_creds_manager,
&fal_task_queue,
).await;
match result {
Err(err) => {
error!("error: {:?}", err);
let mut status = CommandErrorStatus::ServerError;
let mut error_type = EnqueueImageTo3dObjectErrorType::ServerError;
let mut error_message = "A server error occurred. Please try again. If it continues, please tell our staff about the problem.";
match err {
InternalObjectError::NoModelSpecified => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueImageTo3dObjectErrorType::ModelNotSpecified;
error_message = "No model specified for image generation";
}
InternalObjectError::NeedsFalApiKey => {
status = CommandErrorStatus::Unauthorized;
error_type = EnqueueImageTo3dObjectErrorType::NeedsFalApiKey;
error_message = "You need to set a FAL api key";
},
InternalObjectError::NeedsStorytellerCredentials => {
status = CommandErrorStatus::Unauthorized;
error_type = EnqueueImageTo3dObjectErrorType::NeedsStorytellerCredentials;
error_message = "You need to be logged into Artcraft.";
}
_ => {}, // Fall-through
}
Err(CommandErrorResponseWrapper {
status,
error_message: Some(error_message.to_string()),
error_type: Some(error_type),
error_details: None,
})
}
Ok(()) => {
Ok(EnqueueImageTo3dObjectSuccessResponse {}.into())
}
}
}
pub async fn handle_request(
app: &AppHandle,
request: EnqueueImageTo3dObjectRequest,
app_data_root: &AppDataRoot,
fal_creds_manager: &FalCredentialManager,
storyteller_creds_manager: &StorytellerCredentialManager,
fal_task_queue: &FalTaskQueue,
) -> Result<(), InternalObjectError> {
if fal_creds_manager.has_apparent_api_token()? {
handle_object_fal(&app, app_data_root, request, fal_creds_manager, fal_task_queue).await?;
} else {
handle_object_artcraft(request, &app, app_data_root, storyteller_creds_manager).await?;
}
Ok(())
}
@@ -0,0 +1,129 @@
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageModel, EnqueueTextToImageRequest};
use crate::core::commands::enqueue::object::enqueue_image_to_3d_object_command::{EnqueueImageTo3dObjectModel, EnqueueImageTo3dObjectRequest};
use crate::core::commands::enqueue::object::internal_object_error::InternalObjectError;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
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::state::data_dir::app_data_root::AppDataRoot;
use crate::core::utils::download_media_file_to_temp_dir::download_media_file_to_temp_dir;
use crate::core::utils::save_base64_image_to_temp_dir::save_base64_image_to_temp_dir;
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::object::generate_hunyuan_2_image_to_3d::GenerateHunyuan2ImageTo3dRequest;
use fal_client::creds::fal_api_key::FalApiKey;
use fal_client::requests::queue::enqueue_hunyuan2_image_to_3d::{enqueue_hunyuan2_image_to_3d, Hunyuan2Args};
use fal_client::requests::queue::image_gen::enqueue_flux_pro_ultra_text_to_image::{enqueue_flux_pro_ultra_text_to_image, FluxProUltraTextToImageArgs};
use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs};
use idempotency::uuid::generate_random_uuid;
use log::{error, info, warn};
use storyteller_client::generate::object::generate_hunyuan2_image_to_3d::generate_hunyuan2_image_to_3d;
use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, State};
pub async fn handle_object_artcraft(
request: EnqueueImageTo3dObjectRequest,
app: &AppHandle,
app_data_root: &AppDataRoot,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<(), InternalObjectError> {
let creds = match storyteller_creds_manager.get_credentials()? {
Some(creds) => creds,
None => {
error!("No Artcraft credentials are set. Can't perform action.");
let event =
GenerationEnqueueFailureEvent::no_artcraft_credentials(GenerationAction::ImageTo3d);
if let Err(err) = event.send(&app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
return Err(InternalObjectError::NeedsStorytellerCredentials);
},
};
//let api_key = fal_creds_manager.get_key_required()
// .map_err(|err| {
// error!("EnqueueTextToImage FAL api key required: {:?}", err);
// InternalObjectError::NeedsFalApiKey
// })?;
info!("Calling Artcraft image to 3d ...");
let uuid_idempotency_token = generate_random_uuid();
let job_token = match request.model {
None => {
return Err(InternalObjectError::NoModelSpecified);
}
Some(EnqueueImageTo3dObjectModel::Hunyuan3d2) => {
info!("enqueue Hunyuan 3D 2.0");
let request = GenerateHunyuan2ImageTo3dRequest {
uuid_idempotency_token,
media_file_token: request.image_media_token,
};
let result = generate_hunyuan2_image_to_3d(
&ApiHost::Storyteller,
Some(&creds),
request,
).await;
match result {
Ok(enqueued) => {
info!("Successfully enqueued Artcraft Hunyuan 3D 2.0");
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft Hunyuan 3D 2.0: {:?}", err);
return Err(InternalObjectError::StorytellerError(err));
}
}
}
};
info!("Successfully enqueued 3D generation");
let event = GenerationEnqueueSuccessEvent {
action: GenerationAction::ImageTo3d,
service: GenerationServiceProvider::Artcraft,
model: None,
};
if let Err(err) = event.send(app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
//if let Err(err) = fal_task_queue.insert(&enqueued) {
// error!("Failed to enqueue task: {:?}", err);
// return Err(InternalObjectError::AnyhowError(anyhow!("Failed to enqueue task: {:?}", err)));
//}
// match result {
// Ok(enqueued) => {
// }
// Err(err) => {
// error!("Failed to enqueue image to 3d: {:?}", err);
//
// let event = GenerationEnqueueFailureEvent {
// action: GenerationAction::ImageTo3d,
// service: GenerationServiceProvider::Fal,
// model: None,
// reason: None,
// };
//
// if let Err(err) = event.send(app) {
// error!("Failed to emit event: {:?}", err); // Fail open.
// }
//
// return Err(InternalObjectError::FalError(err));
// }
// }
Ok(())
}
@@ -0,0 +1,117 @@
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageModel, EnqueueTextToImageRequest};
use crate::core::commands::enqueue::object::enqueue_image_to_3d_object_command::{EnqueueImageTo3dObjectModel, EnqueueImageTo3dObjectRequest};
use crate::core::commands::enqueue::object::internal_object_error::InternalObjectError;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
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::state::data_dir::app_data_root::AppDataRoot;
use crate::core::utils::download_media_file_to_temp_dir::download_media_file_to_temp_dir;
use crate::core::utils::save_base64_image_to_temp_dir::save_base64_image_to_temp_dir;
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 fal_client::creds::fal_api_key::FalApiKey;
use fal_client::requests::queue::enqueue_hunyuan2_image_to_3d::{enqueue_hunyuan2_image_to_3d, Hunyuan2Args};
use fal_client::requests::queue::image_gen::enqueue_flux_pro_ultra_text_to_image::{enqueue_flux_pro_ultra_text_to_image, FluxProUltraTextToImageArgs};
use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs};
use log::{error, info, warn};
use tauri::AppHandle;
pub async fn handle_object_fal(
app: &AppHandle,
app_data_root: &AppDataRoot,
request: EnqueueImageTo3dObjectRequest,
fal_creds_manager: &FalCredentialManager,
fal_task_queue: &FalTaskQueue,
) -> Result<(), InternalObjectError> {
let api_key = match fal_creds_manager.get_key()? {
Some(key) => key,
None => {
error!("No FAL API key is set. Can't perform action.");
let event =
GenerationEnqueueFailureEvent::no_fal_api_key(GenerationAction::GenerateImage);
if let Err(err) = event.send(&app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
return Err(InternalObjectError::NeedsFalApiKey);
},
};
//let api_key = fal_creds_manager.get_key_required()
// .map_err(|err| {
// error!("EnqueueTextToImage FAL api key required: {:?}", err);
// InternalObjectError::NeedsFalApiKey
// })?;
let mut temp_download;
if let Some(media_token) = request.image_media_token {
temp_download = download_media_file_to_temp_dir(&app_data_root, &media_token).await?;
} else {
return Err(InternalObjectError::AnyhowError(anyhow!("No image media token provided")));
}
info!("Calling FAL image to 3d ...");
let filename = temp_download.path().to_path_buf();
let result = match request.model {
None => {
return Err(InternalObjectError::NoModelSpecified);
}
Some(EnqueueImageTo3dObjectModel::Hunyuan3d2) => {
info!("enqueue Hunyuan 3D 2.0");
enqueue_hunyuan2_image_to_3d(Hunyuan2Args {
image_path: filename,
api_key: &api_key,
}).await
}
};
match result {
Ok(enqueued) => {
info!("Successfully enqueued text to image");
let event = GenerationEnqueueSuccessEvent {
action: GenerationAction::ImageTo3d,
service: GenerationServiceProvider::Fal,
model: None,
};
if let Err(err) = event.send(app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
if let Err(err) = fal_task_queue.insert(&enqueued) {
error!("Failed to enqueue task: {:?}", err);
return Err(InternalObjectError::AnyhowError(anyhow!("Failed to enqueue task: {:?}", err)));
}
}
Err(err) => {
error!("Failed to enqueue image to 3d: {:?}", err);
let event = GenerationEnqueueFailureEvent {
action: GenerationAction::ImageTo3d,
service: GenerationServiceProvider::Fal,
model: None,
reason: None,
};
if let Err(err) = event.send(app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
return Err(InternalObjectError::FalError(err));
}
}
Ok(())
}
@@ -0,0 +1,49 @@
use base64::DecodeError;
use errors::AnyhowError;
use fal_client::error::fal_error_plus::FalErrorPlus;
use storyteller_client::error::api_error::ApiError;
use storyteller_client::error::storyteller_error::StorytellerError;
#[derive(Debug)]
pub enum InternalObjectError {
NoModelSpecified,
NeedsFalApiKey,
NeedsSoraCredentials,
NeedsStorytellerCredentials,
FalError(FalErrorPlus),
AnyhowError(AnyhowError),
StorytellerError(StorytellerError),
StorytellerApiError(ApiError),
DecodeError(DecodeError),
IoError(std::io::Error),
}
impl From<AnyhowError> for InternalObjectError {
fn from(value: AnyhowError) -> Self {
Self::AnyhowError(value)
}
}
impl From<FalErrorPlus> for InternalObjectError {
fn from(value: FalErrorPlus) -> Self {
Self::FalError(value)
}
}
impl From<ApiError> for InternalObjectError {
fn from(value: ApiError) -> Self {
Self::StorytellerApiError(value)
}
}
impl From<DecodeError> for InternalObjectError {
fn from(value: DecodeError) -> Self {
Self::DecodeError(value)
}
}
impl From<std::io::Error> for InternalObjectError {
fn from(value: std::io::Error) -> Self {
Self::IoError(value)
}
}
@@ -0,0 +1,4 @@
pub mod enqueue_image_to_3d_object_command;
pub mod handle_object_artcraft;
pub mod handle_object_fal;
pub mod internal_object_error;
@@ -0,0 +1,40 @@
use crate::core::commands::response::shorthand::InfallibleResponse;
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
use crate::core::state::os_platform::OsPlatform;
use crate::services::fal::commands::fal_background_removal_command::FalBackgroundRemovalSuccessResponse;
use base64::prelude::BASE64_STANDARD;
use base64::Engine;
use chrono::{DateTime, Utc};
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
use image::ImageReader;
use log::info;
use serde_derive::Serialize;
use std::io::Cursor;
#[derive(Debug, Serialize)]
pub struct BuildInfoResponse {
pub build_timestamp: DateTime<Utc>,
pub git_commit_id: Option<String>,
pub git_commit_short_id: Option<String>,
pub git_commit_timestamp: Option<DateTime<Utc>>,
}
impl SerializeMarker for BuildInfoResponse {}
#[tauri::command]
pub fn get_build_info_command() -> InfallibleResponse<BuildInfoResponse> {
info!("get_build_info_command called...");
let build_timestamp = build_metadata::build_timestamp();
let git_commit_id = build_metadata::git_commit_id();
let git_commit_short_id = build_metadata::git_commit_short_id();
let git_commit_timestamp = build_metadata::git_commit_timestamp();
BuildInfoResponse {
build_timestamp,
git_commit_id: git_commit_id.map(|s| s.to_string()),
git_commit_short_id: git_commit_short_id.map(|s| s.to_string()),
git_commit_timestamp,
}.into()
}
@@ -1,5 +1,6 @@
pub mod app_preferences;
pub mod command_response_wrapper;
pub mod enqueue;
pub mod flip_image;
pub mod get_build_info_command;
pub mod platform_info_command;
pub mod sora;
pub mod response;
@@ -1,4 +1,6 @@
use crate::state::os_platform::OsPlatform;
use crate::core::commands::response::shorthand::InfallibleResponse;
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
use crate::core::state::os_platform::OsPlatform;
use base64::prelude::BASE64_STANDARD;
use base64::Engine;
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
@@ -13,6 +15,8 @@ pub struct PlatformInfoResponse {
pub webview_runtime: WebviewRuntime,
}
impl SerializeMarker for PlatformInfoResponse {}
#[derive(Copy, Clone, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DetectedOs {
@@ -36,7 +40,7 @@ pub enum WebviewRuntime {
}
#[tauri::command]
pub fn platform_info_command() -> Result<PlatformInfoResponse, String> {
pub fn platform_info_command() -> InfallibleResponse<PlatformInfoResponse> {
info!("platform_info_command called...");
let os_platform = match OsPlatform::maybe_get() {
@@ -55,9 +59,9 @@ pub fn platform_info_command() -> Result<PlatformInfoResponse, String> {
DetectedOs::Unknown => WebviewRuntime::Unknown,
};
Ok(PlatformInfoResponse {
PlatformInfoResponse {
os_platform,
webview_runtime,
})
}.into()
}
@@ -0,0 +1,42 @@
use serde::Serialize;
/// Statuses for errorful commands.
#[derive(Serialize, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub enum CommandErrorStatus {
BadRequest, // 400
Unauthorized, // 401
TooManyRequests, // 429
ServerError, // 500
}
#[derive(Serialize, Debug)]
pub struct CommandErrorResponseWrapper<ErrType: Serialize, ErrPayload: Serialize> {
/// Both "success" and "error" types have a `status` field for the frontend.
/// We constrain the value types for successes and failures.
pub status: CommandErrorStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub error_message: Option<String>,
/// Represents a "type" of error, preferably an enum without payloads.
/// Preferably this serializes to a string.
#[serde(skip_serializing_if = "Option::is_none")]
pub error_type: Option<ErrType>,
/// Represents a full error details object or payload.
/// This could be a structure, message, or whatever.
#[serde(skip_serializing_if = "Option::is_none")]
pub error_details: Option<ErrPayload>,
}
impl From<&str> for CommandErrorResponseWrapper<(), ()> {
fn from(value: &str) -> Self {
CommandErrorResponseWrapper {
status: CommandErrorStatus::BadRequest, // NB: Default to 500 error.
error_message: Some(value.to_string()),
error_type: None,
error_details: None,
}
}
}
@@ -0,0 +1,3 @@
pub mod failure_response_wrapper;
pub mod shorthand;
pub mod success_response_wrapper;
@@ -0,0 +1,32 @@
use crate::core::commands::response::failure_response_wrapper::CommandErrorResponseWrapper;
use crate::core::commands::response::success_response_wrapper::CommandSuccessResponseWrapper;
// TODO(needs command response refactor):
// - get_app_preferences_command
// - update_app_preferences_command
// - flip_image
// - check_sora_session_command
// - open_sora_login_command
// - sora_image_generation_command
// TODO(already updated, need to update frontend):
// - platform_info_command
// TODO(simplify error types):
// - enqueue_text_to_image_command
// - fal_background_removal_command
// - fal_hunyuan_image_to_3d_command
// - fal_kling_image_to_video_command
// - get_fal_api_key_command
// - set_fal_api_key_command
// - sora_image_remix_command
/// Easy to use infallible response.
pub type InfallibleResponse<SuccessPayload> = CommandSuccessResponseWrapper<SuccessPayload>;
/// Easy to use Result<T, E> type.
pub type Response<SuccessPayload, ErrType, ErrPayload> =
Result<CommandSuccessResponseWrapper<SuccessPayload>, CommandErrorResponseWrapper<ErrType, ErrPayload>>;
/// No inner payloads for this type. Just strings as messages.
pub type SimpleResponse = Response<(), (), ()>;
@@ -1,13 +1,4 @@
use serde::Serialize;
use std::error::Error;
use std::str::FromStr;
/// Easy to use Result<T, E> type.
pub type CommandResult<SuccessPayload, ErrType, ErrPayload> =
Result<CommandSuccessResponseWrapper<SuccessPayload>, CommandErrorResponseWrapper<ErrType, ErrPayload>>;
/// No inner payloads for this type. Just strings as messages.
pub type SimpleCommandResult = CommandResult<(), (), ()>;
/// Statuses for successful commands.
#[derive(Serialize, Clone, Debug)]
@@ -16,49 +7,30 @@ pub enum CommandSuccessStatus {
Success, // 200
}
/// Statuses for errorful commands.
#[derive(Serialize, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub enum CommandErrorStatus {
BadRequest, // 400
Unauthorized, // 401
TooManyRequests, // 429
ServerError, // 500
}
#[derive(Serialize, Debug)]
pub struct CommandSuccessResponseWrapper<T: Serialize> {
/// Both "success" and "error" types have a `status` field for the frontend.
/// We constrain the value types for successes and failures.
pub status: CommandSuccessStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub success_message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub payload: Option<T>,
}
#[derive(Serialize, Debug)]
pub struct CommandErrorResponseWrapper<ErrType: Serialize, ErrPayload: Serialize> {
/// Both "success" and "error" types have a `status` field for the frontend.
pub status: CommandErrorStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub error_message: Option<String>,
/// Represents a "type" of error, preferably an enum without payloads.
/// Preferably this serializes to a string.
#[serde(skip_serializing_if = "Option::is_none")]
pub error_type: Option<ErrType>,
/// Represents a full error details object or payload.
/// This could be a structure, message, or whatever.
#[serde(skip_serializing_if = "Option::is_none")]
pub error_details: Option<ErrPayload>,
}
/// NB: This is a hack because of `()` (a.k.a. the "unit" type) and `Serialize` trait coherence and overlap.
/// We cannot currently implement a trait for `()` and generic `Serialize` because `()` itself implements
/// `Serialize`. Rust's `"specialization"`, which would let us account for this, is an unstable, nightly-only
/// feature. So instead we manually mark all types other than `()` with `SerializeMarker` as a hack to
/// distinguish the two.
pub trait SerializeMarker : Serialize {}
impl CommandSuccessResponseWrapper<()> {
pub fn empty_success() -> Self {
Self::empty()
}
pub fn empty() -> Self {
CommandSuccessResponseWrapper {
status: CommandSuccessStatus::Success,
@@ -68,13 +40,6 @@ impl CommandSuccessResponseWrapper<()> {
}
}
/// NB: This is a hack because of `()` (a.k.a. the "unit" type) and `Serialize` trait coherence and overlap.
/// We cannot currently implement a trait for `()` and generic `Serialize` because `()` itself implements
/// `Serialize`. Rust's `"specialization"`, which would let us account for this, is an unstable, nightly-only
/// feature. So instead we manually mark all types other than `()` with `SerializeMarker` as a hack to
/// distinguish the two.
trait SerializeMarker : Serialize {}
// NB: () is Serialize! So we can't broadly implement just `T: Serialize`.
impl From<()> for CommandSuccessResponseWrapper<()> {
fn from(_: ()) -> Self {
@@ -108,20 +73,9 @@ impl From<&str> for CommandSuccessResponseWrapper<String> {
}
}
impl From<&str> for CommandErrorResponseWrapper<(), ()> {
fn from(value: &str) -> Self {
CommandErrorResponseWrapper {
status: CommandErrorStatus::BadRequest, // NB: Default to 500 error.
error_message: Some(value.to_string()),
error_type: None,
error_details: None,
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_empty_tuple_serialize() {
let response = super::CommandSuccessResponseWrapper::<()>::from(());
@@ -0,0 +1,51 @@
use crate::core::events::sendable_event_error::SendableEventError;
use log::info;
use serde::Serialize;
use std::fmt::Debug;
use tauri::{AppHandle, Emitter};
// Tagged enums here are waaay too "Rustic"
// #[derive(Clone, Serialize)]
// #[serde(rename_all = "camelCase", rename_all_fields = "camelCase", tag = "event", content = "data")]
// pub enum BasicEvent<T: Serialize> {
// Success(T),
// Failure(T),
// }
#[derive(Copy, Clone, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum BasicEventStatus {
Success,
Failure,
}
/// Wrap the event with "status" and "data" fields.
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
struct BasicEventWrapper<T: Serialize + Debug> {
status: BasicEventStatus,
data: T,
}
pub trait BasicSendableEvent : Clone + Debug + Serialize {
/// This is the name of the event that the frontend subscribes to.
const FRONTEND_EVENT_NAME: &'static str;
/// This is the status of the event: Success, Failure, etc.
const EVENT_STATUS: BasicEventStatus;
/// Default implementation of send().
/// This serializes and sends the event to the frontend.
fn send(&self, app: &AppHandle) -> Result<(), SendableEventError> {
let wrapped = BasicEventWrapper {
status: Self::EVENT_STATUS,
data: self.clone(),
};
info!("Sending event to frontend: {} ; payload = {:?}", Self::FRONTEND_EVENT_NAME, wrapped);
let result = app.emit(Self::FRONTEND_EVENT_NAME, wrapped);
if let Err(err) = result {
return Err(SendableEventError::from(err));
}
Ok(())
}
}
@@ -0,0 +1,29 @@
use serde::Serialize;
#[derive(Debug, Copy, Clone, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum GenerationServiceProvider {
Artcraft,
Fal,
Sora,
}
#[derive(Debug, Copy, Clone, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum GenerationModel {
#[serde(rename = "kling_1.6")]
Kling1_6,
#[serde(rename = "kling_2.0")]
Kling2_0,
Sora,
}
#[derive(Debug, Copy, Clone, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum GenerationAction {
GenerateImage,
GenerateVideo,
RemoveBackground,
#[serde(rename = "image_to_3d")]
ImageTo3d,
}
@@ -0,0 +1,16 @@
use crate::core::events::basic_sendable_event_trait::{BasicEventStatus, BasicSendableEvent};
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use serde_derive::Serialize;
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct GenerationCompleteEvent {
pub action: Option<GenerationAction>,
pub service: GenerationServiceProvider,
pub model: Option<GenerationModel>,
}
impl BasicSendableEvent for GenerationCompleteEvent {
const FRONTEND_EVENT_NAME: &'static str = "generation-complete-event";
const EVENT_STATUS: BasicEventStatus = BasicEventStatus::Success;
}
@@ -0,0 +1,39 @@
use crate::core::events::basic_sendable_event_trait::{BasicEventStatus, BasicSendableEvent};
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use serde_derive::Serialize;
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct GenerationEnqueueFailureEvent {
pub action: GenerationAction,
pub service: GenerationServiceProvider,
pub model: Option<GenerationModel>,
/// User-facing reason for the failure, if available.
pub reason: Option<String>,
}
impl BasicSendableEvent for GenerationEnqueueFailureEvent {
const FRONTEND_EVENT_NAME: &'static str = "generation-enqueue-failure-event";
const EVENT_STATUS: BasicEventStatus = BasicEventStatus::Failure;
}
impl GenerationEnqueueFailureEvent {
pub fn no_artcraft_credentials(action: GenerationAction) -> Self {
Self {
action,
service: GenerationServiceProvider::Artcraft,
model: None,
reason: Some("Please log into Artcraft.".to_string()),
}
}
pub fn no_fal_api_key(action: GenerationAction) -> Self {
Self {
action,
service: GenerationServiceProvider::Fal,
model: None,
reason: Some("No FAL API key is set. Configure this in your settings.".to_string()),
}
}
}
@@ -0,0 +1,16 @@
use crate::core::events::basic_sendable_event_trait::{BasicEventStatus, BasicSendableEvent};
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use serde_derive::Serialize;
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct GenerationEnqueueSuccessEvent {
pub action: GenerationAction,
pub service: GenerationServiceProvider,
pub model: Option<GenerationModel>,
}
impl BasicSendableEvent for GenerationEnqueueSuccessEvent {
const FRONTEND_EVENT_NAME: &'static str = "generation-enqueue-success-event";
const EVENT_STATUS: BasicEventStatus = BasicEventStatus::Success;
}
@@ -0,0 +1,19 @@
use crate::core::events::basic_sendable_event_trait::{BasicEventStatus, BasicSendableEvent};
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use serde_derive::Serialize;
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct GenerationFailedEvent {
pub action: GenerationAction,
pub service: GenerationServiceProvider,
pub model: Option<GenerationModel>,
/// User-facing reason for the failure, if available.
pub reason: Option<String>,
}
impl BasicSendableEvent for GenerationFailedEvent {
const FRONTEND_EVENT_NAME: &'static str = "generation-failed-event";
const EVENT_STATUS: BasicEventStatus = BasicEventStatus::Failure;
}
@@ -0,0 +1,5 @@
pub mod common;
pub mod generation_enqueue_failure_event;
pub mod generation_enqueue_success_event;
pub mod generation_complete_event;
pub mod generation_failed_event;
@@ -0,0 +1,4 @@
pub mod basic_sendable_event_trait;
pub mod generation_events;
pub mod sendable_event_error;
pub mod sendable_event_trait;
@@ -1,15 +1,17 @@
use crate::core::events::sendable_event_error::SendableEventError;
use log::info;
use crate::events::sendable_event_error::SendableEventError;
use serde::Serialize;
use tauri::{AppHandle, Emitter};
/// Implement this on a Serializable object to be able to send it as an event to the frontend.
#[deprecated(note = "Use BasicSendableEvent instead as it wraps the event with status and data fields.")]
pub trait SendableEvent : Serialize {
/// This is the name of the event that the frontend subscribes to.
const FRONTEND_EVENT_NAME: &'static str;
/// Default implementation of send().
/// This serializes and sends the event to the frontend.
#[deprecated(note = "Use BasicSendableEvent instead as it wraps the event with status and data fields.")]
fn send(&self, app: &AppHandle) -> Result<(), SendableEventError> {
info!("Sending event to frontend: {}", Self::FRONTEND_EVENT_NAME);
let result = app.emit(Self::FRONTEND_EVENT_NAME, self);
+6
View File
@@ -0,0 +1,6 @@
pub mod commands;
pub mod events;
pub mod state;
pub mod threads;
pub mod utils;
pub mod windows;
@@ -1,6 +1,6 @@
use crate::state::app_preferences::app_preferences_serializable::AppPreferencesSerializable;
use crate::state::app_preferences::preferred_download_directory::{PreferredDownloadDirectory, SystemDownloadDirectory};
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::app_preferences::app_preferences_serializable::AppPreferencesSerializable;
use crate::core::state::app_preferences::preferred_download_directory::{PreferredDownloadDirectory, SystemDownloadDirectory};
use crate::core::state::data_dir::app_data_root::AppDataRoot;
#[derive(Clone)]
pub struct AppPreferences {
@@ -1,5 +1,5 @@
use crate::state::app_preferences::app_preferences::AppPreferences;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::app_preferences::app_preferences::AppPreferences;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use memory_store::clone_cell::CloneCell;
pub type AppPreferencesManager = CloneCell<AppPreferences>;
@@ -1,6 +1,6 @@
use crate::state::app_preferences::app_preferences::AppPreferences;
use crate::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::app_preferences::app_preferences::AppPreferences;
use crate::core::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use errors::AnyhowResult;
use serde_derive::{Deserialize, Serialize};
@@ -58,8 +58,8 @@ impl Display for SystemDownloadDirectory {
#[cfg(test)]
mod tests {
use crate::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
use crate::state::app_preferences::preferred_download_directory::SystemDownloadDirectory;
use crate::core::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
use crate::core::state::app_preferences::preferred_download_directory::SystemDownloadDirectory;
use std::str::FromStr;
mod json {
@@ -1,4 +1,4 @@
use crate::state::data_dir::trait_data_subdir::DataSubdir;
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use chrono::{DateTime, Local};
use std::path::{Path, PathBuf};
@@ -1,4 +1,4 @@
use crate::state::data_dir::trait_data_subdir::DataSubdir;
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use std::path::{Path, PathBuf};
#[derive(Clone)]
@@ -40,4 +40,8 @@ impl AppCredentialsDir {
pub fn get_storyteller_session_cookie_file_path(&self) -> PathBuf {
self.path.join("artcraft_session.txt")
}
pub fn get_fal_api_key_file_path(&self) -> PathBuf {
self.path.join("fal_api_key.txt")
}
}
@@ -1,12 +1,12 @@
use crate::state::data_dir::app_assets_dir::AppAssetsDir;
use crate::state::data_dir::app_credentials_dir::AppCredentialsDir;
use crate::state::data_dir::app_downloads_dir::AppDownloadsDir;
use crate::state::data_dir::app_settings_dir::AppSettingsDir;
use crate::state::data_dir::app_weights_dir::AppWeightsDir;
use crate::state::data_dir::temporary_dir::TemporaryDir;
use crate::state::data_dir::trait_data_subdir::DataSubdir;
use crate::state::expanduser::expanduser;
use crate::state::os_platform::OsPlatform;
use crate::core::state::data_dir::app_assets_dir::AppAssetsDir;
use crate::core::state::data_dir::app_credentials_dir::AppCredentialsDir;
use crate::core::state::data_dir::app_downloads_dir::AppDownloadsDir;
use crate::core::state::data_dir::app_settings_dir::AppSettingsDir;
use crate::core::state::data_dir::app_weights_dir::AppWeightsDir;
use crate::core::state::data_dir::temporary_dir::TemporaryDir;
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use crate::core::state::expanduser::expanduser;
use crate::core::state::os_platform::OsPlatform;
use anyhow::anyhow;
use directories::UserDirs;
use std::path::{Path, PathBuf};
@@ -1,4 +1,4 @@
use crate::state::data_dir::trait_data_subdir::DataSubdir;
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use std::path::{Path, PathBuf};
#[derive(Clone)]
@@ -1,5 +1,5 @@
use crate::state::app_preferences::app_preferences::AppPreferences;
use crate::state::data_dir::trait_data_subdir::DataSubdir;
use crate::core::state::app_preferences::app_preferences::AppPreferences;
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use errors::AnyhowResult;
use serde::Serialize;
use std::fs::OpenOptions;
@@ -1,4 +1,4 @@
use crate::state::data_dir::trait_data_subdir::DataSubdir;
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use std::path::{Path, PathBuf};
#[derive(Clone)]
@@ -1,4 +1,4 @@
use crate::state::data_dir::trait_data_subdir::DataSubdir;
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use std::path::{Path, PathBuf};
use tempdir::TempDir;
use tempfile::{Builder, NamedTempFile};
@@ -35,4 +35,17 @@ impl TemporaryDir {
.tempfile_in(&self.path)?;
Ok(tempfile)
}
pub fn new_named_temp_file_with_extension(&self, extension: &str) -> Result<NamedTempFile, std::io::Error> {
let extension = if extension.starts_with(".") {
extension.to_string()
} else {
format!(".{}", extension)
};
let tempfile = Builder::new()
.prefix("temp_")
.suffix(&extension)
.tempfile_in(&self.path)?;
Ok(tempfile)
}
}
@@ -43,7 +43,7 @@ pub fn home_dir() -> io::Result<PathBuf> {
#[cfg(test)]
mod tests {
use crate::state::expanduser::expanduser;
use crate::core::state::expanduser::expanduser;
use std::env;
use std::path::PathBuf;
@@ -1,5 +1,5 @@
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::windows::main_window::constants::MAIN_WINDOW_NAME;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::windows::main_window::constants::MAIN_WINDOW_NAME;
use errors::AnyhowResult;
use serde_derive::{Deserialize, Serialize};
use std::fs::OpenOptions;
@@ -1,6 +1,6 @@
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::utils::best_window_size_heuristic::best_window_size_heuristic;
use crate::windows::main_window::constants::MAIN_WINDOW_NAME;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::utils::best_window_size_heuristic::best_window_size_heuristic;
use crate::core::windows::main_window::constants::MAIN_WINDOW_NAME;
use errors::AnyhowResult;
use serde_derive::{Deserialize, Serialize};
use std::fs::OpenOptions;
@@ -4,6 +4,4 @@ pub mod data_dir;
pub mod expanduser;
pub mod main_window_position;
pub mod main_window_size;
pub mod os_platform;
pub mod sora;
pub mod storyteller;
pub mod os_platform;
@@ -1,11 +1,11 @@
use crate::state::app_startup_time::AppStartupTime;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::main_window_position::MainWindowPosition;
use crate::state::main_window_size::MainWindowSize;
use crate::state::storyteller::storyteller_credential_manager::StorytellerCredentialManager;
use crate::threads::main_window_thread::persist_storyteller_cookies_task::persist_storyteller_cookies_task;
use crate::threads::main_window_thread::persist_window_position_task::persist_window_position_task;
use crate::threads::main_window_thread::persist_window_resize_task::persist_window_resize_task;
use crate::core::state::app_startup_time::AppStartupTime;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::main_window_position::MainWindowPosition;
use crate::core::state::main_window_size::MainWindowSize;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use crate::core::threads::main_window_thread::persist_storyteller_cookies_task::persist_storyteller_cookies_task;
use crate::core::threads::main_window_thread::persist_window_position_task::persist_window_position_task;
use crate::core::threads::main_window_thread::persist_window_resize_task::persist_window_resize_task;
use errors::AnyhowResult;
use log::{error, info};
use memory_store::clone_slot::CloneSlot;
@@ -23,6 +23,15 @@ pub async fn main_window_thread(
let window_pos_slot: CloneSlot<MainWindowPosition> = CloneSlot::empty();
let app_startup_time = AppStartupTime::new();
// Emit debugging information to the logs (do not remove this!)
info!("git commit id: {:?}", build_metadata::git_commit_id()
.unwrap_or_else(|| "unknown"));
info!("git commit timestamp: {:?}", build_metadata::git_commit_timestamp()
.map(|t| t.to_string())
.unwrap_or_else(|| "unknown".to_string()));
info!("build timestamp: {:?}", build_metadata::build_timestamp()
.to_string());
loop {
for (window_name, window) in app.windows() {
if window_name == MAIN_WINDOW_NAME {
@@ -1,6 +1,6 @@
use crate::state::app_startup_time::AppStartupTime;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::storyteller::storyteller_credential_manager::StorytellerCredentialManager;
use crate::core::state::app_startup_time::AppStartupTime;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use anyhow::anyhow;
use chrono::TimeDelta;
use errors::AnyhowResult;
@@ -1,7 +1,7 @@
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::main_window_position::MainWindowPosition;
use crate::state::main_window_size::MainWindowSize;
use crate::utils::best_window_size_heuristic::best_window_size_heuristic;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::main_window_position::MainWindowPosition;
use crate::core::state::main_window_size::MainWindowSize;
use crate::core::utils::best_window_size_heuristic::best_window_size_heuristic;
use errors::AnyhowResult;
use log::info;
use memory_store::clone_slot::CloneSlot;
@@ -1,6 +1,6 @@
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::main_window_size::MainWindowSize;
use crate::utils::best_window_size_heuristic::best_window_size_heuristic;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::main_window_size::MainWindowSize;
use crate::core::utils::best_window_size_heuristic::best_window_size_heuristic;
use errors::AnyhowResult;
use log::info;
use memory_store::clone_slot::CloneSlot;
@@ -0,0 +1,2 @@
pub mod discord_presence_thread;
pub mod main_window_thread;
@@ -0,0 +1,29 @@
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::utils::get_url_file_extension::get_url_file_extension;
use crate::core::utils::simple_http_download_to_tempfile::simple_http_download_to_tempfile;
use errors::AnyhowResult;
use storyteller_client::media_files::get_media_file::get_media_file;
use storyteller_client::utils::api_host::ApiHost;
use tempfile::NamedTempFile;
use tokens::tokens::media_files::MediaFileToken;
// TODO: Better more concrete error handling
pub async fn download_media_file_to_temp_dir(app_data_root: &AppDataRoot, token: &MediaFileToken) -> AnyhowResult<NamedTempFile> {
let response = get_media_file(&ApiHost::Storyteller, token).await?;
let media_file_url = &response.media_file.media_links.cdn_url;
let extension_with_dot = get_url_file_extension(media_file_url)
.map(|ext| format!(".{}", ext))
.unwrap_or_else(|| ".png".to_string()); // TODO: Better default extension passed by caller as heuristic.
//let filename = format!("{}{}", response.media_file.token.as_str(), extension_with_dot);
//let filename = app_data_root.downloads_dir().path().join(&filename);
let mut file = app_data_root.temp_dir().new_named_temp_file_with_extension(&extension_with_dot)?;
simple_http_download_to_tempfile(&media_file_url, &mut file).await?;
Ok(file) // NB: Must return TempFile to not drop / delete it
}
@@ -0,0 +1,20 @@
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use errors::AnyhowResult;
use std::io::Write;
use tempfile::NamedTempFile;
use url::Url;
pub async fn download_url_to_temp_dir(url: &str, app_data_root: &AppDataRoot) -> AnyhowResult<NamedTempFile> {
let url = Url::parse(&url)?;
let response = reqwest::get(url.clone()).await?;
let response_bytes = response.bytes().await?;
let ext = url.path().split(".").last().unwrap_or("png");
let mut temp_file = app_data_root.temp_dir().new_named_temp_file_with_extension(ext)?;
temp_file.write_all(&response_bytes)?;
Ok(temp_file)
}
@@ -1,7 +1,7 @@
//! Implemented from https://github.com/huggingface/hf_transfer/blob/main/src/lib.rs
use crate::utils::large_file_transfer::error::Error;
use crate::utils::large_file_transfer::util::{exponential_backoff, BASE_WAIT_TIME, MAX_WAIT_TIME};
use crate::core::utils::large_file_transfer::error::Error;
use crate::core::utils::large_file_transfer::util::{exponential_backoff, BASE_WAIT_TIME, MAX_WAIT_TIME};
use futures::stream::FuturesUnordered;
use futures::StreamExt;
use log::info;
@@ -1,7 +1,7 @@
//! Implemented from https://github.com/huggingface/hf_transfer/blob/main/src/lib.rs
use crate::utils::large_file_transfer::error::Error;
use crate::utils::large_file_transfer::util::{exponential_backoff, BASE_WAIT_TIME, MAX_WAIT_TIME};
use crate::core::utils::large_file_transfer::error::Error;
use crate::core::utils::large_file_transfer::util::{exponential_backoff, BASE_WAIT_TIME, MAX_WAIT_TIME};
use futures::stream::FuturesUnordered;
use futures::StreamExt;
use reqwest::header::CONTENT_LENGTH;
@@ -1,9 +1,12 @@
pub mod best_window_size_heuristic;
pub mod clear_all_webview_cookies;
pub mod download_media_file_to_temp_dir;
pub mod download_url_to_temp_dir;
pub mod get_url_file_extension;
pub mod image;
pub mod large_file_transfer;
pub mod object_store;
pub mod save_base64_image_to_temp_dir;
pub mod simple_http_download;
pub mod sora;
pub mod simple_http_download_to_tempfile;
pub mod webview_unsafe;

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