Compare commits

...
Author SHA1 Message Date
CrossPr0duct 617aa75dc5 Background removal race condition fixed. 2025-06-19 13:13:30 -04:00
BFlat 10603f7c86 feature/lightbox drag (#677)
- draggable and resizable lightbox

Bug found: if lightbox is open, gallery modal cannot open the filter
dropdown until lightbox is closed.
2025-06-19 00:41:21 +07:00
5e8c524f84 Feature/login fix (#678)
Co-authored-by: CrossPr0duct <alicextensor@gmail.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
2025-06-18 10:59:43 -04:00
Brandon Thomas 05f0fe1216 fal api endpoints in api gateway (#676) 2025-06-17 01:18:36 -04:00
ArEnScandCrossPr0duct 1f44f7e929 Feature/fixes (#675)
Co-authored-by: CrossPr0duct <alicextensor@gmail.com>
2025-06-16 23:58:35 -04:00
BFlat c56cc8700c gif tooltips for 2d and 3d (#674) 2025-06-16 14:49:02 +07:00
Brandon Thomas caaff569a6 fal client - lots of api wrappers (#673) 2025-06-16 01:08:27 -04:00
BFlat 3b0ed7a8ba feature: create 3d model modal (#668)
- Create 3d model from image works now (accessed via wand button next to
the + asset button).
- Modal pops up, or if you dont have a FAL api set in settings it'll pop
up an action reminder to set it first.
- You can choose image from library (opens gallery select mode) or
upload an image from computer.
- Pressing create enqueues the image to 3d and closes the modal.
Spinning loading toast on the top right says Generating... until it
finishes.
- When done, currently it just pops up in the library and you can drag
it onto the scene yourself.

**Current bugs/missing features:**
1. No 3D object image thumbnail in gallery
2. If you open up the gallery and try add the newly generated 3d object
to the scene, it will be stuck at "loading" text because of a CORS error
(not sure why). Pressing the button to refresh the gallery before
dragging it in seems to fix that though.
2025-06-14 20:44:03 -04:00
Brandon Thomas ea1905cfcf tauri bg removal fix (#672) 2025-06-14 01:25:26 -04:00
ArEnScandCrossPr0duct d1f28877ca Feature/add background remove (#670)
Co-authored-by: CrossPr0duct <alicextensor@gmail.com>
2025-06-13 19:07:41 -04:00
BFlat 1b8d8cbd97 login error messages now show properly (#671) 2025-06-13 17:33:36 +07:00
bflatastic 8be5013260 re-enable login modal 2025-06-13 16:29:09 +07:00
bflatastic 3c9b81c3ea bug fix: 3d promptbox events 2025-06-12 22:35:27 +07:00
BFlat 9d1b7d22ee bug fix: ensure no keyboard events leak through to the canvas (#669) 2025-06-12 22:30:43 +07:00
ArEnScandCrossPr0duct b046f6ff2a Feature/Zlayer and opacity issues (#667)
This adds all the z-layer work and context menus back for composition.
Also fixes the alpha issues and removes menus that are not needed for
now.

---------

Co-authored-by: CrossPr0duct <alicextensor@gmail.com>
2025-06-11 13:43:29 -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
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
3171 changed files with 146526 additions and 40432 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
+2116 -4747
View File
File diff suppressed because it is too large Load Diff
+67 -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",
@@ -46,14 +43,15 @@ members = [
"crates/lib/data/crockford",
"crates/lib/data/datetimes",
"crates/lib/data/primitives",
"crates/lib/data/web_base64",
"crates/lib/deprecated/config", # TODO(bt, 2023-02-13): Deprecate or repurpose
"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 +61,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 +79,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 +115,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 +171,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 +228,5 @@ 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" }
web_base64= { path = "crates/lib/data/web_base64" }
@@ -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"
+10 -10
View File
@@ -23,23 +23,25 @@ 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 }
web_base64 = { 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 +54,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 +72,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://*"
}
]
},
@@ -0,0 +1,48 @@
use std::error::Error;
use std::fmt::{Display, Formatter};
use storyteller_client::error::storyteller_error::StorytellerError;
#[derive(Debug)]
pub enum ArtcraftError {
AnyhowError(anyhow::Error),
DecodeError(base64::DecodeError),
IoError(std::io::Error),
StorytellerError(StorytellerError),
}
impl Error for ArtcraftError {}
impl Display for ArtcraftError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::AnyhowError(e) => write!(f, "AnyhowError: {:?}", e),
Self::DecodeError(e) => write!(f, "DecodeError: {:?}", e),
Self::IoError(e) => write!(f, "IoError: {:?}", e),
Self::StorytellerError(e) => write!(f, "StorytellerError: {:?}", e),
}
}
}
impl From<anyhow::Error> for ArtcraftError {
fn from(value: anyhow::Error) -> Self {
Self::AnyhowError(value)
}
}
impl From<base64::DecodeError> for ArtcraftError {
fn from(value: base64::DecodeError) -> Self {
Self::DecodeError(value)
}
}
impl From<std::io::Error> for ArtcraftError {
fn from(value: std::io::Error) -> Self {
Self::IoError(value)
}
}
impl From<StorytellerError> for ArtcraftError {
fn from(value: StorytellerError) -> Self {
Self::StorytellerError(value)
}
}
@@ -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,195 @@
use crate::core::commands::enqueue::image::handle_image_fal::handle_image_fal;
use crate::core::commands::enqueue::image::handle_image_sora::handle_image_sora;
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
use crate::core::commands::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_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs};
use 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 crate::core::commands::enqueue::image::handle_image_artcraft::handle_image_artcraft;
use crate::core::commands::enqueue::object::handle_object_artcraft::handle_object_artcraft;
#[derive(Deserialize)]
pub struct EnqueueTextToImageRequest {
/// 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,
/// Needs to be logged into Artcraft
NeedsStorytellerCredentials,
}
#[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::GptImage1) => {
handle_image_sora(&app, request, sora_creds_manager, sora_task_queue).await?;
return Ok(());
}
_ => {
// Fall-through
}
};
if fal_creds_manager.has_apparent_api_token()? {
handle_image_fal(&app, request, fal_creds_manager, fal_task_queue).await?;
} else {
handle_image_artcraft(request, &app, app_data_root, storyteller_creds_manager).await?;
}
Ok(())
}
@@ -0,0 +1,97 @@
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, GenerationModel, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::core::state::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::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageRequest;
use artcraft_api_defs::generate::object::generate_hunyuan_2_image_to_3d::GenerateHunyuan2ImageTo3dRequest;
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProImageToVideoRequest;
use fal_client::creds::fal_api_key::FalApiKey;
use fal_client::requests::queue::enqueue_hunyuan2_image_to_3d::{enqueue_hunyuan2_image_to_3d, Hunyuan2Args};
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs};
use idempotency::uuid::generate_random_uuid;
use log::{error, info, warn};
use storyteller_client::generate::image::generate_flux_pro_11_ultra_text_to_image::generate_flux_pro_11_ultra_text_to_image;
use storyteller_client::generate::object::generate_hunyuan2_image_to_3d::generate_hunyuan2_image_to_3d;
use storyteller_client::generate::video::generate_kling_16_pro_image_to_video::generate_kling_16_pro_image_to_video;
use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, State};
pub async fn handle_image_artcraft(
request: EnqueueTextToImageRequest,
app: &AppHandle,
app_data_root: &AppDataRoot,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<(), InternalImageError> {
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::GenerateImage);
if let Err(err) = event.send(&app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
return Err(InternalImageError::NeedsStorytellerCredentials);
},
};
info!("Calling Artcraft text to image...");
let uuid_idempotency_token = generate_random_uuid();
let mut selected_model = None;
let job_token = match request.model {
None => {
return Err(InternalImageError::NoModelSpecified);
}
Some(EnqueueTextToImageModel::GptImage1) => {
return Err(InternalImageError::AnyhowError(anyhow!("wrong logic: artcraft is handling sora images")));
}
Some(EnqueueTextToImageModel::Recraft3) => {
return Err(InternalImageError::AnyhowError(anyhow!("not yet implemented in Artcraft")));
}
Some(EnqueueTextToImageModel::FluxProUltra) => {
info!("enqueue Flux Pro Ultra");
selected_model = Some(GenerationModel::FluxPro11Ultra);
let request = GenerateFluxPro11UltraTextToImageRequest {
uuid_idempotency_token,
prompt: request.prompt,
aspect_ratio: None,
num_images: None,
};
let result = generate_flux_pro_11_ultra_text_to_image(
&ApiHost::Storyteller,
Some(&creds),
request,
).await;
match result {
Ok(enqueued) => {
info!("Successfully enqueued Artcraft flux pro ultra text to image generation");
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft flux pro ultra text to image generation: {:?}", err);
return Err(InternalImageError::StorytellerError(err));
}
}
}
};
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_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs};
use log::{error, info, warn};
use tauri::AppHandle;
pub async fn handle_image_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_11_ultra_text_to_image(FluxPro11UltraTextToImageArgs {
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_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs};
use log::{error, info, 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_image_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,69 @@
use crate::core::artcraft_error::ArtcraftError;
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::storyteller_error::StorytellerError;
#[derive(Debug)]
pub enum InternalImageError {
NoModelSpecified,
NeedsFalApiKey,
NeedsSoraCredentials,
NeedsStorytellerCredentials,
FalError(FalErrorPlus),
SoraError(SoraError),
AnyhowError(AnyhowError),
StorytellerError(StorytellerError),
DecodeError(DecodeError),
IoError(std::io::Error),
ArtcraftError(ArtcraftError),
}
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<StorytellerError> for InternalImageError {
fn from(value: StorytellerError) -> Self {
Self::StorytellerError(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)
}
}
impl From<ArtcraftError> for InternalImageError {
fn from(value: ArtcraftError) -> Self {
match value {
ArtcraftError::AnyhowError(e) => Self::AnyhowError(e),
ArtcraftError::DecodeError(e) => Self::DecodeError(e),
ArtcraftError::IoError(e) => Self::IoError(e),
ArtcraftError::StorytellerError(e) => Self::StorytellerError(e),
_ => Self::ArtcraftError(value),
}
}
}
@@ -0,0 +1,5 @@
pub mod enqueue_text_to_image_command;
pub mod handle_image_artcraft;
pub mod handle_image_fal;
pub mod handle_image_sora;
pub mod internal_image_error;
@@ -0,0 +1,3 @@
pub mod image;
pub mod object;
pub mod video;
@@ -0,0 +1,177 @@
use crate::core::commands::enqueue::object::handle_object_artcraft::handle_object_artcraft;
use crate::core::commands::enqueue::object::handle_object_fal::handle_object_fal;
use crate::core::commands::enqueue::object::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_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs};
use 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;
#[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_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs};
use idempotency::uuid::generate_random_uuid;
use log::{error, info, warn};
use storyteller_client::generate::object::generate_hunyuan2_image_to_3d::generate_hunyuan2_image_to_3d;
use storyteller_client::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_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs};
use log::{error, info, 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::ImageTo3d);
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,61 @@
use crate::core::artcraft_error::ArtcraftError;
use base64::DecodeError;
use errors::AnyhowError;
use fal_client::error::fal_error_plus::FalErrorPlus;
use storyteller_client::error::storyteller_error::StorytellerError;
#[derive(Debug)]
pub enum InternalObjectError {
NoModelSpecified,
NeedsFalApiKey,
NeedsSoraCredentials,
NeedsStorytellerCredentials,
FalError(FalErrorPlus),
AnyhowError(AnyhowError),
StorytellerError(StorytellerError),
DecodeError(DecodeError),
IoError(std::io::Error),
ArtcraftError(ArtcraftError),
}
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<StorytellerError> for InternalObjectError {
fn from(value: StorytellerError) -> Self {
Self::StorytellerError(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)
}
}
impl From<ArtcraftError> for InternalObjectError {
fn from(value: ArtcraftError) -> Self {
match value {
ArtcraftError::AnyhowError(e) => Self::AnyhowError(e),
ArtcraftError::DecodeError(e) => Self::DecodeError(e),
ArtcraftError::IoError(e) => Self::IoError(e),
ArtcraftError::StorytellerError(e) => Self::StorytellerError(e),
_ => Self::ArtcraftError(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,179 @@
use crate::core::commands::enqueue::object::handle_object_artcraft::handle_object_artcraft;
use crate::core::commands::enqueue::object::handle_object_fal::handle_object_fal;
use crate::core::commands::enqueue::video::handle_video_artcraft::handle_video_artcraft;
use crate::core::commands::enqueue::video::handle_video_fal::handle_video_fal;
use crate::core::commands::enqueue::video::internal_video_error::InternalVideoError;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use crate::core::commands::response::shorthand::Response;
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
use crate::core::events::sendable_event_trait::SendableEvent;
use crate::core::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_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs};
use 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;
#[derive(Deserialize)]
pub struct EnqueueImageToVideoRequest {
/// 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<EnqueueImageToVideoModel>,
}
#[derive(Deserialize, Debug, Copy, Clone)]
#[serde(rename_all = "snake_case")]
pub enum EnqueueImageToVideoModel {
#[serde(rename = "kling1_6")]
Kling16,
}
#[derive(Serialize)]
pub struct EnqueueImageToVideoSuccessResponse {
}
impl SerializeMarker for EnqueueImageToVideoSuccessResponse {}
#[derive(Serialize, Debug)]
#[serde(rename_all = "snake_case")]
pub enum EnqueueImageToVideoErrorType {
/// 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_video_command(
app: AppHandle,
request: EnqueueImageToVideoRequest,
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<EnqueueImageToVideoSuccessResponse, EnqueueImageToVideoErrorType, ()> {
info!("enqueue_image_to_video_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 = EnqueueImageToVideoErrorType::ServerError;
let mut error_message = "A server error occurred. Please try again. If it continues, please tell our staff about the problem.";
match err {
InternalVideoError::NoModelSpecified => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueImageToVideoErrorType::ModelNotSpecified;
error_message = "No model specified for image generation";
}
InternalVideoError::NeedsFalApiKey => {
status = CommandErrorStatus::Unauthorized;
error_type = EnqueueImageToVideoErrorType::NeedsFalApiKey;
error_message = "You need to set a FAL api key";
},
InternalVideoError::NeedsStorytellerCredentials => {
status = CommandErrorStatus::Unauthorized;
error_type = EnqueueImageToVideoErrorType::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(EnqueueImageToVideoSuccessResponse {}.into())
}
}
}
pub async fn handle_request(
app: &AppHandle,
request: EnqueueImageToVideoRequest,
app_data_root: &AppDataRoot,
fal_creds_manager: &FalCredentialManager,
storyteller_creds_manager: &StorytellerCredentialManager,
fal_task_queue: &FalTaskQueue,
) -> Result<(), InternalVideoError> {
if fal_creds_manager.has_apparent_api_token()? {
handle_video_fal(&app, app_data_root, request, fal_creds_manager, fal_task_queue).await?;
} else {
handle_video_artcraft(request, &app, app_data_root, storyteller_creds_manager).await?;
}
Ok(())
}
@@ -0,0 +1,139 @@
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageModel, EnqueueTextToImageRequest};
use crate::core::commands::enqueue::object::enqueue_image_to_3d_object_command::{EnqueueImageTo3dObjectModel, EnqueueImageTo3dObjectRequest};
use crate::core::commands::enqueue::video::enqueue_image_to_video_command::{EnqueueImageToVideoModel, EnqueueImageToVideoRequest};
use crate::core::commands::enqueue::video::internal_video_error::InternalVideoError;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::core::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 artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::{GenerateKling16ProAspectRatio, GenerateKling16ProImageToVideoRequest};
use fal_client::creds::fal_api_key::FalApiKey;
use fal_client::requests::queue::enqueue_hunyuan2_image_to_3d::{enqueue_hunyuan2_image_to_3d, Hunyuan2Args};
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs};
use fal_client::requests::queue::video_gen::enqueue_kling_16_pro_image_to_video::Kling16ProAspectRatio;
use idempotency::uuid::generate_random_uuid;
use log::{error, info, warn};
use storyteller_client::generate::object::generate_hunyuan2_image_to_3d::generate_hunyuan2_image_to_3d;
use storyteller_client::generate::video::generate_kling_16_pro_image_to_video::generate_kling_16_pro_image_to_video;
use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, State};
pub async fn handle_video_artcraft(
request: EnqueueImageToVideoRequest,
app: &AppHandle,
app_data_root: &AppDataRoot,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<(), InternalVideoError> {
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::GenerateVideo);
if let Err(err) = event.send(&app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
return Err(InternalVideoError::NeedsStorytellerCredentials);
},
};
//let api_key = fal_creds_manager.get_key_required()
// .map_err(|err| {
// error!("EnqueueTextToImage FAL api key required: {:?}", err);
// InternalVideoError::NeedsFalApiKey
// })?;
info!("Calling Artcraft image to video...");
let uuid_idempotency_token = generate_random_uuid();
let mut selected_model = None;
let job_token = match request.model {
None => {
return Err(InternalVideoError::NoModelSpecified);
}
Some(EnqueueImageToVideoModel::Kling16) => {
info!("enqueue Kling 1.6");
selected_model = Some(GenerationModel::Kling1_6);
let request = GenerateKling16ProImageToVideoRequest {
uuid_idempotency_token,
media_file_token: request.image_media_token,
aspect_ratio: Some(GenerateKling16ProAspectRatio::WideSixteenNine),
prompt: None,
duration: None,
};
let result = generate_kling_16_pro_image_to_video(
&ApiHost::Storyteller,
Some(&creds),
request,
).await;
match result {
Ok(enqueued) => {
info!("Successfully enqueued Artcraft Kling 1.6 video generation");
enqueued.inference_job_token
}
Err(err) => {
error!("Failed to use Artcraft Kling 1.6 video generation: {:?}", err);
return Err(InternalVideoError::StorytellerError(err));
}
}
}
};
info!("Successfully enqueued video generation");
let event = GenerationEnqueueSuccessEvent {
action: GenerationAction::GenerateVideo,
service: GenerationServiceProvider::Artcraft,
model: selected_model,
};
if let Err(err) = event.send(app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
//if let Err(err) = fal_task_queue.insert(&enqueued) {
// error!("Failed to enqueue task: {:?}", err);
// return Err(InternalVideoError::AnyhowError(anyhow!("Failed to enqueue task: {:?}", err)));
//}
// match result {
// Ok(enqueued) => {
// }
// Err(err) => {
// error!("Failed to enqueue image to 3d: {:?}", err);
//
// let event = GenerationEnqueueFailureEvent {
// action: GenerationAction::ImageTo3d,
// service: GenerationServiceProvider::Fal,
// model: None,
// reason: None,
// };
//
// if let Err(err) = event.send(app) {
// error!("Failed to emit event: {:?}", err); // Fail open.
// }
//
// return Err(InternalVideoError::FalError(err));
// }
// }
Ok(())
}
@@ -0,0 +1,124 @@
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageModel, EnqueueTextToImageRequest};
use crate::core::commands::enqueue::video::enqueue_image_to_video_command::{EnqueueImageToVideoModel, EnqueueImageToVideoRequest};
use crate::core::commands::enqueue::video::internal_video_error::InternalVideoError;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::core::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_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enqueue_recraft3_text_to_image, Recraft3TextToImageArgs};
use fal_client::requests::queue::video_gen::enqueue_kling_16_pro_image_to_video::{enqueue_kling_16_pro_image_to_video, Kling16ProArgs, Kling16ProAspectRatio, Kling16ProDuration};
use log::{error, info, warn};
use tauri::AppHandle;
pub async fn handle_video_fal(
app: &AppHandle,
app_data_root: &AppDataRoot,
request: EnqueueImageToVideoRequest,
fal_creds_manager: &FalCredentialManager,
fal_task_queue: &FalTaskQueue,
) -> Result<(), InternalVideoError> {
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::GenerateVideo);
if let Err(err) = event.send(&app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
return Err(InternalVideoError::NeedsFalApiKey);
},
};
//let api_key = fal_creds_manager.get_key_required()
// .map_err(|err| {
// error!("EnqueueTextToImage FAL api key required: {:?}", err);
// InternalVideoError::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(InternalVideoError::AnyhowError(anyhow!("No image media token provided")));
}
info!("Calling FAL image to 3d ...");
let filename = temp_download.path().to_path_buf();
let mut selected_model = None;
let result = match request.model {
None => {
return Err(InternalVideoError::NoModelSpecified);
}
Some(EnqueueImageToVideoModel::Kling16) => {
info!("enqueue Kling 1.6 image to video");
selected_model = Some(GenerationModel::Kling1_6);
enqueue_kling_16_pro_image_to_video(Kling16ProArgs {
image_path: filename,
api_key: &api_key,
duration: Kling16ProDuration::Default,
aspect_ratio: Kling16ProAspectRatio::WideSixteenNine,
prompt: "",
}).await
}
};
match result {
Ok(enqueued) => {
info!("Successfully enqueued Kling 1.6 image to video");
let event = GenerationEnqueueSuccessEvent {
action: GenerationAction::GenerateVideo,
service: GenerationServiceProvider::Fal,
model: selected_model,
};
if let Err(err) = event.send(app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
if let Err(err) = fal_task_queue.insert(&enqueued) {
error!("Failed to enqueue task: {:?}", err);
return Err(InternalVideoError::AnyhowError(anyhow!("Failed to enqueue task: {:?}", err)));
}
}
Err(err) => {
error!("Failed to enqueue image to video: {:?}", err);
let event = GenerationEnqueueFailureEvent {
action: GenerationAction::GenerateVideo,
service: GenerationServiceProvider::Fal,
model: selected_model,
reason: None,
};
if let Err(err) = event.send(app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
return Err(InternalVideoError::FalError(err));
}
}
Ok(())
}
@@ -0,0 +1,61 @@
use crate::core::artcraft_error::ArtcraftError;
use base64::DecodeError;
use errors::AnyhowError;
use fal_client::error::fal_error_plus::FalErrorPlus;
use storyteller_client::error::storyteller_error::StorytellerError;
#[derive(Debug)]
pub enum InternalVideoError {
NoModelSpecified,
NeedsFalApiKey,
NeedsSoraCredentials,
NeedsStorytellerCredentials,
FalError(FalErrorPlus),
AnyhowError(AnyhowError),
StorytellerError(StorytellerError),
DecodeError(DecodeError),
IoError(std::io::Error),
ArtcraftError(ArtcraftError),
}
impl From<AnyhowError> for InternalVideoError {
fn from(value: AnyhowError) -> Self {
Self::AnyhowError(value)
}
}
impl From<FalErrorPlus> for InternalVideoError {
fn from(value: FalErrorPlus) -> Self {
Self::FalError(value)
}
}
impl From<StorytellerError> for InternalVideoError {
fn from(value: StorytellerError) -> Self {
Self::StorytellerError(value)
}
}
impl From<DecodeError> for InternalVideoError {
fn from(value: DecodeError) -> Self {
Self::DecodeError(value)
}
}
impl From<std::io::Error> for InternalVideoError {
fn from(value: std::io::Error) -> Self {
Self::IoError(value)
}
}
impl From<ArtcraftError> for InternalVideoError {
fn from(value: ArtcraftError) -> Self {
match value {
ArtcraftError::AnyhowError(e) => Self::AnyhowError(e),
ArtcraftError::DecodeError(e) => Self::DecodeError(e),
ArtcraftError::IoError(e) => Self::IoError(e),
ArtcraftError::StorytellerError(e) => Self::StorytellerError(e),
_ => Self::ArtcraftError(value),
}
}
}
@@ -0,0 +1,4 @@
pub mod enqueue_image_to_video_command;
pub mod handle_video_artcraft;
pub mod handle_video_fal;
pub mod internal_video_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,31 @@
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 = "flux_pro_1.1_ultra")]
FluxPro11Ultra,
#[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);
+7
View File
@@ -0,0 +1,7 @@
pub mod artcraft_error;
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};
@@ -33,6 +33,7 @@ pub struct AppDataRoot {
impl AppDataRoot {
pub fn create_default() -> anyhow::Result<Self> {
let directory = get_default_data_dir()?;
println!("App data directory: {:?}", directory);
Self::create_existing(directory)
}
@@ -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::artcraft_error::ArtcraftError;
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 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) -> Result<NamedTempFile, ArtcraftError> {
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)
}

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