Compare commits

...
Author SHA1 Message Date
Michael Chung e4bbafeae6 Used Aspect Ratio to fix.
Prompt box handles both web and desktop.
2025-04-21 16:58:02 -04:00
bflatastic e39a7f0360 bug fix: asset modal flickers when items are drag and dropped too fast 2025-04-22 01:47:03 +07:00
bflatastic 5ea90154b1 bug fix: selecting object in scene crashes (Headless UI Transition component bug) 2025-04-22 01:29:51 +07:00
bflatastic f627da6d8f updated readme: ui component import 2025-04-22 01:19:30 +07:00
ArEnScandMichael Chung 682f11c79e Tauri and login for open ai (#540)
Co-authored-by: Michael Chung <michaelchung@michaels-mbp-2.home>
2025-04-21 14:15:14 -04:00
BFlat 0bdbb5214b settings modal (#539)
new shared components:
- button
- close button
- input
- label
- modal
- select
- settings modal
2025-04-22 01:00:58 +07:00
Brandon Thomas 94f2d2ec68 fix tauri build configs 2025-04-21 13:13:23 -04:00
Michael Chung 384b841528 Add a scale factor to the capture canvas
just use a hack to get it released
2025-04-21 12:55:02 -04:00
Michael Chung 62f97317ff Add component information into README 2025-04-21 09:23:30 -04:00
BFlat 8210a9f063 update headless ui version (#538) 2025-04-21 14:36:55 +07:00
BFlat b4d603098d working upload 3d asset modal (#537)
![image](https://github.com/user-attachments/assets/01df49cd-82a2-4f67-a02c-8adec34d1794)
2025-04-21 12:23:46 +07:00
Brandon Thomas ffd86f896c this configuration can launch the 2d app 2025-04-20 15:52:41 -04:00
Brandon Thomas 30b77afb23 fixup tauri scripts (wip) 2025-04-20 01:55:48 -04:00
Brandon Thomas 0d879af3a4 Refactor, clean up tauri builds 2025-04-20 01:24:25 -04:00
Brandon Thomas 4ac71a18c5 fix tauri build 2025-04-20 01:06:37 -04:00
Brandon Thomas 81e1fce6a0 cleanup and delete old scripts and configs (#536) 2025-04-20 00:52:42 -04:00
Brandon Thomas 855ddbdf2d Refactor editor3d pathing (#535) 2025-04-20 00:38:29 -04:00
Brandon Thomas 0a7d3028c9 ignore more vim temp files 2025-04-19 23:03:55 -04:00
Brandon Thomas c5aebaffb9 Ingore files from VSCode/Cursor 2025-04-19 23:01:20 -04:00
Brandon Thomasandbflatastic 931304887f Restore Bombay's toaster PR (#533)
Co-authored-by: bflatastic <bombayapirat@gmail.com>
2025-04-19 22:56:22 -04:00
bflatastic 3dcec8bc85 editor3d: login and sign up page styling fixes 2025-04-20 08:44:43 +07:00
Brandon Thomas af30eea6f7 start removing remix (#531) 2025-04-19 14:40:58 -04:00
bflatastic df7db6b35d editor2d: link to 3d editor 2025-04-19 20:07:33 +07:00
bflatastic d228a43baf editor3d: link to go to editor2d 2025-04-19 19:49:29 +07:00
bflatastic ebcc52dd1a margin fix login and signup pages 2025-04-19 15:59:45 +07:00
bflatastic 726206fd0c editor2d: login and sign up pages restyled 2025-04-19 15:58:07 +07:00
BFlat ac896fd93a editor2d demo video on open (#532)
- demo video modal opens at first load
2025-04-19 14:30:19 +07:00
Brandon Thomas 85a4ff5c55 Revert "shared toaster component (#528)" (#530)
This reverts commit e3f15b0d93.
2025-04-18 18:42:50 -04:00
BFlat e3f15b0d93 shared toaster component (#528) 2025-04-18 14:57:51 +07:00
BFlat e31a2c0a91 fix nx project discoverability (#527) 2025-04-18 14:10:40 +07:00
Kasisnu ea85abdc62 image-gen: make unused bools in task response optional 2025-04-18 10:34:52 +05:30
Brandon Thomas 4f3c93635f don't log debug level in prod 2025-04-18 00:07:51 -04:00
ArEnScandMichael Chung 82b9b90ff9 Added the apis to the api folder it loads correctly now (#525)
Co-authored-by: Michael Chung <michaelchung@michaels-mbp-2.home>
2025-04-17 22:49:27 -04:00
bflatastic ad5ea26a81 commented out test component it was pushing the ui down 2025-04-18 08:59:53 +07:00
bflatastic 6121d118f0 reduced topbar right side gap 2025-04-18 08:54:40 +07:00
bflatastic 28ca7be96c activity update 2025-04-18 08:50:55 +07:00
Brandon Thomas a2a7f7d274 could also be a bearer token issue 2025-04-17 21:39:45 -04:00
Brandon Thomas e1255a55a0 specific error message for cookie expiration 2025-04-17 21:30:16 -04:00
9f629ee5e2 Nx working test (#524)
It works now.

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Tanish Manku <tanish2k09.dev@gmail.com>
Co-authored-by: Michael Chung <michaelchung@michaels-mbp-2.home>
2025-04-17 16:32:30 -04:00
BFlat 6a82fcb6e0 Revert "shared component starting with toaster" (#522)
Reverts storytold/storyteller-rust#521

it was causing an application error on build.
2025-04-17 17:21:05 +07:00
BFlat 35eac53b9d shared component starting with toaster (#521)
- share the toaster component between both editor2d and editor 3d
2025-04-17 17:14:02 +07:00
bflatastic c84c36d774 escape key to deselect objects and exit FK pose mode 2025-04-17 13:33:12 +07:00
bflatastic 3269b62e14 editor2d: artcraft logo 2025-04-17 13:11:55 +07:00
BFlat cd49fe0015 pose mode toggle button on top when character is selected (#520)
- pose mode toggle button to go in and out of pose mode when a character
is selected
- tooltip text size change
- button styling fixes
2025-04-17 13:04:53 +07:00
ArEnScandMichael Chung 326fa8c4a4 Adding API imports (#519)
Co-authored-by: Michael Chung <michaelchung@michaels-mbp-2.home>
2025-04-16 20:16:12 -04:00
Tanish Manku 51bdd4eff8 Create a really sick precision selector (#506)
Steps to activate:
1 - Select an object
2 - In the transform box, click and hold "Scale"
3 - Drag vertically to select the incremental precision
4 - Drag horizontally to apply increments of the selected precision

Issues:
- Releasing the mouse on top of the canvas will also trigger a "mouseup"
event on the canvas (since it listens to the window, not the canvas) and
deselect the object.
2025-04-16 15:01:44 -07:00
BFlat a64ca7d4ac editor3d: activity card improvements and share lightbox with gallery (#518)
**activity:**
- dropdown cards to show better download icon for completed items
- show spinner for in progress items
- activity button itself to show a spinner when at least one job is
running
- use the new lightbox component
- click the completed item to view image in lightbox

**gallery (library modal):**
- use the new lightbox component
- the new lightbox has a download button
- use cdn images
2025-04-17 02:48:25 +07:00
Kasisnu ec3ac242b0 image-gen: generate sentinel token in rust 2025-04-16 16:19:32 +05:30
Brandon Thomas fbe7fa3c77 don't leak logged in session jobs when logging out 2025-04-15 23:26:58 -04:00
Brandon Thomas 418d824c35 session jobs for anonymous users 2025-04-15 22:43:53 -04:00
Tanish Manku 2e6823834f Create the shapes centered on the canvas (#515)
- Also match all the shape sizes
2025-04-15 17:24:29 -07:00
ArEnScandMichael Chung 7ef20bc885 Feature/system prompt modification (#514)
Co-authored-by: Michael Chung <michaelchung@michaels-mbp-2.home>
2025-04-15 18:32:43 -04:00
Michael Chung b031a68000 Redo and Undo for osx fixed correctly.
Issues with the app not properly loading not allowing the engine reference to be set.
2025-04-15 16:35:47 -04:00
Brandon Thomas e4b648b55d bump Rust toolchains to 1.86.0 2025-04-15 15:05:06 -04:00
Brandon Thomas d78ccbca25 system prompt enhancement 2025-04-15 14:57:16 -04:00
Kasisnu bae6bc6863 move sentinel token generation into rust 2025-04-15 21:44:58 +05:30
Brandon Thomas faa43a7361 remove node-sass 2025-04-15 05:14:34 -04:00
Brandon Thomas f8b3b708c0 unblock CI builds for now 2025-04-15 05:05:21 -04:00
ArEnScandMichael Chung 4c40379470 Jobs should add the image to the list (#512)
Jobs also show completed images in the gallery.

Co-authored-by: Michael Chung <michaelchung@michaels-mbp-2.home>
2025-04-15 02:49:45 -04:00
Brandon Thomas 0c32a89828 script to clean up node modules 2025-04-15 00:30:57 -04:00
Brandon Thomas d7365ea045 remove more entries from package-lock.json 2025-04-14 19:26:57 -04:00
Brandon Thomas 3386247858 remove pnpm-lock.yaml to unblock CI - it's causing new failures 2025-04-14 19:10:44 -04:00
Brandon Thomas 7ea9c96d77 remove dead references from editor3d package 2025-04-14 19:08:49 -04:00
Brandon Thomas 059f4b4e38 add pnpm to solve build issues 2025-04-14 18:56:05 -04:00
Brandon Thomas 3c36e041ca remove "workspace:*" URL. Not working on all machines? 2025-04-14 18:40:23 -04:00
Tanish MankuandTanish Manku f027fcb29a Write Gallery component with test data (#508)
Co-authored-by: Tanish Manku <-h>
2025-04-14 10:49:35 -07:00
BFlat 6f2ed9e61e dismiss onboarding helper and local storage 2025-04-14 14:57:33 +07:00
Brandon Thomas 2b40b2f8d6 add index on jobs anonymous visitor tokens 2025-04-14 02:58:49 -04:00
Brandon Thomas 9996b41bc2 Add nx library component and import it 2025-04-14 02:24:29 -04:00
ArEnScandMichael Chung b9a5768679 Feature/polling (#505)
add text back and toasters

Co-authored-by: Michael Chung <michaelchung@michaels-mbp-2.home>
2025-04-13 23:54:20 -04:00
Brandon Thomas 614a5edc55 cleanup netlify.toml files 2025-04-13 21:17:09 -04:00
Brandon Thomas 01468020bd cleanup build script 2025-04-13 21:06:24 -04:00
Brandon Thomas 289ef6f352 debug branch build (#507) 2025-04-13 21:01:09 -04:00
1180 changed files with 15526 additions and 5738 deletions
+6
View File
@@ -0,0 +1,6 @@
{
"files.exclude": {
"**/*~": true,
"frontend_old/**": true
}
}
Generated
+72 -41
View File
@@ -802,47 +802,6 @@ dependencies = [
"num",
]
[[package]]
name = "artcraft"
version = "0.1.0"
dependencies = [
"anyhow",
"assertor",
"base64 0.22.1",
"bytes 1.10.0",
"chrono",
"const_format",
"crockford",
"directories 6.0.0",
"dirs 6.0.0",
"errors",
"filesys",
"futures 0.3.29",
"image",
"libloading 0.8.6",
"log",
"memory_caching",
"once_cell",
"openai_sora_client",
"primitives",
"rand 0.9.0",
"reqwest 0.12.15",
"serde",
"serde_derive",
"serde_json",
"serde_yaml",
"serial_test",
"subprocess_common",
"tauri",
"tauri-build",
"tauri-plugin-log",
"tempdir",
"tempfile",
"tokio",
"tokio-util",
"workspace-build-acceleration",
]
[[package]]
name = "ascii"
version = "0.9.3"
@@ -5035,6 +4994,12 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hex-literal"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcaaec4551594c969335c98c903c1397853d4198408ea609190f420500f6be71"
[[package]]
name = "hf-hub"
version = "0.3.2"
@@ -6130,6 +6095,15 @@ dependencies = [
"signature",
]
[[package]]
name = "keccak"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654"
dependencies = [
"cpufeatures",
]
[[package]]
name = "kernel32-sys"
version = "0.2.2"
@@ -7849,14 +7823,20 @@ name = "openai_sora_client"
version = "0.0.1"
dependencies = [
"anyhow",
"base64 0.22.1",
"chrono",
"crockford",
"errors",
"filesys",
"hex-literal",
"idempotency",
"log",
"rand 0.9.0",
"reqwest 0.12.15",
"serde",
"serde_derive",
"serde_json",
"sha3",
"subprocess_common",
"testing",
"thiserror 2.0.11",
@@ -10605,6 +10585,16 @@ dependencies = [
"digest 0.10.7",
]
[[package]]
name = "sha3"
version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60"
dependencies = [
"digest 0.10.7",
"keccak",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
@@ -11995,6 +11985,47 @@ dependencies = [
"windows 0.60.0",
]
[[package]]
name = "tauri-artcraft"
version = "0.1.0"
dependencies = [
"anyhow",
"assertor",
"base64 0.22.1",
"bytes 1.10.0",
"chrono",
"const_format",
"crockford",
"directories 6.0.0",
"dirs 6.0.0",
"errors",
"filesys",
"futures 0.3.29",
"image",
"libloading 0.8.6",
"log",
"memory_caching",
"once_cell",
"openai_sora_client",
"primitives",
"rand 0.9.0",
"reqwest 0.12.15",
"serde",
"serde_derive",
"serde_json",
"serde_yaml",
"serial_test",
"subprocess_common",
"tauri",
"tauri-build",
"tauri-plugin-log",
"tempdir",
"tempfile",
"tokio",
"tokio-util",
"workspace-build-acceleration",
]
[[package]]
name = "tauri-build"
version = "2.1.1"
+4 -2
View File
@@ -24,7 +24,7 @@ members = [
"crates/cli/production/migration-tool",
# Desktop apps
"crates/desktop/tauri-4o",
"crates/desktop/tauri-artcraft",
"crates/desktop/tauri-realtime",
# Shared libraries
@@ -152,7 +152,9 @@ workspace-build-acceleration = { path = "crates/workspace-build-acceleration" }
# 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"
base64 = "0.22.1"
chrono = { version = "0.4.40", features = ["serde"] }
hex-literal = "1.0.0"
log = "0.4.27"
once_cell = "1.21.3"
rand = "0.9.0"
@@ -162,10 +164,10 @@ serde = { version = "1.0.219", features = ["derive"] }
serde_derive = "1.0.219"
serde_json = "1.0.140"
serde_yaml = "0.9.1"
sha3 = "0.10.8"
thiserror = "2"
toml = "0.7.4"
url = "2.5.4"
# Internal libs
crockford = { path = "crates/lib/data/crockford" }
errors = { path = "crates/lib/errors" }
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
DROP INDEX `index_maybe_creator_anonymous_visitor_token`
ON `generic_inference_jobs`;
@@ -0,0 +1,7 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE generic_inference_jobs
ADD INDEX index_maybe_creator_anonymous_visitor_token (maybe_creator_anonymous_visitor_token),
ALGORITHM=INPLACE, LOCK=NONE;
@@ -297,6 +297,7 @@ CREATE TABLE generic_inference_jobs (
KEY fk_maybe_input_source_token_and_type (maybe_input_source_token, maybe_input_source_token_type),
KEY fk_on_success_result_entity_token (on_success_result_entity_token),
KEY fk_maybe_creator_user_token (maybe_creator_user_token),
KEY index_maybe_creator_anonymous_visitor_token (maybe_creator_anonymous_visitor_token),
KEY index_creator_ip_address (creator_ip_address),
KEY index_priority_level (priority_level),
KEY index_is_debug_request (is_debug_request),
@@ -1,7 +0,0 @@
#[test]
fn test_dynamic_library_loading() {
let name = libloading::library_filename("cublas");
assert_eq!(name, "libcublas.so");
}
@@ -1,5 +1,5 @@
[package]
name = "artcraft"
name = "tauri-artcraft"
version = "0.1.0"
description = "Artcraft"
authors = ["you"]
@@ -12,7 +12,7 @@ publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "app_lib"
name = "tauri_artcraft_app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

@@ -2,5 +2,5 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
app_lib::run();
tauri_artcraft_app_lib::run();
}
@@ -10,7 +10,7 @@ pub fn encode_rgb_image_base64_png(
image.write_to(&mut Cursor::new(&mut bytes), ImageFormat::Png)?;
let bytes = BASE64_STANDARD.encode(bytes);
let bytes = (&BASE64_STANDARD).encode(bytes);
Ok(bytes)
}
@@ -0,0 +1,5 @@
# For some reason, we can't pass json files to the tauri dev command
[build]
frontendDist = "../../../frontend/apps/editor2d/dist"
beforeDevCommand = "nx run editor2d:dev"
devUrl = "http://localhost:5741"
@@ -0,0 +1,5 @@
# For some reason, we can't pass json files to the tauri dev command
[build]
frontendDist = "../../../frontend/apps/editor3d/dist"
beforeDevCommand = "nx run editor3d:dev"
devUrl = "http://localhost:5173"
@@ -2,12 +2,12 @@
"$schema": "https://schema.tauri.app/config/2.0.0-rc",
"productName": "ArtCraft",
"version": "0.1.0",
"identifier": "com.unique.value.thing.why",
"identifier": "ai.artcraft.app",
"build": {
"frontendDist": "../../../frontend/realtime/dist",
"devUrl": "http://localhost:5174",
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build2"
"frontendDist": "../../../frontend/apps/editor2d/dist",
"devUrl": "http://localhost:5741",
"beforeDevCommand": "nx run editor2d:dev",
"beforeBuildCommand": "nx run editor2d:build"
},
"app": {
"windows": [
@@ -20,6 +20,7 @@ workspace-build-acceleration = { workspace = true }
crockford = { path = "../../data/crockford" }
errors = { path = "../../errors" }
subprocess_common = { path = "../../subprocess_common" }
idempotency = { path = "../../idempotency" }
# External
anyhow = { workspace = true }
@@ -28,9 +29,14 @@ reqwest = { workspace = true, features = ["multipart"] }
serde = { workspace = true }
serde_derive = { workspace = true }
serde_json = { workspace = true }
sha3 = { workspace = true }
hex-literal = { workspace = true }
base64 = { workspace = true }
tokio = { version = "1.38.0", features = ["macros"] }
toml = { workspace = true }
thiserror = { workspace = true }
chrono = { workspace = true}
rand = { workspace = true }
[dev-dependencies]
testing = { path = "../../testing" }
@@ -15,6 +15,9 @@ pub enum SoraError {
#[error("Sora sentinel block: {0}")]
SentinelBlock(String),
#[error("Sora too many concurrent tasks: {0}")]
TooManyConcurrentTasks(String),
#[error("Sora API error: {0}")]
GenericError(String),
@@ -115,10 +118,10 @@ pub enum RawSoraError {
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "snake_case")]
pub enum SoraErrorCode {
pub enum SoraErrorCode {
SentinelBlock,
TokenExpired,
TooManyConcurrentTasks,
Unknown(String),
}
@@ -179,6 +182,9 @@ pub (crate) async fn image_gen_http_request(sora_request: RawSoraImageGenRequest
SoraErrorCode::SentinelBlock => {
return Err(SoraError::SentinelBlock(error_response.error.message));
}
SoraErrorCode::TooManyConcurrentTasks => {
return Err(SoraError::TooManyConcurrentTasks(error_response.error.message));
}
SoraErrorCode::Unknown(_) => {
return Err(SoraError::GenericError(error_response.error.message));
}
@@ -83,16 +83,16 @@ pub struct Generation {
pub created_at: String,
pub deleted_at: Option<String>,
pub url: String,
pub seed: i64,
pub can_download: bool,
pub download_status: String,
pub is_favorite: bool,
pub is_liked: bool,
pub is_public: bool,
pub is_archived: bool,
pub is_featured: bool,
pub has_feedback: bool,
pub like_count: i32,
pub seed: Option<i64>,
pub can_download: Option<bool>,
pub download_status: Option<String>,
pub is_favorite: Option<bool>,
pub is_liked: Option<bool>,
pub is_public: Option<bool>,
pub is_archived: Option<bool>,
pub is_featured: Option<bool>,
pub has_feedback: Option<bool>,
pub like_count: Option<i32>,
pub cloudflare_metadata: Option<serde_json::Value>,
pub cf_thumbnail_url: Option<String>,
pub encodings: Encodings,
@@ -2,4 +2,5 @@ pub mod credentials;
pub mod image_gen;
pub mod job;
pub mod upload;
pub mod sentinel_refresh;
pub mod sentinel_refresh;
mod sora_error;
@@ -0,0 +1,671 @@
// cores = [8, 16, 24, 32]
// cached_scripts = ["https://cdn.oaistatic.com/_next/static/cXh69klOLzS0Gy2joLDRS/_ssgManifest.js?dpl=453ebaec0d44c2decab71692e1bfe39be35a24b3"]
// cached_dpl = ["prod-f501fe933b3edf57aea882da888e1a544df99840"]
// navigator_key = [
// "registerProtocolHandler−function registerProtocolHandler() { [native code] }",
// "storage−[object StorageManager]",
// "locks−[object LockManager]",
// "appCodeName−Mozilla",
// "permissions−[object Permissions]",
// "share−function share() { [native code] }",
// "webdriver−false",
// "managed−[object NavigatorManagedData]",
// "canShare−function canShare() { [native code] }",
// "vendor−Google Inc.",
// "vendor−Google Inc.",
// "mediaDevices−[object MediaDevices]",
// "vibrate−function vibrate() { [native code] }",
// "storageBuckets−[object StorageBucketManager]",
// "mediaCapabilities−[object MediaCapabilities]",
// "getGamepads−function getGamepads() { [native code] }",
// "bluetooth−[object Bluetooth]",
// "share−function share() { [native code] }",
// "cookieEnabled−true",
// "virtualKeyboard−[object VirtualKeyboard]",
// "product−Gecko",
// "mediaDevices−[object MediaDevices]",
// "canShare−function canShare() { [native code] }",
// "getGamepads−function getGamepads() { [native code] }",
// "product−Gecko",
// "xr−[object XRSystem]",
// "clipboard−[object Clipboard]",
// "storageBuckets−[object StorageBucketManager]",
// "unregisterProtocolHandler−function unregisterProtocolHandler() { [native code] }",
// "productSub−20030107",
// "login−[object NavigatorLogin]",
// "vendorSub−",
// "login−[object NavigatorLogin]",
// "getInstalledRelatedApps−function getInstalledRelatedApps() { [native code] }",
// "mediaDevices−[object MediaDevices]",
// "locks−[object LockManager]",
// "webkitGetUserMedia−function webkitGetUserMedia() { [native code] }",
// "vendor−Google Inc.",
// "xr−[object XRSystem]",
// "mediaDevices−[object MediaDevices]",
// "virtualKeyboard−[object VirtualKeyboard]",
// "virtualKeyboard−[object VirtualKeyboard]",
// "appName−Netscape",
// "storageBuckets−[object StorageBucketManager]",
// "presentation−[object Presentation]",
// "onLine−true",
// "mimeTypes−[object MimeTypeArray]",
// "credentials−[object CredentialsContainer]",
// "presentation−[object Presentation]",
// "getGamepads−function getGamepads() { [native code] }",
// "vendorSub−",
// "virtualKeyboard−[object VirtualKeyboard]",
// "serviceWorker−[object ServiceWorkerContainer]",
// "xr−[object XRSystem]",
// "product−Gecko",
// "keyboard−[object Keyboard]",
// "gpu−[object GPU]",
// "getInstalledRelatedApps−function getInstalledRelatedApps() { [native code] }",
// "webkitPersistentStorage−[object DeprecatedStorageQuota]",
// "doNotTrack",
// "clearAppBadge−function clearAppBadge() { [native code] }",
// "presentation−[object Presentation]",
// "serial−[object Serial]",
// "locks−[object LockManager]",
// "requestMIDIAccess−function requestMIDIAccess() { [native code] }",
// "locks−[object LockManager]",
// "requestMediaKeySystemAccess−function requestMediaKeySystemAccess() { [native code] }",
// "vendor−Google Inc.",
// "pdfViewerEnabled−true",
// "language−zh-CN",
// "setAppBadge−function setAppBadge() { [native code] }",
// "geolocation−[object Geolocation]",
// "userAgentData−[object NavigatorUAData]",
// "mediaCapabilities−[object MediaCapabilities]",
// "requestMIDIAccess−function requestMIDIAccess() { [native code] }",
// "getUserMedia−function getUserMedia() { [native code] }",
// "mediaDevices−[object MediaDevices]",
// "webkitPersistentStorage−[object DeprecatedStorageQuota]",
// "sendBeacon−function sendBeacon() { [native code] }",
// "hardwareConcurrency−32",
// "credentials−[object CredentialsContainer]",
// "storage−[object StorageManager]",
// "cookieEnabled−true",
// "pdfViewerEnabled−true",
// "windowControlsOverlay−[object WindowControlsOverlay]",
// "scheduling−[object Scheduling]",
// "pdfViewerEnabled−true",
// "hardwareConcurrency−32",
// "xr−[object XRSystem]",
// "webdriver−false",
// "getInstalledRelatedApps−function getInstalledRelatedApps() { [native code] }",
// "getInstalledRelatedApps−function getInstalledRelatedApps() { [native code] }",
// "bluetooth−[object Bluetooth]"
// ]
// document_key = ['_reactListeningo743lnnpvdg', 'location']
// window_key = [
// "0",
// "window",
// "self",
// "document",
// "name",
// "location",
// "customElements",
// "history",
// "navigation",
// "locationbar",
// "menubar",
// "personalbar",
// "scrollbars",
// "statusbar",
// "toolbar",
// "status",
// "closed",
// "frames",
// "length",
// "top",
// "opener",
// "parent",
// "frameElement",
// "navigator",
// "origin",
// "external",
// "screen",
// "innerWidth",
// "innerHeight",
// "scrollX",
// "pageXOffset",
// "scrollY",
// "pageYOffset",
// "visualViewport",
// "screenX",
// "screenY",
// "outerWidth",
// "outerHeight",
// "devicePixelRatio",
// "clientInformation",
// "screenLeft",
// "screenTop",
// "styleMedia",
// "onsearch",
// "isSecureContext",
// "trustedTypes",
// "performance",
// "onappinstalled",
// "onbeforeinstallprompt",
// "crypto",
// "indexedDB",
// "sessionStorage",
// "localStorage",
// "onbeforexrselect",
// "onabort",
// "onbeforeinput",
// "onbeforematch",
// "onbeforetoggle",
// "onblur",
// "oncancel",
// "oncanplay",
// "oncanplaythrough",
// "onchange",
// "onclick",
// "onclose",
// "oncontentvisibilityautostatechange",
// "oncontextlost",
// "oncontextmenu",
// "oncontextrestored",
// "oncuechange",
// "ondblclick",
// "ondrag",
// "ondragend",
// "ondragenter",
// "ondragleave",
// "ondragover",
// "ondragstart",
// "ondrop",
// "ondurationchange",
// "onemptied",
// "onended",
// "onerror",
// "onfocus",
// "onformdata",
// "oninput",
// "oninvalid",
// "onkeydown",
// "onkeypress",
// "onkeyup",
// "onload",
// "onloadeddata",
// "onloadedmetadata",
// "onloadstart",
// "onmousedown",
// "onmouseenter",
// "onmouseleave",
// "onmousemove",
// "onmouseout",
// "onmouseover",
// "onmouseup",
// "onmousewheel",
// "onpause",
// "onplay",
// "onplaying",
// "onprogress",
// "onratechange",
// "onreset",
// "onresize",
// "onscroll",
// "onsecuritypolicyviolation",
// "onseeked",
// "onseeking",
// "onselect",
// "onslotchange",
// "onstalled",
// "onsubmit",
// "onsuspend",
// "ontimeupdate",
// "ontoggle",
// "onvolumechange",
// "onwaiting",
// "onwebkitanimationend",
// "onwebkitanimationiteration",
// "onwebkitanimationstart",
// "onwebkittransitionend",
// "onwheel",
// "onauxclick",
// "ongotpointercapture",
// "onlostpointercapture",
// "onpointerdown",
// "onpointermove",
// "onpointerrawupdate",
// "onpointerup",
// "onpointercancel",
// "onpointerover",
// "onpointerout",
// "onpointerenter",
// "onpointerleave",
// "onselectstart",
// "onselectionchange",
// "onanimationend",
// "onanimationiteration",
// "onanimationstart",
// "ontransitionrun",
// "ontransitionstart",
// "ontransitionend",
// "ontransitioncancel",
// "onafterprint",
// "onbeforeprint",
// "onbeforeunload",
// "onhashchange",
// "onlanguagechange",
// "onmessage",
// "onmessageerror",
// "onoffline",
// "ononline",
// "onpagehide",
// "onpageshow",
// "onpopstate",
// "onrejectionhandled",
// "onstorage",
// "onunhandledrejection",
// "onunload",
// "crossOriginIsolated",
// "scheduler",
// "alert",
// "atob",
// "blur",
// "btoa",
// "cancelAnimationFrame",
// "cancelIdleCallback",
// "captureEvents",
// "clearInterval",
// "clearTimeout",
// "close",
// "confirm",
// "createImageBitmap",
// "fetch",
// "find",
// "focus",
// "getComputedStyle",
// "getSelection",
// "matchMedia",
// "moveBy",
// "moveTo",
// "open",
// "postMessage",
// "print",
// "prompt",
// "queueMicrotask",
// "releaseEvents",
// "reportError",
// "requestAnimationFrame",
// "requestIdleCallback",
// "resizeBy",
// "resizeTo",
// "scroll",
// "scrollBy",
// "scrollTo",
// "setInterval",
// "setTimeout",
// "stop",
// "structuredClone",
// "webkitCancelAnimationFrame",
// "webkitRequestAnimationFrame",
// "chrome",
// "caches",
// "cookieStore",
// "ondevicemotion",
// "ondeviceorientation",
// "ondeviceorientationabsolute",
// "launchQueue",
// "documentPictureInPicture",
// "getScreenDetails",
// "queryLocalFonts",
// "showDirectoryPicker",
// "showOpenFilePicker",
// "showSaveFilePicker",
// "originAgentCluster",
// "onpageswap",
// "onpagereveal",
// "credentialless",
// "speechSynthesis",
// "onscrollend",
// "webkitRequestFileSystem",
// "webkitResolveLocalFileSystemURL",
// "sendMsgToSolverCS",
// "webpackChunk_N_E",
// "__next_set_public_path__",
// "next",
// "__NEXT_DATA__",
// "__SSG_MANIFEST_CB",
// "__NEXT_P",
// "_N_E",
// "regeneratorRuntime",
// "__REACT_INTL_CONTEXT__",
// "DD_RUM",
// "_",
// "filterCSS",
// "filterXSS",
// "__SEGMENT_INSPECTOR__",
// "__NEXT_PRELOADREADY",
// "Intercom",
// "__MIDDLEWARE_MATCHERS",
// "__STATSIG_SDK__",
// "__STATSIG_JS_SDK__",
// "__STATSIG_RERENDER_OVERRIDE__",
// "_oaiHandleSessionExpired",
// "__BUILD_MANIFEST",
// "__SSG_MANIFEST",
// "__intercomAssignLocation",
// "__intercomReloadLocation"
// ]
use rand::Rng;
const CORES: &[usize] = &[8, 16, 24, 32];
const CACHED_SCRIPTS: &[&str] = &["https://cdn.oaistatic.com/_next/static/cXh69klOLzS0Gy2joLDRS/_ssgManifest.js?dpl=453ebaec0d44c2decab71692e1bfe39be35a24b3"];
const CACHED_DPL: &[&str] = &["prod-f501fe933b3edf57aea882da888e1a544df99840"];
const NAVIGATOR_KEY: &[&str] = &[
"registerProtocolHandler−function registerProtocolHandler() { [native code] }",
"storage−[object StorageManager]",
"locks−[object LockManager]",
"appCodeName−Mozilla",
"permissions−[object Permissions]",
"share−function share() { [native code] }",
"webdriver−false",
"managed−[object NavigatorManagedData]",
"canShare−function canShare() { [native code] }",
"vendor−Google Inc.",
"vendor−Google Inc.",
"mediaDevices−[object MediaDevices]",
"vibrate−function vibrate() { [native code] }",
"storageBuckets−[object StorageBucketManager]",
"mediaCapabilities−[object MediaCapabilities]",
"getGamepads−function getGamepads() { [native code] }",
"bluetooth−[object Bluetooth]",
"share−function share() { [native code] }",
"cookieEnabled−true",
"pdfViewerEnabled−true",
"windowControlsOverlay−[object WindowControlsOverlay]",
"scheduling−[object Scheduling]",
"pdfViewerEnabled−true",
"hardwareConcurrency−32",
"xr−[object XRSystem]",
"webdriver−false",
"getInstalledRelatedApps−function getInstalledRelatedApps() { [native code] }",
"getInstalledRelatedApps−function getInstalledRelatedApps() { [native code] }",
"bluetooth−[object Bluetooth]"
];
const DOCUMENT_KEY: &[&str] = &["_reactListeningo743lnnpvdg", "location"];
const WINDOW_KEY: &[&str] = &[
"0",
"window",
"self",
"document",
"name",
"location",
"customElements",
"history",
"navigation",
"locationbar",
"menubar",
"personalbar",
"scrollbars",
"statusbar",
"toolbar",
"status",
"closed",
"frames",
"length",
"top",
"opener",
"parent",
"frameElement",
"navigator",
"origin",
"external",
"screen",
"innerWidth",
"innerHeight",
"scrollX",
"pageXOffset",
"scrollY",
"pageYOffset",
"visualViewport",
"screenX",
"screenY",
"outerWidth",
"outerHeight",
"devicePixelRatio",
"clientInformation",
"screenLeft",
"screenTop",
"styleMedia",
"onsearch",
"isSecureContext",
"trustedTypes",
"performance",
"onappinstalled",
"onbeforeinstallprompt",
"crypto",
"indexedDB",
"sessionStorage",
"localStorage",
"onbeforexrselect",
"onabort",
"onbeforeinput",
"onbeforematch",
"onbeforetoggle",
"onblur",
"oncancel",
"oncanplay",
"oncanplaythrough",
"onchange",
"onclick",
"onclose",
"oncontentvisibilityautostatechange",
"oncontextlost",
"oncontextmenu",
"oncontextrestored",
"oncuechange",
"ondblclick",
"ondrag",
"ondragend",
"ondragenter",
"ondragleave",
"ondragover",
"ondragstart",
"ondrop",
"ondurationchange",
"onemptied",
"onended",
"onerror",
"onfocus",
"onformdata",
"oninput",
"oninvalid",
"onkeydown",
"onkeypress",
"onkeyup",
"onload",
"onloadeddata",
"onloadedmetadata",
"onloadstart",
"onmousedown",
"onmouseenter",
"onmouseleave",
"onmousemove",
"onmouseout",
"onmouseover",
"onmouseup",
"onmousewheel",
"onpause",
"onplay",
"onplaying",
"onprogress",
"onratechange",
"onreset",
"onresize",
"onscroll",
"onsecuritypolicyviolation",
"onseeked",
"onseeking",
"onselect",
"onslotchange",
"onstalled",
"onsubmit",
"onsuspend",
"ontimeupdate",
"ontoggle",
"onvolumechange",
"onwaiting",
"onwebkitanimationend",
"onwebkitanimationiteration",
"onwebkitanimationstart",
"onwebkittransitionend",
"onwheel",
"onauxclick",
"ongotpointercapture",
"onlostpointercapture",
"onpointerdown",
"onpointermove",
"onpointerrawupdate",
"onpointerup",
"onpointercancel",
"onpointerover",
"onpointerout",
"onpointerenter",
"onpointerleave",
"onselectstart",
"onselectionchange",
"onanimationend",
"onanimationiteration",
"onanimationstart",
"ontransitionrun",
"ontransitionstart",
"ontransitionend",
"ontransitioncancel",
"onafterprint",
"onbeforeprint",
"onbeforeunload",
"onhashchange",
"onlanguagechange",
"onmessage",
"onmessageerror",
"onoffline",
"ononline",
"onpagehide",
"onpageshow",
"onpopstate",
"onrejectionhandled",
"onstorage",
"onunhandledrejection",
"onunload",
"crossOriginIsolated",
"scheduler",
"alert",
"atob",
"blur",
"btoa",
"cancelAnimationFrame",
"cancelIdleCallback",
"captureEvents",
"clearInterval",
"clearTimeout",
"close",
"confirm",
"createImageBitmap",
"fetch",
"find",
"focus",
"getComputedStyle",
"getSelection",
"matchMedia",
"moveBy",
"moveTo",
"open",
"postMessage",
"print",
"prompt",
"queueMicrotask",
"releaseEvents",
"reportError",
"requestAnimationFrame",
"requestIdleCallback",
"resizeBy",
"resizeTo",
"scroll",
"scrollBy",
"scrollTo",
"setInterval",
"setTimeout",
"stop",
"structuredClone",
"webkitCancelAnimationFrame",
"webkitRequestAnimationFrame",
"chrome",
"caches",
"cookieStore",
"ondevicemotion",
"ondeviceorientation",
"ondeviceorientationabsolute",
"launchQueue",
"documentPictureInPicture",
"getScreenDetails",
"queryLocalFonts",
"showDirectoryPicker",
"showOpenFilePicker",
"showSaveFilePicker",
"originAgentCluster",
"onpageswap",
"onpagereveal",
"credentialless",
"speechSynthesis",
"onscrollend",
"webkitRequestFileSystem",
"webkitResolveLocalFileSystemURL",
"sendMsgToSolverCS",
"webpackChunk_N_E",
"__next_set_public_path__",
"next",
"__NEXT_DATA__",
"__SSG_MANIFEST_CB",
"__NEXT_P",
"_N_E",
"regeneratorRuntime",
"__REACT_INTL_CONTEXT__",
"DD_RUM",
"_",
"filterCSS",
"filterXSS",
"__SEGMENT_INSPECTOR__",
"__NEXT_PRELOADREADY",
"Intercom",
"__MIDDLEWARE_MATCHERS",
"__STATSIG_SDK__",
"__STATSIG_JS_SDK__",
"__STATSIG_RERENDER_OVERRIDE__",
"_oaiHandleSessionExpired",
"__BUILD_MANIFEST",
"__SSG_MANIFEST",
"__intercomAssignLocation",
"__intercomReloadLocation"
];
pub fn get_random_core_count() -> usize {
return CORES[rand::rng().random_range(0..CORES.len())];
}
pub fn get_random_navigator_key() -> String {
return NAVIGATOR_KEY[rand::rng().random_range(0..NAVIGATOR_KEY.len())].to_string();
}
pub fn get_random_document_key() -> String {
return DOCUMENT_KEY[rand::rng().random_range(0..DOCUMENT_KEY.len())].to_string();
}
pub fn get_random_window_key() -> String {
return WINDOW_KEY[rand::rng().random_range(0..WINDOW_KEY.len())].to_string();
}
@@ -0,0 +1,3 @@
pub mod request;
pub mod token;
pub(crate) mod config;
@@ -0,0 +1,225 @@
use crate::sentinel_refresh::generate::config::{get_random_core_count, get_random_document_key, get_random_navigator_key, get_random_window_key};
use chrono::FixedOffset;
use chrono::Utc;
use idempotency::uuid::generate_random_uuid;
use rand::Rng;
use base64::prelude::BASE64_STANDARD;
use base64::Engine;
use sha3::{Digest, Sha3_512};
// [
// 4000, // first
// "Fri Apr 11 2025 13:28:14 GMT+0530 (India Standard Time)", // second
// None, // third
// 28, // fourth
// "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:137.0) Gecko/20100101 Firefox/137.0", // fifth
// "https://sora-cdn.oaistatic.com/_next/static/chunks/f90f2e60-61da6d33257661c4.js", // sixth
// None, // seventh
// "en-US", // eighth
// "en-US,en", // ninth
// 29, // tenth
// "productSub−20100101", // eleventh
// "location", // twelfth
// "ondrop", // thirteenth
// 10520, // fourteenth - generated from time.perf_counter() * 1000
// "48094014-a024-4d2d-8ec4-ba9c7cfef9c4", // fifteenth
// "", // sixteenth
// 16, // seventeenth - Number of cores
#[derive(Debug, Clone)]
pub struct GenerateSentinelRefreshRequest {
first: usize,
second: String, // Date
third: Option<String>,
fourth: Option<usize>,
fifth: String, // User agent
sixth: String, // URL
seventh: Option<String>, // Language
eighth: String, // Accept-Language
ninth: String, // Product Sub
tenth: Option<usize>,
eleventh: String, // Product Sub - Navigator key
twelfth: String, // Location - Document key
thirteenth: String, // On Drop - Window key
fourteenth: usize, // generated from time.perf_counter() * 1000
fifteenth: String, // UUID
sixteenth: String, // empty string
seventeenth: usize, // Number of cores
}
fn generate_date() -> String {
let now_utc = Utc::now();
let offset = FixedOffset::west_opt(5 * 60 * 60).unwrap();
let now_with_offset = Utc::now().with_timezone(&offset);
let formatted_date = now_with_offset.format("%a %b %d %Y %H:%M:%S GMT-0500 (Eastern Standard Time)");
return formatted_date.to_string();
}
impl GenerateSentinelRefreshRequest {
pub fn new() -> Self {
let first_candidates = vec![1920 + 1080, 2560 + 1440, 1920 + 1200, 2560 + 1600];
let first = first_candidates[rand::rng().random_range(0..first_candidates.len())];
let second = generate_date();
let third = None;
let fourth = None;
let fifth = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:137.0) Gecko/20100101 Firefox/137.0".to_string();
let sixth = "https://sora-cdn.oaistatic.com/_next/static/chunks/f90f2e60-61da6d33257661c4.js".to_string();
let seventh = None;
let eighth = "en-US".to_string();
let ninth = "en-US,en".to_string();
let tenth = None;
let eleventh = get_random_navigator_key();
let twelfth = get_random_document_key();
let thirteenth = get_random_window_key();
let fourteenth = 10520;
let fifteenth = generate_random_uuid();
let sixteenth = "".to_string();
let seventeenth = get_random_core_count();
Self { first, second, third, fourth, fifth, sixth, seventh, eighth, ninth, tenth, eleventh: eleventh.to_string(), twelfth: twelfth.to_string(), thirteenth: thirteenth.to_string(), fourteenth, fifteenth, sixteenth, seventeenth }
}
pub fn with_fourth(mut self, fourth: usize) -> Self {
self.fourth = Some(fourth);
self
}
pub fn with_tenth(mut self, tenth: usize) -> Self {
self.tenth = Some(tenth);
self
}
//TODO(kasisnu): This is a temporary function to build the request.
// It is extremely hacky and this whole struct should be rewritten to be more ergonomic.
pub fn build(self) -> String {
let mut request = String::new();
request.push_str("[");
request.push_str(&self.first.to_string());
request.push_str(",");
request.push_str(&string_with_quotes(self.second));
request.push_str(",");
request.push_str(&self.third.unwrap_or("null".to_string()));
request.push_str(",");
request.push_str(&self.fourth.unwrap_or(0).to_string());
request.push_str(",");
request.push_str(&string_with_quotes(self.fifth));
request.push_str(",");
request.push_str(&string_with_quotes(self.sixth));
request.push_str(",");
request.push_str(&self.seventh.unwrap_or("null".to_string()));
request.push_str(",");
request.push_str(&string_with_quotes(self.eighth));
request.push_str(",");
request.push_str(&string_with_quotes(self.ninth));
request.push_str(",");
request.push_str(&self.tenth.unwrap_or(0).to_string());
request.push_str(",");
request.push_str(&string_with_quotes(self.eleventh));
request.push_str(",");
request.push_str(&string_with_quotes(self.twelfth));
request.push_str(",");
request.push_str(&string_with_quotes(self.thirteenth));
request.push_str(",");
request.push_str(&self.fourteenth.to_string());
request.push_str(",");
request.push_str(&string_with_quotes(self.fifteenth));
request.push_str(",");
request.push_str(&string_with_quotes(self.sixteenth));
request.push_str(",");
request.push_str(&self.seventeenth.to_string());
request.push_str("]");
request
}
pub fn hash_request(seed: f64, request: String) -> Vec<u8> {
let mut hasher = Sha3_512::new();
hasher.update(seed.to_string());
let base64_request = BASE64_STANDARD.encode(request.as_bytes());
hasher.update(base64_request);
hasher.finalize().to_vec()
}
pub fn with_fourth_and_tenth(mut self) -> (Self, String) {
let mut fourth = 0;
let mut tenth = 0;
let max_iterations = 500000;
let diff_len = 6;
let target_bytes = vec![0x0f, 0xff, 0xff];
let random_seed: f64 = rand::rng().random();
let mut base64_request = BASE64_STANDARD.encode(self.clone().build().as_bytes());
for i in 0..max_iterations {
fourth = i;
tenth = i >> 1;
let request = self.clone().with_fourth(fourth).with_tenth(tenth).build();
let hash_bytes = Self::hash_request(random_seed, request.clone());
base64_request = BASE64_STANDARD.encode(request.as_bytes());
let mut is_less_or_equal = true;
for j in 0..std::cmp::min(diff_len, target_bytes.len()) {
if j < hash_bytes.len() {
if hash_bytes[j] > target_bytes[j] {
is_less_or_equal = false;
break;
} else if hash_bytes[j] < target_bytes[j] {
break;
}
}
}
if is_less_or_equal {
break;
}
}
(self.with_fourth(fourth).with_tenth(tenth), format!("gAAAAAC{}", base64_request))
}
}
fn string_with_quotes(value: String) -> String {
format!("\"{}\"", value)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_sentinel_refresh_request() {
let request = GenerateSentinelRefreshRequest::new();
println!("{}", request.build());
}
#[test]
fn test_with_fourth_and_tenth() {
let request = GenerateSentinelRefreshRequest::new().with_fourth(100).with_tenth(200);
println!("{}", request.build());
}
#[test]
fn test_hash_request() {
let request = GenerateSentinelRefreshRequest::new().with_fourth(100).with_tenth(200);
let hash = GenerateSentinelRefreshRequest::hash_request(0.1, request.build());
println!("{}", hash.iter().map(|b| format!("{:02x}", b)).collect::<String>());
}
#[test]
fn test_with_fourth_and_tenth_2() {
let (request, base64_request) = GenerateSentinelRefreshRequest::new().with_fourth_and_tenth();
println!("{}", base64_request);
}
}
@@ -0,0 +1,69 @@
use errors::AnyhowResult;
use serde_derive::{Deserialize, Serialize};
use thiserror::Error;
use idempotency::uuid::generate_random_uuid;
use super::request::GenerateSentinelRefreshRequest;
const SORA_IMAGE_GEN_URL: &str = "https://chatgpt.com/backend-api/sentinel/req";
/// This user agent is tied to the sentinel generation. If we need to change it, we may need to change sentinel generation too.
const USER_AGENT : &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36";
const SENTINEL_FLOW: &str = "sora_create_task";
#[derive(Debug, Serialize, Deserialize)]
pub struct SentinelRequest {
#[serde(rename = "p")]
problem: String,
#[serde(rename = "id")]
id: String,
#[serde(rename = "flow")]
flow: String,
}
impl SentinelRequest {
pub fn new(problem: String) -> Self {
let id = generate_random_uuid();
Self { problem, id, flow: SENTINEL_FLOW.to_string() }
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SentinelResponse {
persona: String,
token: String,
}
pub async fn generate_token() -> AnyhowResult<String> {
let (_request, base64_request) = GenerateSentinelRefreshRequest::new().with_fourth_and_tenth();
let request = SentinelRequest::new(base64_request);
let client = reqwest::Client::new();
let response = client.post(SORA_IMAGE_GEN_URL)
.header("User-Agent", USER_AGENT)
.header("Content-Type", "application/json")
.json(&request)
.send()
.await?;
let response_json: SentinelResponse = response.json().await?;
Ok(response_json.token)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_generate_token() {
let token = generate_token().await.unwrap();
println!("{}", token);
}
}
@@ -1,2 +1,4 @@
pub use refresh::refresh_sentinel;
pub mod refresh;
pub mod refresh;
pub mod generate;
@@ -0,0 +1,48 @@
use std::error::Error;
use std::fmt::{Display, Formatter};
#[derive(Debug)]
pub enum SoraError {
/// Unauthorized, cookie and/or bearer token expired. We'll need to ask for a refreshed login.
///
/// Example message, e.g. from the upload endpoint:
/// {
/// "error": {
/// "message": "Your authentication token has expired. Please try signing in again.",
/// "type": "invalid_request_error",
/// "param": null,
/// "code": "token_expired"
/// }
/// }
UnauthorizedCookieOrBearerExpired,
/// Another error occurred.
OtherBadStatus(anyhow::Error),
/// Reqwest Error
ReqwestError(reqwest::Error),
}
impl Error for SoraError {}
impl Display for SoraError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
SoraError::UnauthorizedCookieOrBearerExpired => {
write!(f, "Unauthorized: cookie and/or bearer token expired")
}
SoraError::OtherBadStatus(err) => {
write!(f, "Other error: {}", err)
}
SoraError::ReqwestError(err) => {
write!(f, "Reqwest error: {}", err)
}
}
}
}
impl From<reqwest::Error> for SoraError {
fn from(err: reqwest::Error) -> SoraError {
SoraError::ReqwestError(err)
}
}
@@ -3,6 +3,7 @@ use errors::AnyhowResult;
use reqwest::multipart::{Form, Part};
use reqwest::Client;
use serde::Deserialize;
use crate::sora_error::SoraError;
const SORA_UPLOAD_MEDIA_URL: &str = "https://sora.com/backend/uploads";
@@ -25,6 +26,17 @@ Response:
"size_bytes": 118501,
"thumbnail_url": "https://videos.openai.com/vg-assets/assets%2Fclient_upload%2Fmedia%2Fuser-9wf6JFdgRSJLjvSJ53LNAbV8%2Fmedia_01jqt9vt20erx9zvryf3v1pecx.jpg?st=2025-04-02T02%3A39%3A01Z&se=2025-04-08T03%3A39%3A01Z&sks=b&skt=2025-04-02T02%3A39%3A01Z&ske=2025-04-08T03%3A39%3A01Z&sktid=a48cca56-e6da-484e-a814-9c849652bcb3&skoid=3d249c53-07fa-4ba4-9b65-0bf8eb4ea46a&skv=2019-02-02&sv=2018-11-09&sr=b&sp=r&spr=https%2Chttp&sig=9aU9xKI0BkhRtrTb0RD05%2BT6prBnmXk1I7nV2nTsFbY%3D&az=oaivgprodscus"
}
Error response:
Failed to upload scene media to Sora: Upload failed with status 401 Unauthorized: {
"error": {
"message": "Your authentication token has expired. Please try signing in again.",
"type": "invalid_request_error",
"param": null,
"code": "token_expired"
}
}
*/
#[derive(Debug, Deserialize)]
@@ -50,7 +62,13 @@ pub (crate) struct SoraMediaUploadRequest<'a> {
pub credentials: &'a SoraCredentials,
}
pub (crate) async fn upload_media_http_request(file_bytes: Vec<u8>, filename: String, mime_type: &str, credentials: &SoraCredentials) -> AnyhowResult<SoraMediaUploadResponse> {
pub (crate) async fn upload_media_http_request(
file_bytes: Vec<u8>,
filename: String,
mime_type: &str,
credentials: &SoraCredentials,
) -> Result<SoraMediaUploadResponse, SoraError> {
// Create multipart form
let part = Part::bytes(file_bytes) // NB: Reqwest needs to own the bytes.
.file_name(filename) // NB: Reqwest needs to own the bytes
@@ -70,12 +88,29 @@ pub (crate) async fn upload_media_http_request(file_bytes: Vec<u8>, filename: St
// Check response status
if !response.status().is_success() {
let status = response.status();
let text = response.text().await?;
return Err(anyhow::anyhow!("Upload failed with status {}: {}", status, text));
let error = classify_error(response).await;
return Err(error);
}
// Parse response
let upload_response = response.json::<SoraMediaUploadResponse>().await?;
Ok(upload_response)
}
async fn classify_error(response: reqwest::Response) -> SoraError {
let status = response.status();
let message = match response.text().await {
Ok(text) => text,
Err(err) => return SoraError::ReqwestError(err),
};
let cookie_expired =
message.contains("Your authentication token has expired. Please try signing in again.")
|| message.contains("token_expired");
if cookie_expired {
SoraError::UnauthorizedCookieOrBearerExpired
} else {
SoraError::OtherBadStatus(anyhow::anyhow!("Upload failed with status {}: {}", status, message))
}
}
@@ -4,7 +4,6 @@
/// NB: `sqlx::query` is spammy and dumps all queries as "info"-level log lines.
/// NB: `hyper::proto::h1::io` is incredibly spammy and logs every chunk of bytes in very large files being downloaded.
pub const DEFAULT_RUST_LOG: &str = concat!(
"debug,",
"actix_web=info,",
"sqlx::query=warn,",
"hyper::proto::h1::io=warn,",
@@ -21,9 +21,14 @@ use crate::helpers::boolean_converters::i8_to_bool;
use crate::payloads::generic_inference_args::generic_inference_args::{GenericInferenceArgs, PolymorphicInferenceArgs};
use crate::queries::generic_inference::web::job_status::{GenericInferenceJobStatus, RequestDetails, ResultDetails, UserDetails};
pub enum SessionUser<'a> {
User(&'a UserToken),
Anonymous(&'a AnonymousVisitorTrackingToken),
}
pub struct ListSessionJobsForUserArgs<'a> {
// TODO(bt,2024-04-23): Support AnonymousVisitorToken
pub user_token: &'a UserToken,
pub user: SessionUser<'a>,
pub maybe_include_job_statuses: Option<&'a HashSet<JobStatusPlus>>,
pub maybe_exclude_job_statuses: Option<&'a HashSet<JobStatusPlus>>,
}
@@ -63,14 +68,29 @@ pub async fn list_session_jobs_from_connection(
// Furthermore, even though the join query's `created_at` lacks an index, sort
// by id will accomplish this quickly.
{
query_builder.push(r#"
INNER JOIN (
SELECT id
FROM generic_inference_jobs
WHERE maybe_creator_user_token =
"#);
query_builder.push_bind(args.user_token.to_string());
match args.user {
SessionUser::User(user_token) => {
query_builder.push(r#"
INNER JOIN (
SELECT id
FROM generic_inference_jobs
WHERE maybe_creator_user_token =
"#);
query_builder.push_bind(user_token.to_string());
}
SessionUser::Anonymous(avt_token) => {
query_builder.push(r#"
INNER JOIN (
SELECT id
FROM generic_inference_jobs
WHERE maybe_creator_anonymous_visitor_token =
"#);
query_builder.push_bind(avt_token.to_string());
query_builder.push(r#"
AND maybe_creator_user_token IS NULL
"#);
}
}
query_builder.push(r#"
AND created_at > DATE_SUB(NOW(), INTERVAL 36 HOUR)
@@ -16,9 +16,12 @@ use log::warn;
use mysql_queries::queries::media_files::get::get_media_file::get_media_file;
use openai_sora_client::credentials::SoraCredentials;
use openai_sora_client::image_gen::SoraError;
use openai_sora_client::sentinel_refresh::generate::token::generate_token;
use openai_sora_client::sentinel_refresh::refresh_sentinel;
use serde::Deserialize;
use serde::Serialize;
use shared_service_components::sora_redis_credentials::set_sora_credential_field_in_redis::set_sora_credential_field_in_redis;
use shared_service_components::sora_redis_credentials::keys::RedisSoraCredentialSubkey;
use sqlx::MySqlPool;
use tempdir::TempDir;
use utoipa::ToSchema;
@@ -76,6 +79,9 @@ pub struct EnqueueStudioImageGenRequest {
/// The user's image generation prompt.
pub prompt: String,
/// Turn off the system prompt.
pub disable_system_prompt: Option<bool>,
/// Additional images to include (optional). Up to nine images.
pub maybe_additional_images: Option<Vec<MediaFileToken>>,
@@ -94,6 +100,7 @@ pub enum EnqueueImageGenRequestError {
NotAuthorized,
ServerError,
RateLimited,
TooManyConcurrentTasks,
}
impl ResponseError for EnqueueImageGenRequestError {
@@ -103,6 +110,7 @@ impl ResponseError for EnqueueImageGenRequestError {
EnqueueImageGenRequestError::NotAuthorized => StatusCode::UNAUTHORIZED,
EnqueueImageGenRequestError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
EnqueueImageGenRequestError::RateLimited => StatusCode::TOO_MANY_REQUESTS,
EnqueueImageGenRequestError::TooManyConcurrentTasks => StatusCode::TOO_MANY_REQUESTS,
}
}
@@ -112,6 +120,7 @@ impl ResponseError for EnqueueImageGenRequestError {
EnqueueImageGenRequestError::NotAuthorized => "unauthorized".to_string(),
EnqueueImageGenRequestError::ServerError => "server error".to_string(),
EnqueueImageGenRequestError::RateLimited => "rate limited".to_string(),
EnqueueImageGenRequestError::TooManyConcurrentTasks => "too many concurrent tasks".to_string(),
};
to_simple_json_error(&error_reason, self.status_code())
@@ -283,30 +292,69 @@ pub async fn enqueue_studio_image_generation_handler(http_request: HttpRequest,
None => 1,
};
let prompt = request.prompt.clone();
let prompt = create_prompt(&request);
let mut response = sora_image_gen_remix(SoraImageGenRemixRequest { prompt: prompt.clone(), num_images: NumImages::One, image_size: ImageSize::Square, sora_media_tokens: sora_media_tokens.clone(), credentials: &sora_credentials }).await;
let mut response = sora_image_gen_remix(SoraImageGenRemixRequest {
prompt: prompt.clone(),
num_images: NumImages::One,
image_size: ImageSize::Square,
sora_media_tokens: sora_media_tokens.clone(),
credentials: &sora_credentials
}).await;
debug!("Sora image gen response: {:?}", response);
if let Err(SoraError::SentinelBlock(msg)) = &response {
error!("Sora sentinel block, attempting refresh: {}", msg);
// Try to refresh sentinel
match refresh_sentinel().await {
Ok(new_sentinel) => {
// Update the credentials with new sentinel
match generate_token().await {
Ok(sentinel_token) => {
set_sora_credential_field_in_redis(
&mut redis,
RedisSoraCredentialSubkey::Sentinel,
&sentinel_token
).map_err(|e| {
EnqueueImageGenRequestError::ServerError
})?;
let updated_sora_credentials = get_sora_credentials_from_request(&http_request, &mut redis).map_err(|e| {
error!("sora credential error: {:?}", e);
EnqueueImageGenRequestError::ServerError
})?;
// Retry the request with new sentinel
response = sora_image_gen_remix(SoraImageGenRemixRequest { prompt: prompt.clone(), num_images: NumImages::One, image_size: ImageSize::Square, sora_media_tokens: sora_media_tokens.clone(), credentials: &updated_sora_credentials }).await;
response = sora_image_gen_remix(SoraImageGenRemixRequest {
prompt: prompt.clone(),
num_images: NumImages::One,
image_size: ImageSize::Square,
sora_media_tokens: sora_media_tokens.clone(),
credentials: &updated_sora_credentials
}).await;
},
Err(e) => {
error!("Failed to refresh Sora sentinel: {:?}", e);
},
// Fallback to the old refresh stragegy relying on the external service
match refresh_sentinel().await {
Ok(new_sentinel) => {
// Update the credentials with new sentinel
let updated_sora_credentials = get_sora_credentials_from_request(&http_request, &mut redis).map_err(|e| {
error!("sora credential error: {:?}", e);
EnqueueImageGenRequestError::ServerError
})?;
// Retry the request with new sentinel
response = sora_image_gen_remix(SoraImageGenRemixRequest {
prompt: prompt.clone(),
num_images: NumImages::One,
image_size: ImageSize::Square,
sora_media_tokens: sora_media_tokens.clone(),
credentials: &updated_sora_credentials
}).await;
},
Err(e) => {
error!("Failed to refresh Sora sentinel: {:?}", e);
}
}
}
}
}
@@ -315,21 +363,34 @@ pub async fn enqueue_studio_image_generation_handler(http_request: HttpRequest,
SoraError::TokenExpired(msg) => {
error!("Sora token expired, needs refresh: {}", msg);
// TODO: Implement token refresh logic here
EnqueueImageGenRequestError::ServerError
},
SoraError::SentinelBlock(msg) => {
error!("Sora sentinel block still occurring after refresh: {}", msg);
EnqueueImageGenRequestError::ServerError
},
SoraError::TooManyConcurrentTasks(msg) => {
error!("Sora too many concurrent tasks: {}", msg);
EnqueueImageGenRequestError::TooManyConcurrentTasks
},
_ => {
error!("Failed to call Sora image generation: {:?}", err);
EnqueueImageGenRequestError::ServerError
},
}
EnqueueImageGenRequestError::ServerError
})?;
debug!("Sora image gen response: {:?}", response);
// Store the actual inference args that will go to the database
let inference_args = SoraImageGenArgs { prompt: Some(prompt), scene_snapshot_media_token: Some(request.snapshot_media_token.clone()), maybe_additional_media_file_tokens: Some(additional_images), maybe_number_of_samples: Some(number_of_samples), maybe_sora_media_upload_tokens: Some(sora_media_tokens), maybe_sora_task_id: Some(response.task_id) };
let inference_args = SoraImageGenArgs {
prompt: Some(prompt),
scene_snapshot_media_token: Some(request.snapshot_media_token.clone()),
maybe_additional_media_file_tokens: Some(additional_images),
maybe_number_of_samples: Some(number_of_samples),
maybe_sora_media_upload_tokens: Some(sora_media_tokens),
maybe_sora_task_id: Some(response.task_id)
};
// create the inference args here
let maybe_avt_token = server_state.avt_cookie_manager.get_avt_token_from_request(&http_request);
@@ -348,7 +409,10 @@ pub async fn enqueue_studio_image_generation_handler(http_request: HttpRequest,
maybe_cover_image_media_file_token: None,
maybe_raw_inference_text: None,
maybe_max_duration_seconds: None,
maybe_inference_args: Some(GenericInferenceArgs { inference_category: Some(InferenceCategoryAbbreviated::ImageGeneration), args: Some(PolymorphicInferenceArgs::Sg(inference_args)) }),
maybe_inference_args: Some(GenericInferenceArgs {
inference_category: Some(InferenceCategoryAbbreviated::ImageGeneration),
args: Some(PolymorphicInferenceArgs::Sg(inference_args)),
}),
maybe_creator_user_token: maybe_user_token.as_ref(),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
@@ -412,7 +476,10 @@ async fn query_and_download_media_file(media_file_token: &MediaFileToken, downlo
},
};
let media_file_bucket_path = MediaFileBucketPath::from_object_hash(&media_file.public_bucket_directory_hash, media_file.maybe_public_bucket_prefix.as_deref(), media_file.maybe_public_bucket_extension.as_deref());
let media_file_bucket_path = MediaFileBucketPath::from_object_hash(
&media_file.public_bucket_directory_hash,
media_file.maybe_public_bucket_prefix.as_deref(),
media_file.maybe_public_bucket_extension.as_deref());
let extension = media_file.maybe_public_bucket_extension.clone().unwrap_or_else(|| ".jpg".to_string());
let download_filename = format!("download_file_{}.{}", media_file.token.as_str(), extension);
@@ -424,3 +491,30 @@ async fn query_and_download_media_file(media_file_token: &MediaFileToken, downlo
Ok(download_file_path)
}
const SYSTEM_PROMPT_SINGLE_IMAGE: &str =
"Use the image as a reference for the composition exactly. Copy the character pose, layout, scene, and the objects exactly - no deviation.";
const SYSTEM_PROMPT_MULTI_IMAGE: &str =
"Use the first image as a reference for the composition exactly. Copy the character pose, layout, scene, and the objects exactly from the first image - no deviation.";
fn create_prompt(request: &EnqueueStudioImageGenRequest) -> String {
let disable_system_prompt = request.disable_system_prompt
.clone()
.unwrap_or(false);
if disable_system_prompt {
return request.prompt.clone();
}
let extra_image_count = request.maybe_additional_images
.as_ref()
.map(|v| v.len())
.unwrap_or(0);
if extra_image_count == 0 {
format!("{} {}", SYSTEM_PROMPT_SINGLE_IMAGE, request.prompt)
} else {
format!("{} {}", SYSTEM_PROMPT_MULTI_IMAGE, request.prompt)
}
}
@@ -31,7 +31,7 @@ use enums::by_table::generic_inference_jobs::inference_category::InferenceCatego
use enums::common::job_status_plus::JobStatusPlus;
use enums::no_table::style_transfer::style_transfer_name::StyleTransferName;
use mysql_queries::queries::generic_inference::web::job_status::GenericInferenceJobStatus;
use mysql_queries::queries::generic_inference::web::list_session_jobs::{list_session_jobs_from_connection, ListSessionJobsForUserArgs};
use mysql_queries::queries::generic_inference::web::list_session_jobs::{list_session_jobs_from_connection, ListSessionJobsForUserArgs, SessionUser};
use primitives::numerics::i64_to_u64_zero_clamped::i64_to_u64_zero_clamped;
use redis_common::redis_keys::RedisKeys;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
@@ -238,11 +238,6 @@ pub async fn list_session_jobs_handler(
ListSessionJobsError::ServerError
})?;
let session = match maybe_user_session {
Some(session) => session,
None => return Err(ListSessionJobsError::NotAuthorized),
};
let include_states = query.include_states
.as_deref()
.map(|s| s.split(",")
@@ -255,8 +250,23 @@ pub async fn list_session_jobs_handler(
.filter_map(|status| JobStatusPlus::from_str(status).ok())
.collect::<HashSet<_>>());
let user;
match (maybe_user_session.as_ref(), maybe_avt_token.as_ref()) {
(Some(session), _) => {
user = SessionUser::User(&session.user_token_typed);
}
(None, Some(avt_token)) => {
user = SessionUser::Anonymous(avt_token);
}
(None, None) => {
// TODO(bt,2025-04-15): We should install an AVT cookie.
return Err(ListSessionJobsError::NotAuthorized);
}
}
let args = ListSessionJobsForUserArgs {
user_token: &session.user_token_typed,
user,
maybe_include_job_statuses: include_states.as_ref(),
maybe_exclude_job_statuses: exclude_states.as_ref(),
};
+1
View File
@@ -0,0 +1 @@
node_modules
+56
View File
@@ -0,0 +1,56 @@
{
"root": true,
"ignorePatterns": [
"**/*"
],
"plugins": [
"@nx"
],
"overrides": [
{
"files": [
"*.ts",
"*.tsx",
"*.js",
"*.jsx"
],
"rules": {
"@nx/enforce-module-boundaries": [
"error",
{
"enforceBuildableLibDependency": true,
"allow": [],
"depConstraints": [
{
"sourceTag": "*",
"onlyDependOnLibsWithTags": [
"*"
]
}
]
}
]
}
},
{
"files": [
"*.ts",
"*.tsx"
],
"extends": [
"plugin:@nx/typescript"
],
"rules": {}
},
{
"files": [
"*.js",
"*.jsx"
],
"extends": [
"plugin:@nx/javascript"
],
"rules": {}
}
]
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"recommendations": [
"nrwl.angular-console",
"esbenp.prettier-vscode"
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint"
]
}
+30 -2
View File
@@ -2,8 +2,7 @@
This is an `nx` monorepo that can contain multiple apps and shared libraries.
All commands to run these projects are performed from _this_ directory.
All commands to run these projects are performed from _this_ directory.
## Install dependencies
@@ -27,4 +26,33 @@ Run dev server:
nx dev editor3d
```
```
call when starting on main or a new branch:
./clean_modules
The names for @frontend and @storyteller come from the package.json file in the libs folder.
import { Login } from "@frontend/login";
import { api } from "@storyteller/api";
For shared UI components, import from @storyteller/ui-[componentname].
import { Button } from "@storyteller/ui-button";
import { Modal } from "@storyteller/ui-modal";
building:
npx nx build editor3d
npx nx build editor2d
dev:
npx nx dev editor3d
npx nx dev editor2d
# component build
1.
npx nx g @nx/react:library libs/components/toaster --import-path=@storyteller/ui-toaster --bundler=vite
npm install
2.import it first in code
3.nx sync
4.nx build
```
+1 -2
View File
@@ -1,5 +1,5 @@
[build]
#command = "./script/netlify_build.sh"
command = "./apps/editor2d/script/netlify_build.sh"
publish = "apps/editor2d/dist/"
#[build.environment]
@@ -16,7 +16,6 @@ publish = "apps/editor2d/dist/"
for = "/build/*"
[headers.values]
"Cache-Control" = "public, max-age=31560000, immutable"
# [[redirects]]
+8 -2
View File
@@ -4,7 +4,14 @@
"version": "0.0.0",
"type": "module",
"nx": {
"name": "editor2d"
"name": "editor2d",
"targets": {
"build": {
"dependsOn": [
"^build"
]
}
}
},
"scripts": {
"dev": "echo 'editor2d' && vite",
@@ -21,7 +28,6 @@
"@fortawesome/pro-regular-svg-icons": "^6.7.2",
"@fortawesome/pro-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@headlessui/react": "^2.1.3",
"@preact/signals-react": "^2.2.0",
"@tauri-apps/api": "^2.2.0",
"@techstark/opencv-js": "^4.10.0-release.1",

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.
@@ -15,6 +15,6 @@ cp _redirects dist/
echo "Copy netlify 404.html page into dist"
cp "404.html" dist/
echo "List files after build"
echo "List final files in build"
find dist/
@@ -0,0 +1,122 @@
import { faFolder } from "@fortawesome/pro-solid-svg-icons"
import { useEffect, useState } from "react";
import { Button } from "~/components/ui"
const testUrl = "https://api.fakeyou.com/v1/media_files/list/user/hanashi?include_user_uploads=true&page_size=24&filter_media_classes=unknown";
const getTestData = (callback: (data: any) => void) => {
const netCall = async () => {
// GET the test url
const testResults = await fetch(testUrl);
const testJson = await testResults.json();
console.log("testResults", testJson);
return testJson;
}
netCall().then(callback);
}
export const GalleryRootComponent = ({
className
}: {
className?: string
}) => {
const [testData, setTestData] = useState<any>();
useEffect(() => {
getTestData(setTestData);
}, [])
const aggregatedData = aggregateGalleryData({ data: testData });
return (
<div className={className}>
<div className="m-32">
<div className="flex w-fit gap-x-6 items-center">
<span className="text-white font-bold text-5xl">My Gallery</span>
<Button variant="secondary" icon={faFolder} className="h-8">
Open Folder
</Button>
</div>
<div className="overflow-y-auto mt-12 flex flex-col gap-y-8">
{Array.from(aggregatedData.entries()).map(([dateEpoch, results]) => (<GallerySection dateEpoch={dateEpoch} results={results} />))}
</div>
</div>
</div>
)
}
const GallerySection = ({
dateEpoch,
results
}: {
dateEpoch: number,
results: any[]
}) => {
const dateObj = new Date(dateEpoch);
const title = dateObj.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric"
});
return (
<div className="w-full">
<span className="text-lg">{title}</span>
<div className="flex gap-4 flex-wrap mt-4">
{results.map((result) => (<GalleryCard result={result} />))}
</div>
</div>
)
}
const GalleryCard = ({ result }: {
result: {
media_links: {
cdn_url: string,
maybe_thumbnail_template: string,
},
}
}) => {
return (
<div className="rounded-md size-48 flex items-center justify-center overflow-hidden">
<img src={result.media_links.cdn_url} alt="Thumbnail" className="h-full w-full object-cover" />
</div>
)
}
const aggregateGalleryData = ({
data
}: {
data?: {
results: any[]
}
}) => {
const aggregateMap = new Map<number, any[]>();
if (!data) {
return aggregateMap;
}
data.results.forEach((result) => {
if (result.media_type !== "image") {
return;
}
const dateString = result.updated_at as string;
const dateObj = new Date(dateString);
// Move it the start of the day for comparison
dateObj.setUTCHours(0, 0, 0, 0);
const dateEpoch = dateObj.getTime();
if (!aggregateMap.has(dateEpoch)) {
aggregateMap.set(dateEpoch, []);
}
aggregateMap.get(dateEpoch)!.push(result);
})
return aggregateMap;
}
@@ -0,0 +1 @@
export * from "./GalleryRootComponent";
+40 -19
View File
@@ -148,7 +148,6 @@ export class ApiManager {
formData.append("file", blob);
}
// console.log(`HELLO${formData}`);
return this.fetchMultipartFormData<T>(endpoint, {
method: "POST",
headers: {
@@ -281,10 +280,12 @@ export class Api extends ApiManager {
}
public async enqueueImageGeneration({
disableSystemPrompt,
prompt,
snapshotMediaToken,
additionalImages,
}: {
disableSystemPrompt: boolean;
prompt: string;
snapshotMediaToken: string;
additionalImages?: string[];
@@ -294,6 +295,7 @@ export class Api extends ApiManager {
const uuidIdempotencyToken = crypto.randomUUID(); // Generate a new UUID
const body = {
uuid_idempotency_token: uuidIdempotencyToken,
disable_system_prompt: disableSystemPrompt,
prompt,
snapshot_media_token: snapshotMediaToken, // Changed from scene_media_token to snapshot_media_token
additional_images: additionalImages,
@@ -302,6 +304,7 @@ export class Api extends ApiManager {
const postResponse = await this.post<
{
uuid_idempotency_token: string;
disable_system_prompt: boolean;
prompt: string;
snapshot_media_token: string;
additional_images?: string[];
@@ -327,7 +330,10 @@ export class Api extends ApiManager {
return result;
}
public async pollJobSession(jobToken: string): Promise<
public async pollJobSession(
jobToken: string,
thumbnailWidth: number = 256,
): Promise<
ApiResponse<{
job_token: string;
request: {
@@ -338,6 +344,9 @@ export class Api extends ApiManager {
progress_percentage: number;
};
result: {
image_url: string;
thumbnail_url: string;
public_bucket_path: string;
generated_images?: string[];
error?: string;
};
@@ -345,41 +354,53 @@ export class Api extends ApiManager {
> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/jobs/job/${jobToken}`;
const response = await this.get<{
success?: boolean;
status?: string;
result?: {
generated_images?: string[];
error?: string;
};
BadInput?: string;
}>({
const response = await this.get<any>({
endpoint,
});
console.log("Job Polling Response:");
console.log(response);
let image_url = response.state.maybe_result?.media_links?.cdn_url;
let public_bucket_path =
response.state.maybe_result?.maybe_public_bucket_media_path;
let thumbnail_url =
response.state.maybe_result?.media_links?.maybe_thumbnail_template?.replace(
"{WIDTH}",
thumbnailWidth.toString(),
);
let progress_percentage = response.state.status.progress_percentage;
let status_string = response.state.status.status;
console.log("Image URL:", image_url);
console.log("Thumbnail URL:", thumbnail_url);
console.log("Progress Percentage:", progress_percentage);
console.log("Status", status_string);
console.log("response FROM JOBS");
console.log(response);
const success = response.success ?? false;
const status = response.status ?? "";
const result = response.result ?? {
generated_images: [],
error: undefined,
};
const errorMessage = response.BadInput;
return {
success,
data: {
result,
result: {
image_url: image_url || "",
thumbnail_url: thumbnail_url || "",
public_bucket_path: public_bucket_path || "",
error: undefined,
},
job_token: jobToken,
request: {
maybe_model_title: "Image Generation",
},
status: {
status: status.toLowerCase(),
progress_percentage: 0,
status: status_string.toLowerCase(),
progress_percentage: progress_percentage,
},
},
errorMessage,
@@ -62,6 +62,7 @@ export class EditEngine {
this.editMode = EditModes.SELECT;
this.boardCanvasRef = boardCanvasRef;
this.stage = new Konva.Stage({
container: this.boardCanvasRef,
width: window.innerWidth,
@@ -238,13 +239,19 @@ export class EditEngine {
}
private onBoardCanvasResize() {
this.realTimeDrawEngine.updateCaptureCanvas(undefined, undefined);
this.realTimeDrawEngine.updateCaptureCanvas(0, 0);
console.log("onBoardCanvasResize Event called");
console.log("Board Canvas values below");
console.log(this.boardCanvasRef.offsetWidth);
console.log(this.boardCanvasRef.offsetHeight);
this.matteBox.updateSize({
boardCanvasSize: {
width: this.boardCanvasRef.offsetWidth,
height: this.boardCanvasRef.offsetHeight,
},
});
this.uiLayer.draw();
this.stage.width(this.boardCanvasRef.offsetWidth);
this.stage.height(this.boardCanvasRef.offsetHeight);
+17 -4
View File
@@ -55,7 +55,7 @@ export class Engine {
private uiLayer: Konva.Layer;
private previewLayer: Konva.Layer;
private offScreenCanvas: OffscreenCanvas;
private realTimeDrawEngine: RealTimeDrawEngine;
public realTimeDrawEngine: RealTimeDrawEngine;
private nodesManager: NodesManager;
private nodeIsolator: NodeIsolator;
private nodeTransformer: NodeTransformer;
@@ -66,8 +66,8 @@ export class Engine {
private backgroundNode: ShapeNode | null = null;
private sceneManager: SceneManager;
private undoStackManager: UndoStackManager;
private commandManager: CommandManager;
public undoStackManager: UndoStackManager;
public commandManager: CommandManager;
private videoExtractionHandler: VideoExtractionHandler;
public segmentationButtonCanBePressed: boolean = true;
@@ -510,7 +510,20 @@ export class Engine {
}
private onBoardCanvasResize() {
this.realTimeDrawEngine.updateCaptureCanvas(undefined, undefined);
console.log("onBoardCanvasResize Event called");
// Print window dimensions for debugging
console.log("Window dimensions:", {
innerWidth: window.innerWidth,
innerHeight: window.innerHeight,
outerWidth: window.outerWidth,
outerHeight: window.outerHeight,
});
this.realTimeDrawEngine.updateCaptureCanvas(
window.innerWidth,
window.innerHeight,
);
this.matteBox.updateSize({
boardCanvasSize: {
width: this.boardCanvasRef.offsetWidth,
@@ -56,11 +56,8 @@ export class ShapeNode extends NetworkedNode {
switch (shapeType) {
case ShapeType.CIRCLE:
shape = new Konva.Ellipse({
x: size.width,
y: size.height,
radiusX: size.width,
radiusY: size.height,
...transform,
radiusX: size.width / 2,
radiusY: size.height / 2,
fill: color,
strokeScaleEnabled: false,
draggable: true,
@@ -80,8 +77,6 @@ export class ShapeNode extends NetworkedNode {
case ShapeType.TRIANGLE:
shape = new Konva.Shape({
x: 0,
y: 0,
width: size.width,
height: size.height,
...transform,
@@ -113,12 +108,20 @@ export class ShapeNode extends NetworkedNode {
this.size = size;
this.color = color;
// Add shape to group
// Center the shape
const centerPosition = NodeUtilities.positionNodeOnCanvasCenter({
canvasOffset: canvasPosition,
componentSize: canvasSize,
componentSize: this.size,
maxSize: canvasSize,
});
// If the shape is an ellipse, the center is the origin so we need to offset it
if (shapeType === ShapeType.CIRCLE) {
centerPosition.x += size.width / 2;
centerPosition.y += size.height / 2;
}
// Add shape to group
// bring to the front
this.kNode.moveToTop();
this.kNode.setPosition(centerPosition);
@@ -516,36 +516,74 @@ export class RealTimeDrawEngine {
width: number | undefined,
height: number | undefined,
) {
console.log("updateCaptureCanvas");
if (!this.captureCanvas) {
return;
}
if (width) {
this.width = width;
}
if (height) {
this.height = height;
}
// Ensures that all the nodes stag in the same place should
// there be a window resize.
// recompute the position
// to ensure that the position of this stays
const scaleFactor = 0.9;
// Store old values for calculating delta
const oldPositionX = this.positionX;
const oldPositionY = this.positionY;
// recompute the position
const oldWidth = this.width;
const oldHeight = this.height;
// Original aspect ratio
const originalAspectRatio = oldWidth / oldHeight;
// Calculate new dimensions based on window size
let newWidth: number;
let newHeight: number;
if (width === undefined) {
newWidth = window.innerWidth * scaleFactor;
} else {
newWidth = width * scaleFactor;
}
if (height === undefined) {
newHeight = window.innerHeight * scaleFactor;
} else {
newHeight = height * scaleFactor;
}
// Preserve aspect ratio
const newAspectRatio = newWidth / newHeight;
if (newAspectRatio > originalAspectRatio) {
// Width is constraining factor
newWidth = newHeight * originalAspectRatio;
} else {
// Height is constraining factor
newHeight = newWidth / originalAspectRatio;
}
this.width = newWidth;
this.height = newHeight;
console.log("New dimensions:", this.width, this.height);
// Calculate new position to center the canvas in the window
this.positionX = (window.innerWidth - this.width) / 2;
this.positionY = (window.innerHeight - this.height) / 2;
console.log("New centered position:", this.positionX, this.positionY);
// Update the canvas position and size
this.captureCanvas.setPosition({
x: this.positionX / 2,
y: this.positionY / 2,
x: this.positionX,
y: this.positionY,
});
this.captureCanvas.size({ width: this.width, height: this.height });
// this is the change in positions
const deltaX = this.positionX - oldPositionX;
const deltaY = this.positionY - oldPositionY;
// Calculate scale factors to maintain proportions
const scaleX = this.width / oldWidth;
const scaleY = this.height / oldHeight;
// Use a uniform scale factor to preserve aspect ratio
const uniformScale = Math.min(scaleX, scaleY);
// Update all children except the capture canvas
var children = this.mediaLayerRef.getChildren();
for (let i = 0; i < children.length; i++) {
let node = children[i];
@@ -554,11 +592,27 @@ export class RealTimeDrawEngine {
if (node.name() === "CaptureCanvas") {
continue;
}
const pos = node.getPosition();
const oldScale = node.scale();
// Apply both position shift and scaling to maintain relative positioning
node.setPosition({
x: pos.x + deltaX,
y: pos.y + deltaY,
x: (pos.x - oldPositionX) * uniformScale + this.positionX,
y: (pos.y - oldPositionY) * uniformScale + this.positionY,
});
// Update scale proportionally if the node has a scale property
if (
oldScale &&
typeof oldScale.x === "number" &&
typeof oldScale.y === "number"
) {
node.scale({
x: oldScale.x * uniformScale,
y: oldScale.y * uniformScale,
});
}
}
this.mediaLayerRef.batchDraw();
@@ -1,14 +1,12 @@
import { useCallback, useRef } from "react";
import { useCallback, useRef, useState } from "react";
// Components
import { KonvaCanvasContainer } from "./KonvaCanvasContainer";
import { ContextualButtonRetry } from "./ContextualButtonRetry";
import { ContextualToolbarNode } from "./ContextualToolbarNode";
import { ContextualLoadingBar } from "./ContextualLoadingBar";
import { SignaledCanvasDragDropFiles } from "./SignaledCanvasDragDropFiles";
import { SignaledDialogs } from "./SignaledDialogs";
import { SignaledToolbarMain } from "./SignaledToolbarMain";
import { SignaledToolbarVideoExtraction } from "./SignaledToolbarVideoExtraction";
import { PromptBox } from "~/components/PromptBox";
// The KonvaApp is the root of the Konva stage
// and only entry point for anything in Konva JS
@@ -22,9 +20,8 @@ import { useLayoutContext } from "./contextSignals/layout";
// common hooks
import { useRenderCounter } from "~/hooks/useRenderCounter";
import { useNavigate } from "react-router-dom";
import { SignaledMagicBox } from "./SignaledMagicBox";
import { SignaledPromptSlider } from "./SignaledPromptSlider";
import { SignaledPromptText } from "./SignaledPromptText";
import { UndoRedo } from "~/components/reusable/UndoRedo/UndoRedo";
export const KonvaRootComponent = ({
className,
@@ -39,44 +36,66 @@ export const KonvaRootComponent = ({
const navigate = useNavigate();
const appUiContext = useAppUiContext();
const layoutContext = useLayoutContext();
// Add state to track engine initialization
const [isEngineReady, setIsEngineReady] = useState(false);
const engineRef = useRef<EngineType | null>(null);
const konvaContainerCallbackRef = useCallback((node: HTMLDivElement) => {
if (node !== null && engineRef.current === null) {
const options = {
navigate: navigate,
sceneToken: sceneToken,
};
engineRef.current = KonvaApp(node, options);
// Only initialize if we have a node and haven't initialized yet
if (node !== null && !isEngineReady && engineRef.current === null) {
try {
const options = {
navigate: navigate,
sceneToken: sceneToken,
};
engineRef.current = KonvaApp(node, options);
// Only set ready if initialization succeeded
if (engineRef.current) {
setIsEngineReady(true);
}
} catch (error) {
console.error("Failed to initialize KonvaApp:", error);
engineRef.current = null;
setIsEngineReady(false);
}
}
}, []);
}, [navigate, sceneToken, isEngineReady]);
return (
<>
{/* Always render the canvas container */}
<KonvaCanvasContainer
ref={konvaContainerCallbackRef}
className={className}
// retreive the classNames from the parent for sizing/styling
/>
{/* <SignaledMagicBox /> */}
<SignaledCanvasDragDropFiles
openAddImage={appUiContext.openAddImage}
openAddVideo={appUiContext.openAddVideo}
/>
<PromptBox />
<SignaledToolbarMain
layoutSignal={layoutContext.signal}
appUiContext={appUiContext}
/>
{/* <SignaledToolbarVideoExtraction /> */}
<SignaledDialogs
appUiSignal={appUiContext.signal}
resetAll={appUiContext.resetAll}
/>
{/* Conditionally render the loading overlay */}
{!isEngineReady && (
<div className="absolute inset-0 flex items-center justify-center">
<div className="text-gray-500">Initializing canvas...</div>
</div>
)}
{/* <ContextualLoadingBar /> */}
<ContextualToolbarNode />
{/* <ContextualButtonRetry /> */}
{/* Conditionally render the UI components that need the engine */}
{isEngineReady && engineRef.current && (
<UndoRedo engine={engineRef.current}>
<SignaledCanvasDragDropFiles
openAddImage={appUiContext.openAddImage}
openAddVideo={appUiContext.openAddVideo}
/>
<PromptBox />
<SignaledToolbarMain
layoutSignal={layoutContext.signal}
appUiContext={appUiContext}
/>
<SignaledDialogs
appUiSignal={appUiContext.signal}
resetAll={appUiContext.resetAll}
/>
<ContextualToolbarNode />
</UndoRedo>
)}
</>
);
};
+37 -22
View File
@@ -2,6 +2,38 @@
@tailwind components;
@tailwind utilities;
* {
scrollbar-width: thin;
scrollbar-color: #363636 #171717;
}
/* Works on Chrome, Edge, and Safari */
*::-webkit-scrollbar {
width: 8px;
}
*::-webkit-scrollbar-track {
background: #171717;
}
*::-webkit-scrollbar-thumb {
background-color: #363636;
border-radius: 4px;
border: 2px solid #171717;
}
/* Chrome, Safari, Edge, Opera */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type="number"] {
-moz-appearance: textfield;
}
@layer base {
:root {
@apply select-none bg-ui-background font-sans text-white;
@@ -27,28 +59,7 @@
background-size: 36px 36px;
@apply bg-center;
}
h1 {
@apply text-3xl font-medium md:text-5xl;
}
h2 {
@apply text-2xl font-medium md:text-4xl;
}
h3 {
@apply text-xl font-medium md:text-3xl;
}
h4 {
@apply text-lg font-medium;
}
h5,
label {
@apply text-base;
}
h6 {
@apply text-sm;
}
p {
@apply text-sm font-normal;
}
a {
@apply font-medium text-primary-400 hover:text-primary-300;
}
@@ -125,6 +136,10 @@
@apply relative border border-ui-border shadow-lg backdrop-blur-xl transition-colors duration-300;
@apply bg-ui-controls/50 hover:bg-ui-controls/90 !important;
}
.glass-no-hover {
@apply hover:bg-ui-controls/50 !important;
}
}
@keyframes glow {
+10 -16
View File
@@ -1,12 +1,9 @@
import { useRef } from "react";
import { useNavigate, useSearchParams, Link } from "react-router-dom";
import { useSignalEffect } from "@preact/signals-react/runtime";
import { faKey, faUser } from "@fortawesome/pro-solid-svg-icons";
import { Button, Input, LoadingSpinner } from "~/components/ui";
import { authentication } from "~/signals";
import { paperWrapperStyles } from "~/components/styles";
import { twMerge } from "tailwind-merge";
export const Login = () => {
@@ -62,24 +59,21 @@ export const Login = () => {
});
return (
<div className="fixed flex h-full w-full flex-col items-center justify-center bg-[url('/svg/bg-dots.svg')] bg-[length:200px_200px] bg-center bg-repeat">
<div className="my-7 flex w-10/12 max-w-2xl items-center justify-center gap-4">
<div className="pegboard fixed flex h-full w-full flex-col items-center justify-center bg-center bg-repeat">
<div className="mb-7 flex w-10/12 max-w-2xl items-center justify-center gap-4">
<img
src="/brand/Storyteller-Logo-Black.png"
alt="Storyteller Logo"
className="h-11 select-none"
src="/brand/artcraft-logo.png"
alt="ArtCraft Logo"
className="h-9 select-none"
/>
</div>
<div className="w-full max-w-xl rounded-xl shadow-lg">
<div
className={twMerge(
paperWrapperStyles,
"relative mx-auto w-full rounded-none rounded-t-xl p-8 shadow-none",
"glass glass-no-hover relative mx-auto w-full rounded-none rounded-t-xl p-6 shadow-none",
)}
>
<h1 className="mb-9 text-center text-2xl font-bold">
Log in to Board
</h1>
<h1 className="mb-6 text-center text-2xl font-bold">Log in</h1>
<form
ref={formRef}
onSubmit={handleOnSumbit}
@@ -106,7 +100,7 @@ export const Login = () => {
required
/>
<Button className="mt-6 py-3">Continue</Button>
<Button className="mt-4 py-3">Continue</Button>
</form>
{shouldShowLoader && (
<div className="absolute left-0 top-0 flex h-full w-full items-center justify-center">
@@ -114,10 +108,10 @@ export const Login = () => {
</div>
)}
</div>
<div className="align-items flex w-full max-w-xl rounded-b-xl border border-t-0 bg-gray-100 px-8 py-4">
<div className="align-items flex w-full max-w-xl rounded-b-xl bg-ui-controls px-6 py-4">
<a
href="https://storyteller.ai/password-reset"
className="text-brand-primary hover:text-brand-primary-400 grow text-sm transition-all duration-150"
className="grow text-sm !text-primary-400 transition-all duration-150 hover:!text-primary-300"
>
Forgot your password?
</a>
@@ -1,6 +1,7 @@
import { useParams } from "react-router-dom";
import { withProtectionRoute } from "~/components/hoc";
import { useRenderCounter } from "~/hooks/useRenderCounter";
import { Resource, invoke } from '@tauri-apps/api/core';
// Components of the page
import { ToolbarUserProfile } from "~/components/features";
@@ -14,9 +15,22 @@ import { appMode } from "~/signals";
import { useSignals } from "@preact/signals-react/runtime";
import { GenerationRootComponent } from "~/GenerationRootComponent/GenerationRootComponent";
import { EditModeRootComponent } from "~/EditModeRootComponent/EditModeRootComponent";
import { useRef } from "react";
import { useRef, useState } from "react";
import { EngineType } from "~/KonvaApp";
import { GenerationEngine } from "~/KonvaApp/GenerationEngine";
import { GalleryRootComponent } from "~/GalleryRootComponent";
import { BaseDialog } from "~/components/ui/BaseDialog";
import { Button } from "~/components/ui/Button";
import {
faArrowRight,
faMagicWandSparkles,
} from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
export const isTauri = (): boolean => {
console.log(window.__TAURI_INTERNALS__);
return window.__TAURI_INTERNALS__;
};
export const Main = withProtectionRoute(() => {
// This is a hook that will log the number of times the component has rerendered
@@ -31,7 +45,8 @@ export const Main = withProtectionRoute(() => {
const realtimeEngineRef = useRef<EngineType | null>(null);
const generationEngineRef = useRef<GenerationEngine | null>(null);
const [firstTimeDialogOpen, setFirstTimeDialogOpen] = useState(true);
const [signinDialogOpen, setSigninDialogOpen] = useState(false);
const appModeValue = appMode.value;
let childView;
@@ -59,21 +74,87 @@ export const Main = withProtectionRoute(() => {
}
return (
<div className="fixed grid h-full w-full grid-cols-12 grid-rows-12">
{childView}
<div className="absolute top-0 w-full p-3.5">
<div className="relative flex w-full">
<div className="absolute left-0">
<ToolbarTopLeft />
</div>
<div className="flex w-full justify-center">
<ToolbarTopCenter />
</div>
<div className="absolute right-0">
<ToolbarTopRight />
<>
<div className="fixed h-full w-full">
<div className="fixed grid h-full w-full grid-cols-12 grid-rows-12">
{childView}
</div>
<div className="absolute top-0 w-full p-3.5">
<div className="relative flex w-full">
<div className="absolute left-0">
<ToolbarTopLeft />
</div>
<div className="flex w-full justify-center">
<ToolbarTopCenter />
</div>
<div className="absolute right-0">
<ToolbarTopRight />
</div>
</div>
</div>
{appModeValue === "gallery" && (
<GalleryRootComponent className="z-1 fixed h-full w-full bg-black" />
)}
</div>
</div>
<BaseDialog
isOpen={firstTimeDialogOpen}
onClose={() => {
setFirstTimeDialogOpen(false);
}}
className="max-w-5xl"
>
<div className="flex flex-col items-center justify-center">
<div className="flex flex-col items-center gap-3">
<h1 className="text-3xl font-bold">
<FontAwesomeIcon
icon={faMagicWandSparkles}
className="mr-3 text-[24px]"
/>
Welcome to ArtCraft
</h1>
<div className="text-center">
<p className="text-lg font-medium text-white/80">
Your creative canvas for digital art and design
</p>
<p className="text-white/60">
Draw freely, create collages, and design with our intuitive
tool. Bring your ideas to life in seconds!
</p>
</div>
</div>
<div className="aspect-video w-full overflow-hidden rounded-md">
<video autoPlay muted loop controls={false}>
<source src="/videos/demo_video.mp4" type="video/mp4" />
</video>
</div>
<Button
className="font-semibold"
icon={faArrowRight}
iconFlip={true}
onClick={async () => {
if (isTauri()) {
await invoke('open_login_command');
setFirstTimeDialogOpen(false);
} else {
console.error('Tauri is not available in this environment');
}
}}
>
Signin to OpenAI to get Started
</Button>
{/* <Button
className="font-semibold"
icon={faArrowRight}
iconFlip={true}
onClick={() => {
setFirstTimeDialogOpen(false);
}}
>
Start creating now
</Button> */}
</div>
</BaseDialog>
</>
);
});
@@ -1,12 +1,9 @@
import { useRef } from "react";
import { useNavigate, useSearchParams, Link } from "react-router-dom";
import { useSignalEffect } from "@preact/signals-react/runtime";
import { faKey, faUser } from "@fortawesome/pro-solid-svg-icons";
import { Button, Input, LoadingSpinner } from "~/components/ui";
import { authentication } from "~/signals";
import { paperWrapperStyles } from "~/components/styles";
import { twMerge } from "tailwind-merge";
export const Signup = () => {
@@ -77,22 +74,21 @@ export const Signup = () => {
});
return (
<div className="fixed flex h-full w-full flex-col items-center justify-center bg-[url('/svg/bg-dots.svg')] bg-[length:200px_200px] bg-center bg-repeat">
<div className="my-7 flex w-10/12 max-w-2xl items-center justify-center gap-4">
<div className="pegboard fixed flex h-full w-full flex-col items-center justify-center bg-center bg-repeat">
<div className="mb-7 flex w-10/12 max-w-2xl items-center justify-center gap-4">
<img
src="/brand/Storyteller-Logo-Black.png"
alt="Storyteller Logo"
className="h-11 select-none"
src="/brand/artcraft-logo.png"
alt="ArtCraft Logo"
className="h-9 select-none"
/>
</div>
<div className="w-full max-w-xl rounded-xl shadow-lg">
<div
className={twMerge(
paperWrapperStyles,
"relative mx-auto w-full rounded-none rounded-t-xl p-8 shadow-none",
"glass glass-no-hover relative mx-auto w-full rounded-none rounded-t-xl p-6 shadow-none",
)}
>
<h1 className="mb-9 text-center text-2xl font-bold">
<h1 className="mb-6 text-center text-2xl font-bold">
Create your Account
</h1>
<form
@@ -139,7 +135,7 @@ export const Signup = () => {
required
/>
<Button className="mt-6 py-3">Create Account</Button>
<Button className="mt-4 py-3">Create Account</Button>
</form>
{shouldShowLoader && (
<div className="absolute left-0 top-0 flex h-full w-full items-center justify-center">
@@ -147,7 +143,7 @@ export const Signup = () => {
</div>
)}
</div>
<div className="align-items flex w-full max-w-xl rounded-b-xl border border-t-0 bg-gray-100 px-8 py-4">
<div className="align-items flex w-full max-w-xl rounded-b-xl bg-ui-controls px-6 py-4">
<div className="flex w-full justify-end gap-1 text-sm">
<p className="opacity-75">Already have an account?</p>
<Link to="/login">Log In</Link>
@@ -1,8 +1,10 @@
import React, { createContext, useContext, useState } from "react";
interface JobContextType {
jobToken: string | null;
setJobToken: (token: string | null) => void;
jobTokens: string[];
addJobToken: (token: string) => void;
removeJobToken: (token: string) => void;
clearJobTokens: () => void;
}
const JobContext = createContext<JobContextType | undefined>(undefined);
@@ -10,10 +12,24 @@ const JobContext = createContext<JobContextType | undefined>(undefined);
export const JobProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [jobToken, setJobToken] = useState<string | null>(null);
const [jobTokens, setJobTokens] = useState<string[]>([]);
const addJobToken = (token: string) => {
setJobTokens((prevTokens) => [...prevTokens, token]);
};
const removeJobToken = (token: string) => {
setJobTokens((prevTokens) => prevTokens.filter((t) => t !== token));
};
const clearJobTokens = () => {
setJobTokens([]);
};
return (
<JobContext.Provider value={{ jobToken, setJobToken }}>
<JobContext.Provider
value={{ jobTokens, addJobToken, removeJobToken, clearJobTokens }}
>
{children}
</JobContext.Provider>
);
@@ -47,7 +47,8 @@ export const PromptBox = () => {
const isDesktopApp = window.navigator.userAgent.includes("Tauri");
console.log("Is this a desktop app?", isDesktopApp);
const { jobToken, setJobToken } = useJobContext();
const { jobTokens, addJobToken, removeJobToken, clearJobTokens } =
useJobContext();
//const { lastRenderedBitmap } = useCanvasSignal();
const [prompt, setPrompt] = useState("");
@@ -119,7 +120,7 @@ export const PromptBox = () => {
setUploadingImages((prev) =>
prev.filter((img) => img.id !== uploadId),
);
console.log("Reference image added:", referenceImage);
toast.success("Reference image added.");
} else if (
newState.status === UploaderStates.assetError ||
newState.status === UploaderStates.imageCreateError
@@ -127,7 +128,8 @@ export const PromptBox = () => {
setUploadingImages((prev) =>
prev.filter((img) => img.id !== uploadId),
);
console.error("Upload failed");
toast.error("Upload failed. Please try again.");
}
},
});
@@ -175,7 +177,100 @@ export const PromptBox = () => {
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setPrompt(e.target.value);
};
const handleTauriEnqueue = async () => {
const api = new Api();
let image = getCanvasRenderBitmap();
if (image === undefined) {
return;
}
const base64Bitmap = await EncodeImageBitmapToBase64(image);
const byteString = atob(base64Bitmap);
const mimeString = "image/png";
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
const uuid = crypto.randomUUID(); // Generate a new UUID
const file = new File([ab], `${uuid}.png`, { type: mimeString });
const snapshotMediaToken = await api.uploadSceneSnapshot({
screenshot: file,
});
if (snapshotMediaToken.data === undefined) {
toast.error("Error: Unable to upload scene snapshot Please try again.");
return;
}
console.log("useSystemPrompt", useSystemPrompt);
console.log("Snapshot media token:", snapshotMediaToken.data);
const generateResponse = await invoke("sora_image_generation_command", {
snapshot_media_token: snapshotMediaToken.data,
disable_system_prompt: !useSystemPrompt,
prompt: prompt,
maybe_additional_images: referenceImages.map((image) => image.mediaToken),
maybe_number_of_samples: 1,
});
toast.success("Please wait while we process your image.");
};
const handleWebEnqueue = async () => {
const api = new Api();
let image = getCanvasRenderBitmap();
if (image === undefined) {
toast.error(
"Error: Unable to generate image. Please check the input and try again.",
);
return;
}
const base64Bitmap = await EncodeImageBitmapToBase64(image);
const byteString = atob(base64Bitmap);
const mimeString = "image/png";
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
const uuid = crypto.randomUUID(); // Generate a new UUID
const file = new File([ab], `${uuid}.png`, { type: mimeString });
const snapshotMediaToken = await api.uploadSceneSnapshot({
screenshot: file,
});
if (snapshotMediaToken.data === undefined) {
toast.error("Error: Unable to upload scene snapshot Please try again.");
return;
}
console.log("useSystemPrompt", useSystemPrompt);
console.log("Snapshot media token:", snapshotMediaToken.data);
const response = await api.enqueueImageGeneration({
disableSystemPrompt: !useSystemPrompt,
prompt: prompt,
snapshotMediaToken: snapshotMediaToken.data,
additionalImages: referenceImages.map((image) => image.mediaToken),
});
if (response.success === true) {
toast.success("Please wait while we process your image.");
if (response.data) {
addJobToken(response.data);
}
return;
} else {
toast.error("Failed to enqueue image generation. Please try again.");
}
};
const handleEnqueue = async () => {
if (!prompt.trim()) return;
@@ -189,71 +284,10 @@ export const PromptBox = () => {
referenceImages,
);
if (isDesktopApp) {
let image = getCanvasRenderBitmap();
if (image === undefined) {
return;
}
const base64Bitmap = await EncodeImageBitmapToBase64(image);
const generateResponse = await invoke("image_generation_command", {
image: base64Bitmap,
prompt: prompt,
});
await new Promise((resolve) => setTimeout(resolve, 2000));
}
if (!isDesktopApp) {
const api = new Api();
let image = getCanvasRenderBitmap();
if (image === undefined) {
console.error(
"Error: Unable to generate image. Please check the input and try again.",
);
return;
}
const base64Bitmap = await EncodeImageBitmapToBase64(image);
const byteString = atob(base64Bitmap);
const mimeString = "image/png";
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
const uuid = crypto.randomUUID(); // Generate a new UUID
const file = new File([ab], `${uuid}.png`, { type: mimeString });
const snapshotMediaToken = await api.uploadSceneSnapshot({
screenshot: file,
});
if (snapshotMediaToken.data === undefined) {
console.error("Error: Unable to upload scene snapshot.");
return;
}
console.log("Snapshot media token:", snapshotMediaToken.data);
const response = await api.enqueueImageGeneration({
prompt: prompt,
snapshotMediaToken: snapshotMediaToken.data,
additionalImages: referenceImages.map((image) => image.mediaToken),
});
if (response.success === true) {
console.error("Successfully enqueued image generation.");
if (response.data) {
console.log("Response:", response.data);
setJobToken(response.data);
}
return;
} else {
console.log("Image generation Not Successful.");
toast.error("Failed to generate image. Please try again.");
}
if (isDesktopApp == true) {
handleTauriEnqueue();
} else {
handleWebEnqueue();
}
} catch (error) {
console.error("Error during image generation:", error);
@@ -63,12 +63,11 @@ export const ToolbarMain = ({
buttonProps={buttonProps.SELECT}
tooltip="Select"
/>
{/* <ToolbarButton
<ToolbarButton
icon={faText}
buttonProps={buttonProps.ADD_TEXT}
tooltip="Add Text"
/> */}
/>
<Popover className="relative">
{({ close }) => (
<>
@@ -7,6 +7,8 @@ import {
GenerationLoadingState,
generationSignal,
} from "~/signals";
import { faChevronRight } from "@fortawesome/pro-solid-svg-icons";
import { Button } from "~/components/ui/Button";
// import { paperWrapperStyles } from "~/components/styles";
// import { faPlus, faQuestion } from "@fortawesome/pro-solid-svg-icons";
@@ -32,6 +34,10 @@ export const ToolbarTopLeft = () => {
const generationState = generationSignal.value;
const handleClick = () => {
window.location.href = "https://storyteller-3d.netlify.app/";
};
console.log(appMode.value);
return (
@@ -41,19 +47,30 @@ export const ToolbarTopLeft = () => {
)}
>
<img
src="/brand/mira-logo.png"
src="/brand/artcraft-logo.png"
alt="logo"
className="h-[30px] select-none pb-1"
className="h-[28px] select-none"
/>
<TabSelector
<Button
variant="secondary"
icon={faChevronRight}
iconClassName="text-xs"
iconFlip={true}
onClick={handleClick}
className="bg-transparent p-0 text-sm text-white/80 hover:bg-transparent hover:text-white hover:underline hover:underline-offset-2"
>
Go to 3D Stage Editor
</Button>
{/* <TabSelector
tabs={tabs}
activeTab={activeTab.value}
onTabChange={handleTabChange}
disabled={
generationState.loadingState === GenerationLoadingState.GENERATING
}
/>
/> */}
{/* <ToolbarButton icon={faPlus}>
<span className="font-semibold">New Board</span>
@@ -1,15 +1,32 @@
import { faGear } from "@fortawesome/pro-solid-svg-icons";
import { twMerge } from "tailwind-merge";
import { Activity } from "~/components/ui/Activity/Activity";
// import { paperWrapperStyles } from "~/components/styles";
// import { faPlus, faQuestion } from "@fortawesome/pro-solid-svg-icons";
// import { ToolbarButton } from "~/components/features/ToolbarButton";
import { Button } from "@storyteller/ui-button";
import { SettingsModal } from "@storyteller/ui-settings-modal";
import { useState } from "react";
export const ToolbarTopRight = () => {
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
return (
<div
className={twMerge("relative z-50 flex h-fit w-fit items-center gap-2")}
>
<Activity />
</div>
<>
<div
className={twMerge("relative z-50 flex h-fit w-fit items-center gap-2")}
>
<Button
icon={faGear}
className="h-[42px] w-[42px] bg-[#5F5F68]/60 backdrop-blur-lg hover:bg-[#5F5F68]/90"
onClick={() => setIsSettingsModalOpen(true)}
/>
<Activity />
</div>
<SettingsModal
isOpen={isSettingsModalOpen}
onClose={() => setIsSettingsModalOpen(false)}
/>
</>
);
};

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