Compare commits

...
Author SHA1 Message Date
bflatastic aebf78f765 pose mode button on character select 2025-04-17 13:01:18 +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
132 changed files with 8040 additions and 4425 deletions
Generated
+31
View File
@@ -5035,6 +5035,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 +6136,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 +7864,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 +10626,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"
+3 -1
View File
@@ -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),
@@ -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)
}
@@ -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));
}
@@ -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;
@@ -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"
]
}
+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]]
@@ -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,
@@ -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;
@@ -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);
@@ -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>
)}
</>
);
};
@@ -17,6 +17,7 @@ import { EditModeRootComponent } from "~/EditModeRootComponent/EditModeRootCompo
import { useRef } from "react";
import { EngineType } from "~/KonvaApp";
import { GenerationEngine } from "~/KonvaApp/GenerationEngine";
import { GalleryRootComponent } from "~/GalleryRootComponent";
export const Main = withProtectionRoute(() => {
// This is a hook that will log the number of times the component has rerendered
@@ -59,8 +60,10 @@ export const Main = withProtectionRoute(() => {
}
return (
<div className="fixed grid h-full w-full grid-cols-12 grid-rows-12">
{childView}
<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">
@@ -74,6 +77,8 @@ export const Main = withProtectionRoute(() => {
</div>
</div>
</div>
{appModeValue === "gallery" &&
(<GalleryRootComponent className="fixed h-full w-full z-1 bg-black" />)}
</div>
);
});
@@ -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.");
}
},
});
@@ -206,7 +208,7 @@ export const PromptBox = () => {
const api = new Api();
let image = getCanvasRenderBitmap();
if (image === undefined) {
console.error(
toast.error(
"Error: Unable to generate image. Please check the input and try again.",
);
return;
@@ -229,30 +231,28 @@ export const PromptBox = () => {
});
if (snapshotMediaToken.data === undefined) {
console.error("Error: Unable to upload scene snapshot.");
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) {
console.error("Successfully enqueued image generation.");
toast.success("Please wait while we process your image.");
if (response.data) {
console.log("Response:", response.data);
setJobToken(response.data);
addJobToken(response.data);
}
return;
} else {
console.log("Image generation Not Successful.");
toast.error("Failed to generate image. Please try again.");
toast.error("Failed to enqueue image generation. Please try again.");
}
}
} catch (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 }) => (
<>
@@ -1,4 +1,5 @@
import { twMerge } from "tailwind-merge";
import { Login } from "~/app/pages/login";
import { Activity } from "~/components/ui/Activity/Activity";
// import { paperWrapperStyles } from "~/components/styles";
// import { faPlus, faQuestion } from "@fortawesome/pro-solid-svg-icons";
@@ -0,0 +1,73 @@
import { useRef, useCallback, useContext, ReactNode, useEffect } from "react";
import { EngineType } from "~/KonvaApp";
interface UndoRedoProps {
children: ReactNode;
engine: EngineType | null;
}
/**
* A transparent component that provides undo/redo functionality
* with keyboard shortcuts for macOS (Command+Z, Command+Shift+Z)
*/
export const UndoRedo = ({
engine,
children,
}: UndoRedoProps) => {
console.log("UndoRedo");
// We don't need a separate ref since we want to track the prop changes
// Create the event handler with useCallback
const handleKeyDown = useCallback((e: KeyboardEvent) => {
if (!engine) {
return;
}
// Don't capture events when user is editing text
const target = e.target as HTMLElement;
const isEditingText = target.tagName === 'INPUT' ||
target.tagName === 'TEXTAREA' ||
target.isContentEditable;
// Command+Z for Undo
// Ctrl+Shift+Z for Redo (Windows/Linux alternative)
// Windows/Linux: Ctrl+Shift+Z for Redo
if (!navigator.userAgent.includes('Mac') && e.ctrlKey && e.shiftKey && e.key === 'z') {
console.log("Ctrl+Shift+Z pressed: Redo (Windows/Linux)");
e.preventDefault();
engine.undoStackManager.redo();
}
// Delete key handling - only if not editing text
if (!isEditingText && (e.key === 'Delete' || e.key === 'Backspace')) {
console.log("Delete/Backspace pressed: Delete selected nodes");
e.preventDefault();
engine.commandManager.deleteNodes();
}
// macOS: Command+Z for Undo
if (navigator.userAgent.includes('Mac') && e.metaKey && e.key === 'z' && !e.shiftKey) {
console.log("Command+Z pressed: Undo");
e.preventDefault();
engine.undoStackManager.undo();
}
// macOS: Command+Shift+Z for Redo
if (navigator.userAgent.includes('Mac') && e.metaKey && e.key === 'z' && e.shiftKey) {
console.log("Command+Shift+Z pressed: Redo");
e.preventDefault();
engine.undoStackManager.redo();
engine.realTimeDrawEngine.render();
}
}, [engine]);
// Actually set up the event listeners using useEffect
useEffect(() => {
window.addEventListener('keydown', handleKeyDown);
// Cleanup function to remove the event listener
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [handleKeyDown]); // Depend on the memoized handler
return children;
};
@@ -6,6 +6,8 @@ import { CompletedCard } from "./CompletedCard";
import { InProgressCard } from "./InProgressCard";
import { useJobContext } from "~/components/JobContext";
import { Api } from "~/KonvaApp/Api";
import { toast } from "sonner";
// Dummy data types
interface CompletedItem {
token: string;
@@ -65,90 +67,72 @@ const dummyCompletedItems: CompletedItem[] = [
// ];
export function Activity() {
const { jobToken, setJobToken } = useJobContext();
const { jobTokens, addJobToken, removeJobToken, clearJobTokens } =
useJobContext();
const [completedItems, setCompletedItems] = useState<CompletedItem[]>([]);
const [jobs, setJobs] = useState<ActiveJob[]>([]);
const [loading, setLoading] = useState(true);
// Dummy polling function
useEffect(() => {
const pollData = async () => {
if (jobToken) {
console.log("Current Job Token:", jobToken);
if (jobTokens.length > 0) {
console.log("Current Job Tokens:", jobTokens);
const api = new Api();
const job = await api.pollJobSession(jobToken);
console.log("Job response:", job);
if (job.success && job.data) {
const { status, progress_percentage } = job.data.status;
const { job_token } = job.data;
// Create a map to store the latest job data
const updatedJobs = new Map<string, ActiveJob>();
console.log("Job status:", status);
console.log("Progress percentage:", progress_percentage);
console.log("Job token:", job_token);
for (const jobToken of jobTokens) {
const job = await api.pollJobSession(jobToken);
// Update jobs with real job data
if (
status.toLowerCase() === "completed" ||
status.toLowerCase() === "failed"
) {
// If job is completed or failed, remove it from active jobs
setJobs((prevJobs) =>
prevJobs.filter((j) => j.job_token !== job_token),
);
if (job.success && job.data) {
const { status, progress_percentage } = job.data.status;
const { job_token } = job.data;
// Add to completed items if it was successful
if (
status.toLowerCase() === "completed" &&
job.data.result.generated_images
status.toLowerCase() === "complete_success" ||
status.toLowerCase() === "failed"
) {
const newCompletedItem: CompletedItem = {
id: job_token,
title: job.data.request.maybe_model_title || "Image Generation",
timestamp: new Date().toISOString(),
type: "image",
imageUrl: job.data.result.generated_images[0] || "",
};
// If job is complete_success or failed, we'll remove it later
removeJobToken(job_token);
setCompletedItems((prev) => [newCompletedItem, ...prev]);
// Add to complete_success items if it was successful
if (status.toLowerCase() === "complete_success") {
const newCompletedItem: CompletedItem = {
token: job_token,
maybe_title:
job.data.request.maybe_model_title || "Image Generation",
public_bucket_path: job.data.result.image_url || "",
updated_at: new Date().toISOString(),
};
toast.success("Done Processing Image");
setCompletedItems((prev) => [newCompletedItem, ...prev]);
}
} else {
// Store the job in our map
const updatedJob: ActiveJob = {
job_token,
request: {
maybe_model_title:
job.data.request.maybe_model_title || "Image Generation",
},
status: {
status: status.toUpperCase(),
progress_percentage: progress_percentage || 0,
},
};
updatedJobs.set(job_token, updatedJob);
}
} else {
// Update or add the job to the active jobs list
const updatedJob: ActiveJob = {
job_token,
request: {
maybe_model_title:
job.data.request.maybe_model_title || "Image Generation",
},
status: {
status: status.toUpperCase(),
progress_percentage: progress_percentage || 0,
},
};
setJobs((prevJobs) => {
const existingJobIndex = prevJobs.findIndex(
(j) => j.job_token === job_token,
);
if (existingJobIndex >= 0) {
// Update existing job
const newJobs = [...prevJobs];
newJobs[existingJobIndex] = updatedJob;
return newJobs;
} else {
// Add new job
return [updatedJob, ...prevJobs];
}
});
console.error("Failed to fetch job status:", job.errorMessage);
}
} else {
console.error("Failed to fetch job status:", job.errorMessage);
}
} else {
console.log("No Job Token available.");
}
if (jobs.length === 0) {
// Update jobs state with the deduplicated jobs
setJobs(Array.from(updatedJobs.values()));
}
if (jobTokens.length === 0) {
setJobs([]);
setLoading(false);
}
};
@@ -163,7 +147,7 @@ export function Activity() {
}, 5000);
return () => clearInterval(interval);
}, [jobToken]);
}, [jobTokens, removeJobToken]);
return (
<PopoverMenu
@@ -17,8 +17,10 @@ interface Props {
export function CompletedCard({ job }: Props) {
const [loadError, setLoadError] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const imageUrl = job.public_bucket_path + "-thumb.gif";
const imageUrl = job.public_bucket_path;
const fullImageUrl = job.public_bucket_path;
console.log("I am hjere");
console.log(job);
return (
<>
@@ -87,19 +87,22 @@ export class EngineApi extends ApiManager {
}
public async enqueueImageGeneration({
disableSystemPrompt,
prompt,
snapshotMediaToken,
additionalImages,
}: {
disableSystemPrompt: boolean;
prompt: string;
snapshotMediaToken: string;
additionalImages?: string[];
}): Promise<ApiResponse<string>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/image_studio/prompt`;
// for now ...
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,
@@ -108,6 +111,7 @@ export class EngineApi extends ApiManager {
const postResponse = await this.post<
{
uuid_idempotency_token: string;
disable_system_prompt: boolean;
prompt: string;
snapshot_media_token: string;
additional_images?: string[];
@@ -40,7 +40,7 @@ export class JobsApi extends ApiManager {
public ListRecentJobs(): Promise<ApiResponse<Job[]>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/jobs/session`;
return this.get<{
success: boolean;
jobs: Job[];
@@ -45,7 +45,7 @@ export const Button = ({
);
const className = twMerge(
"text-sm font-medium rounded-lg px-3 py-2 border border-[#363636] shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 transition-all duration-150 flex gap-2 items-center justify-center",
"text-sm font-medium rounded-lg px-3 py-2 border border-transparent shadow-sm focus-visible:outline focus-visible:outline-0 focus-visible:outline-offset-0 transition-all duration-150 flex gap-2 items-center justify-center",
getVariantClassNames(propsVariant),
propsClassName,
disabledClass,
@@ -15,7 +15,7 @@ export const CloseButton = ({
}: CloseButtonProps) => {
const sizeClasses = {
sm: "h-5 w-5 text-sm",
md: "h-7 w-7 text-lg",
md: "h-7 w-7 text-md",
lg: "h-9 w-9 text-xl",
};
@@ -0,0 +1,122 @@
import { CloseButton, Button } from "~/components";
import { Transition } from "@headlessui/react";
import { createPortal } from "react-dom";
import dayjs from "dayjs";
import { faDownToLine } from "@fortawesome/pro-solid-svg-icons";
interface LightboxModalProps {
isOpen: boolean;
onClose: () => void;
imageUrl?: string | null;
imageAlt?: string;
onImageError?: () => void;
title?: string;
createdAt?: string;
additionalInfo?: React.ReactNode;
downloadUrl?: string;
}
export function LightboxModal({
isOpen,
onClose,
imageUrl,
imageAlt = "",
onImageError,
title,
createdAt,
additionalInfo,
downloadUrl,
}: LightboxModalProps) {
return createPortal(
<Transition appear show={isOpen}>
<div className="fixed inset-0 z-[100]">
<Transition.Child
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div
className="fixed inset-0 cursor-pointer bg-black/60"
onClick={onClose}
/>
</Transition.Child>
<div
className="fixed inset-0 flex items-center justify-center p-4"
onClick={onClose}
>
<Transition.Child
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<div
className="relative h-[90vh] w-[80vw] rounded-xl bg-[#2C2C2C]"
onClick={(e) => e.stopPropagation()}
>
<CloseButton
onClick={onClose}
className="absolute right-4 top-4 z-10"
/>
<div className="grid h-full grid-cols-3 gap-6">
<div className="col-span-2 flex h-full items-center justify-center overflow-hidden rounded-l-xl bg-black/40">
{!imageUrl ? (
<div className="flex h-full w-full items-center justify-center bg-gray-800">
<span className="text-white/60">Image not available</span>
</div>
) : (
<img
src={imageUrl}
alt={imageAlt}
className="h-full w-full object-contain"
onError={onImageError}
crossOrigin="anonymous"
referrerPolicy="no-referrer"
/>
)}
</div>
<div className="flex h-full flex-col py-5 pe-5">
<div className="flex-1 space-y-4">
<div className="text-xl font-medium">
{title || "Image Generation"}
</div>
{createdAt && (
<div className="text-sm text-white/60">
Created:{" "}
{dayjs(createdAt).format("MMM D, YYYY HH:mm:ss")}
</div>
)}
{additionalInfo}
</div>
{downloadUrl && (
<div className="flex justify-end">
<a
href={downloadUrl}
download
onClick={(e) => e.stopPropagation()}
className="no-underline"
>
<Button
icon={faDownToLine}
onClick={(e) => e.stopPropagation()}
>
Download
</Button>
</a>
</div>
)}
</div>
</div>
</div>
</Transition.Child>
</div>
</div>
</Transition>,
document.body,
);
}
@@ -137,7 +137,7 @@ export const Tooltip = ({
transitionTimingFunction: "ease-out",
}}
className={twMerge(
"pointer-events-none absolute z-10 w-max rounded-lg bg-[#5F5F68] px-2.5 py-1.5 text-sm font-medium text-white shadow-xl",
"pointer-events-none absolute z-10 w-max rounded-lg bg-[#5F5F68] px-2.5 py-1.5 text-[13px] font-medium text-white shadow-xl",
className ? className : "",
)}
>
@@ -27,16 +27,16 @@ export const TopBar = ({ pageName }: Props) => {
return (
<>
<header className="fixed left-0 top-0 z-30 w-full border-b border-white/5 bg-ui-background">
<header className="fixed left-0 top-0 z-[60] w-full border-b border-white/5 bg-ui-background">
<nav
className="mx-auto grid h-[56px] w-screen grid-cols-3 items-center justify-between px-3"
aria-label="Global"
>
<div className="flex gap-4">
<a href="/" className="">
<a href="/">
<span className="sr-only">ArtCraft</span>
<img
className="h-[30px] w-auto"
className="h-[28px] w-auto"
src="/resources/images/artcraft-logo-3.png"
alt="Logo ArtCraft"
/>
+1
View File
@@ -33,4 +33,5 @@ export interface JobStatusResponse {
maybe_first_started_at: string;
requires_keepalive: boolean;
status: JobStatus;
progress_percentage: number;
}
@@ -17,7 +17,11 @@ import { SceneManager, SceneObject } from "./scene_manager_api";
import { FreeCam } from "./free_cam";
import Stats from "three/examples/jsm/libs/stats.module.js";
import { FKHelper } from "./KinHelpers/FKHelper";
import { selectedMode } from "../signals/selectedMode";
import {
selectedMode,
poseMode,
showPoseControls,
} from "../signals/selectedMode";
export enum KinMode {
FK,
@@ -122,6 +126,10 @@ export class MouseControls {
scene: this.scene,
onDragChange: this.onFKControlsDragging.bind(this),
});
// Expose handlePoseModeChange to window
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).__mouseControls = this;
}
onFKControlsDragging(dragging: boolean) {
@@ -185,7 +193,11 @@ export class MouseControls {
this.setSelected(this.selected);
this.publishSelect();
// this.editor.update_properties()
if (currentObject.userData.isCharacter) {
showPoseControls.value = true;
}
// Normal selection behavior
if (currentObject.userData["locked"] !== true && this.control) {
this.scene.add(this.control);
this.control.attach(currentObject);
@@ -235,6 +247,9 @@ export class MouseControls {
this.fkHelper.clear();
this.kinMode = KinMode.NONE;
this.reattachTransformControls();
if (poseMode.value === "pose") {
poseMode.value = "select";
}
console.log("FK mode off");
return;
}
@@ -255,6 +270,9 @@ export class MouseControls {
this.hideTransformControls();
this.kinMode = KinMode.FK;
this.fkHelper.setTarget(this.selected[0]);
if (poseMode.value === "select") {
poseMode.value = "pose";
}
console.log("FK mode on");
return;
}
@@ -305,30 +323,20 @@ export class MouseControls {
if ((event.ctrlKey || event.metaKey) && !this.isProcessing) {
if (event.key === "Z" || event.key === "z") {
if (event.shiftKey) {
// redo
this.isProcessing = true;
await this.sceneManager?.redo();
this.isProcessing = false;
} else {
// undo
this.isProcessing = true;
await this.sceneManager?.undo();
this.isProcessing = false;
}
return;
// redo
this.isProcessing = true;
await this.sceneManager?.redo();
this.isProcessing = false;
} else if (event.key === "c") {
// redo
this.isProcessing = true;
await this.sceneManager?.copy();
this.isProcessing = false;
return;
} else if (event.key === "v") {
// redo
this.isProcessing = true;
await this.sceneManager?.paste();
this.isProcessing = false;
return;
} else if (event.key === "0") {
// Stats Menu
this.enable_stats();
@@ -454,7 +462,8 @@ export class MouseControls {
this.selected = [];
this.setSelected(this.selected);
this.removeTransformControls();
//hideObjectPanel();
hideObjectPanel();
showPoseControls.value = false;
}
if (this.sceneManager) {
@@ -7,6 +7,7 @@ import { ControlPanelSceneObject } from "./comps/ControlPanelSceneObject";
import { PreviewEngineCamera } from "./comps/PreviewEngineCamera";
import { PreviewFrameImage } from "./comps/PreviewFrameImage";
import { pageHeight, pageWidth } from "~/signals";
import { PoseModeSelector } from "./comps/PoseModeSelector";
import {
timelineHeight,
@@ -24,7 +25,6 @@ import { Outliner } from "./comps/Outliner";
import { CameraAspectRatio } from "./enums";
import { PromptBox } from "./comps/PromptBox";
import { OnboardingHelper } from "./comps/OnboardingHelper";
import { focalLengthDragging } from "./signals/camera";
import { FocalLengthDisplay } from "./comps/FocalLengthDisplay/FocalLengthDisplay";
export const PageEditor = () => {
@@ -106,6 +106,9 @@ export const PageEditor = () => {
{/* Focal Length Display */}
<FocalLengthDisplay />
{/* Pose Mode Selector */}
<PoseModeSelector />
{/* Top controls */}
<div
className="absolute left-0 top-0 w-full"
@@ -16,6 +16,8 @@ import * as gpu from "detect-gpu";
import { UsersApi } from "~/Classes/ApiManager";
import { PageEnigmaComponent } from "~/pages/PageEnigma/PageEnigmaComponent";
import { TurnOnGpu } from "~/pages/PageEnigma/TurnOnGpu";
import { PrecisionSelector } from "./comps/PrecisionSelector/PrecisionSelector";
import { precisionSelectedValue, precisionSelectorMenuCoords, precisionSelectorValues, showPrecisionSelector } from "./signals/precisionSelectorMenu";
export const PageEnigma = ({ sceneToken }: { sceneToken?: string }) => {
useSignals();
@@ -71,7 +73,7 @@ export const PageEnigma = ({ sceneToken }: { sceneToken?: string }) => {
case "apple gpu (Apple GPU)":
// TODO(bt,2025-04-08): We may want to disable this heuristic.
// We're getting lack of hardware acceleration using Tauri on Mac and Linux.
isValid = true;
isValid = true;
break;
default:
break;
@@ -93,6 +95,12 @@ export const PageEnigma = ({ sceneToken }: { sceneToken?: string }) => {
<EngineProvider sceneToken={sceneToken}>
<PageEnigmaComponent />
<DragComponent />
<PrecisionSelector
showSignal={showPrecisionSelector}
coordSignal={precisionSelectorMenuCoords}
valuesSignal={precisionSelectorValues}
selectedValueSignal={precisionSelectedValue}
/>
<GenerateModals />
<ErrorDialog />
<Wizard />
@@ -22,6 +22,7 @@ import { sanitize } from "./utils/sanitize";
import { objectMismatch } from "~/pages/PageEnigma/comps/ControlPanelSceneObject/utils/objectMismatch";
import { XYZ } from "~/pages/PageEnigma/datastructures/common";
import { pageHeight } from "~/signals";
import { DraggablePrecisionMutator } from "./DraggablePrecisionMutator";
// TODO this will be useful later to fix the bug on leading zeros
// const formatNumber = (input: string): number => {
@@ -147,6 +148,15 @@ export const ControlPanelSceneObject = () => {
setLocalRotation(xyz);
};
const handleUniformScaleChange = (scale: number) => {
const updatedScaleValues: Record<string, string> = {};
updatedScaleValues.x = (parseFloat(localScale.x) + scale).toString();
updatedScaleValues.y = (parseFloat(localScale.y) + scale).toString();
updatedScaleValues.z = (parseFloat(localScale.z) + scale).toString();
handleScaleChange(updatedScaleValues);
}
const handleScaleChange = (xyz: Record<string, string>) => {
if (isInvalid(xyz)) {
setLocalScale(xyz);
@@ -255,7 +265,9 @@ export const ControlPanelSceneObject = () => {
</div>
<div className="mb-1 flex flex-col gap-1">
<H5>Scale</H5>
<DraggablePrecisionMutator onChange={handleUniformScaleChange}>
<h5>Scale</h5>
</DraggablePrecisionMutator>
<InputVector
x={localScale.x.toString()}
y={localScale.y.toString()}
@@ -0,0 +1,101 @@
import { useCallback, useRef } from "react";
import { twMerge } from "tailwind-merge";
import { precisionSelectedValue, precisionSelectorMenuCoords, precisionSelectorValues, showPrecisionSelector } from "../../signals/precisionSelectorMenu";
const DEFAULT_PRECISIONS = [
10,
1,
0.1,
0.01,
0.001
]
export const DraggablePrecisionMutator = ({
precisions = DEFAULT_PRECISIONS,
className = "",
initialValue = 0,
onChange = () => { },
onCommit = () => { },
children
}: {
precisions?: number[],
className?: string,
initialValue?: number,
onChange?: (value: number) => void,
onCommit?: (value: number) => void,
children?: React.ReactNode
}) => {
const classes = twMerge(["cursor-ew-resize", className]);
const divRef = useRef<HTMLDivElement>(null);
const isDraggingRef = useRef(false);
const dragStartCoordsRef = useRef<{ x: number, y: number } | null>(null);
const localAdjustedValueRef = useRef(initialValue);
const handleMouseMove = useCallback((event: MouseEvent) => {
// Ignore mouse movement if we're not even dragging
// or if the menu is still open (we're selecting the precision)
if (!isDraggingRef.current || showPrecisionSelector.value) {
return;
}
event.stopPropagation();
// Record the initial drag coordinates if they haven't been set yet
if (!dragStartCoordsRef.current) {
dragStartCoordsRef.current = {
x: event.clientX,
y: event.clientY
};
return;
}
// Check how far the mouse has moved, and calculate the updated change
const distX = event.x - dragStartCoordsRef.current.x;
const scaledDistX = distX / 10;
const scaledPrecision = Math.round(scaledDistX) * precisionSelectedValue.value;
localAdjustedValueRef.current = initialValue + scaledPrecision;
// Update the listeners
onChange(localAdjustedValueRef.current);
}, [initialValue, onChange]);
const handleMouseUp = useCallback((event: MouseEvent) => {
event.stopPropagation();
showPrecisionSelector.value = false;
isDraggingRef.current = false;
dragStartCoordsRef.current = null;
// clean up the mouse listeners
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
// Call back the commit listener
onCommit(localAdjustedValueRef.current);
}, [handleMouseMove, onCommit]);
const handleMouseDown = useCallback((event: React.MouseEvent) => {
if (event.button !== 0) {
return;
}
event.preventDefault();
event.stopPropagation();
precisionSelectorMenuCoords.value = {
x: event.clientX,
y: event.clientY
};
precisionSelectorValues.value = precisions;
showPrecisionSelector.value = true;
isDraggingRef.current = true;
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
}, [handleMouseMove, handleMouseUp, precisions]);
return (
<div className={classes} ref={divRef} onMouseDown={handleMouseDown}>
{children}
</div>
)
}
@@ -15,7 +15,7 @@ export const FocalLengthDisplay = () => {
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="absolute left-1/2 top-16 -translate-x-1/2 transform">
<div className="absolute left-1/2 top-16 z-10 -translate-x-1/2 transform">
<div className="glass rounded-xl px-5 py-2.5 text-center text-2xl font-bold text-white">
{focalLengthDragging.value.focalLength}mm
</div>
@@ -5,10 +5,163 @@ import { PopoverMenu } from "~/components/reusable/Popover/Popover";
import { CompletedCard } from "./CompletedCard";
import { InProgressCard } from "./InProgressCard";
import { H3, P, Tooltip } from "~/components";
import { activeImageGenerationJobs, userMovies } from "~/signals";
import { activeImageGenerationJobs, toasts, userMovies } from "~/signals";
import { useEffect, useState } from "react";
import { JobsApi } from "~/Classes/ApiManager/JobsApi";
import { toast } from "sonner";
import { ActiveJob } from "~/pages/PageEnigma/models";
// TODO ensure we de-dupe all this extra code.
interface CompletedItem {
token: string;
maybe_title: string;
public_bucket_path: string;
updated_at: string;
}
// {
// "job_token": "jinf_421nnr6rj8wy2sj0s11v9fns32s",
// "request": {
// "inference_category": "image_generation",
// "maybe_model_type": "image_gen_api",
// "maybe_model_token": null,
// "maybe_model_title": "Image Generation",
// "maybe_raw_inference_text": null,
// "maybe_style_name": null,
// "maybe_live_portrait_details": null,
// "maybe_lipsync_details": null
// },
// "status": {
// "status": "complete_success",
// "maybe_extra_status_description": null,
// "maybe_assigned_worker": "inference-job-4o-image-848c55d5dd-555kn",
// "maybe_assigned_cluster": "storyteller-eks-1",
// "maybe_first_started_at": "2025-04-15T04:41:01Z",
// "maybe_current_execution_duration_seconds": null,
// "attempt_count": 1,
// "requires_keepalive": false,
// "maybe_failure_category": null,
// "progress_percentage": 100
// },
// "maybe_result": {
// "entity_type": "media_file",
// "entity_token": "m_ytrjp1fcd5tb43ebcs3ewhq6y05tfb",
// "maybe_public_bucket_media_path": "/media/p/t/7/y/g/pt7yg2xfv4ynemssfv1mwb7yxe26e2nb/image_pt7yg2xfv4ynemssfv1mwb7yxe26e2nb.png",
// "media_links": {
// "cdn_url": "https://cdn-2.fakeyou.com/media/p/t/7/y/g/pt7yg2xfv4ynemssfv1mwb7yxe26e2nb/image_pt7yg2xfv4ynemssfv1mwb7yxe26e2nb.png",
// "maybe_thumbnail_template": "https://cdn-2.fakeyou.com/cdn-cgi/image/width={WIDTH},quality=95/media/p/t/7/y/g/pt7yg2xfv4ynemssfv1mwb7yxe26e2nb/image_pt7yg2xfv4ynemssfv1mwb7yxe26e2nb.png",
// "maybe_video_previews": null
// },
// "maybe_successfully_completed_at": "2025-04-15T04:42:14Z"
// },
// "created_at": "2025-04-15T04:41:01Z",
// "updated_at": "2025-04-15T04:42:14Z"
// }
export function Activity() {
useSignals();
const [completedItems, setCompletedItems] = useState<CompletedItem[]>([]);
const [loading, setLoading] = useState(true);
const [jobs, setJobs] = useState<ActiveJob[]>([]);
useEffect(() => {
const pollData = async () => {
const fetchJobs = async () => {
try {
const jobsApi = new JobsApi();
const jobsResponse = await jobsApi.ListRecentJobs();
if (jobsResponse.success && jobsResponse.data) {
// Process active jobs
const activeJobs = jobsResponse.data
.filter((job) => {
const status = job.status?.status?.toLowerCase();
return (
status && status !== "complete_success" && status !== "failed"
);
})
.map((job) => ({
job_token: job.job_token,
request: {
maybe_model_title:
job.request?.maybe_model_title || "Image Generation",
},
status: {
status: job.status?.status?.toUpperCase() || "",
progress_percentage: job.status?.progress_percentage || 0,
},
}));
// Process completed items
// Get completed items and filter out any that are already in our list
const successfulJobs = jobsResponse.data.filter(
(job) => job.status?.status?.toLowerCase() === "complete_success",
);
// Create a set of existing tokens for faster lookup
const existingTokens = new Set(
completedItems.map((item) => item.token),
);
// Filter out jobs we already have in our list
const newCompletedItems = successfulJobs
.filter((job) => !existingTokens.has(job.job_token))
.map((job) => ({
token: job.job_token,
maybe_title:
job.request?.maybe_model_title || "Image Generation",
public_bucket_path:
job.maybe_result?.maybe_public_bucket_media_path || "",
updated_at: job.updated_at || new Date().toISOString(),
}));
// Show toast notification for newly completed items
if (newCompletedItems.length > 0) {
const count = newCompletedItems.length;
const message =
count === 1
? `${newCompletedItems[0].maybe_title} completed successfully`
: `${count} images completed successfully`;
toast.success(message);
}
// Update completed items with a maximum limit of 50 items
setCompletedItems((prev) => {
const combined = [...newCompletedItems, ...prev];
// Remove duplicates based on token
const uniqueItems = Array.from(
new Map(combined.map((item) => [item.token, item])).values(),
);
// Sort by updated_at in descending order and limit to 50 items
return uniqueItems
.sort(
(a, b) =>
new Date(b.updated_at).getTime() -
new Date(a.updated_at).getTime(),
)
.slice(0, 50);
});
setJobs(activeJobs as ActiveJob[]);
setLoading(false);
}
} catch (error) {
toast.error("Error Fetching Recent Jobs");
console.error("Failed to fetch recent jobs:", error);
}
};
await fetchJobs();
};
// Initial load
// Initial call to pollData
pollData();
// Set up polling interval (every 5 seconds)
const interval = setInterval(async () => {
await pollData();
}, 5000);
return () => clearInterval(interval);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Tooltip content="Activity" position="bottom" closeOnClick={true}>
@@ -20,24 +173,26 @@ export function Activity() {
align="end"
triggerIcon={
<div>
<FontAwesomeIcon icon={faBell} />
{activeImageGenerationJobs.value &&
activeImageGenerationJobs.value.length > 0 && (
<div className="bg-blue-500 absolute -right-1 -top-1 h-3 w-3 animate-pulse rounded-full" />
)}
{jobs && jobs.length > 0 ? (
<FontAwesomeIcon icon={faSpinnerThird} className="animate-spin" />
) : (
<FontAwesomeIcon icon={faBell} />
)}
{jobs && jobs.length > 0 && (
<div className="bg-blue-500 absolute -right-1 -top-1 h-3 w-3 animate-pulse rounded-full" />
)}
</div>
}
>
<div className="max-h-[480px] overflow-y-auto">
{/* In progress */}
{activeImageGenerationJobs.value &&
activeImageGenerationJobs.value.length > 0 && (
<div>
{activeImageGenerationJobs.value.map((movieJob) => (
<InProgressCard key={movieJob.job_token} movie={movieJob} />
))}
</div>
)}
{jobs && jobs.length > 0 && (
<div>
{jobs.map((job) => (
<InProgressCard key={job.job_token} job={job} />
))}
</div>
)}
{!userMovies.value && !activeImageGenerationJobs.value ? (
<div className="flex h-48 w-full flex-col justify-center gap-4 p-4 text-center align-middle">
@@ -62,8 +217,8 @@ export function Activity() {
<div>
{/* Completed */}
<div className="flex flex-col">
{userMovies.value?.map((movie) => (
<CompletedCard key={movie.token} movie={movie} />
{completedItems.map((item) => (
<CompletedCard key={item.token} job={item} />
))}
</div>
</div>
@@ -1,91 +1,84 @@
import { MediaInfo } from "~/pages/PageEnigma/models/movies";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowDownToLine } from "@fortawesome/pro-solid-svg-icons";
import { useRef, useState } from "react";
import { BucketConfig } from "~/api/BucketConfig";
import { useState } from "react";
import dayjs from "dayjs";
import environmentVariables from "~/Classes/EnvironmentVariables";
import { useSignals } from "@preact/signals-react/runtime";
import { downloadFile } from "~/pages/PageEnigma/comps/GenerateModals/utils/downloadFile";
import { styleList } from "~/pages/PageEnigma/styleList";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faDownToLine } from "@fortawesome/pro-solid-svg-icons";
import { LightboxModal } from "~/components/reusable/LightboxModal";
import { GetCdnOrigin } from "~/api/GetCdnOrigin";
export function getStyleName(typeInput: string) {
const foundStyle = styleList.find((style) => {
return style.type === typeInput;
});
return foundStyle ? foundStyle.label : "Unknown Style";
}
interface Props {
movie: MediaInfo;
setMovieId?: (id: string) => void;
job: {
token: string;
maybe_title: string;
updated_at: string;
public_bucket_path: string;
maybe_style_name?: string;
};
}
export function CompletedCard({ movie, setMovieId }: Props) {
useSignals();
const bucketConfig = useRef<BucketConfig>(new BucketConfig());
export function CompletedCard({ job }: Props) {
const [loadError, setLoadError] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const cdnOrigin = GetCdnOrigin();
const downloadLink = `https://${cdnOrigin}${movie.public_bucket_path}`;
const mediaPath = job?.public_bucket_path || "";
const downloadLink = `${cdnOrigin}${mediaPath}`;
const imageUrl = downloadLink;
const fullImageUrl = downloadLink;
const imageUrl = bucketConfig.current.getCdnUrl(
movie.public_bucket_path + "-thumb.gif",
360,
20,
);
const styleName =
movie.maybe_style_name && getStyleName(movie.maybe_style_name);
const handleClick = () => {
setMovieId?.(movie.token);
};
if (!job) return null;
return (
<div
className="flex w-full items-center justify-between rounded-lg p-2 text-start transition-all duration-150 hover:cursor-pointer hover:bg-white/10"
onClick={handleClick}
>
<div className="flex gap-4">
<div className="flex aspect-square h-14 w-14 justify-center overflow-hidden rounded-lg border border-[#A9A9A9]/50 bg-black/60">
<img
src={
loadError ? "/resources/images/movie-placeholder.png" : imageUrl
}
className="h-full w-full object-cover"
alt={movie.maybe_title ?? "unknown"}
crossOrigin="anonymous"
onError={() => setLoadError(true)}
loading="lazy"
/>
</div>
<div className="flex flex-col justify-center gap-1">
<div className="font-medium">{movie.maybe_title || "Untitled"}</div>
<div>
{/* {styleName && (
<div className="text-sm text-white/60">{styleName}</div>
)} */}
<>
<div
className="flex w-full items-center justify-between rounded-lg p-2 text-start transition-all duration-150 hover:cursor-pointer hover:bg-white/10"
onClick={() => setIsModalOpen(true)}
>
<div className="flex gap-4">
<div className="flex aspect-square h-14 w-14 justify-center overflow-hidden rounded-lg border border-[#A9A9A9]/50 bg-black/60">
<img
src={
loadError ? "/resources/images/movie-placeholder.png" : imageUrl
}
className="h-full w-full object-cover"
alt={job.maybe_title ?? "unknown"}
crossOrigin="anonymous"
onError={() => setLoadError(true)}
loading="lazy"
/>
</div>
<div className="flex flex-col justify-center gap-1">
<div className="font-medium">{job.maybe_title || "Untitled"}</div>
<div className="text-sm text-white/60">
{dayjs(movie.updated_at).format("MMM D, YYYY HH:mm:ss")}
{dayjs(job.updated_at).format("MMM D, YYYY HH:mm:ss")}
</div>
</div>
</div>
</div>
{/* <div className="pr-5">
<button
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
const title =
movie.maybe_title !== null ? movie.maybe_title : "Untitled";
downloadFile({ url: downloadLink, title });
}}
className="text-[15px] font-medium text-white/50 transition-all duration-150 hover:text-white/100"
<a
href={downloadLink}
download
onClick={(e) => e.stopPropagation()}
className="flex h-9 w-9 items-center justify-center text-lg text-white/60 transition-all hover:text-white"
>
<FontAwesomeIcon icon={faArrowDownToLine} className="mr-2" />
Download
</button>
</div> */}
</div>
<FontAwesomeIcon icon={faDownToLine} />
</a>
</div>
<LightboxModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
imageUrl={fullImageUrl}
imageAlt={job.maybe_title ?? "unknown"}
onImageError={() => setLoadError(true)}
title={job.maybe_title}
createdAt={job.updated_at}
downloadUrl={downloadLink}
additionalInfo={
job.maybe_style_name && (
<div className="text-sm text-white/60">
Style: {job.maybe_style_name}
</div>
)
}
/>
</>
);
}
@@ -1,22 +1,15 @@
import { useCallback } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faClose,
faCircleExclamation,
faSpinnerThird,
} from "@fortawesome/pro-solid-svg-icons";
import { faClose, faSpinnerThird } from "@fortawesome/pro-solid-svg-icons";
import { ToastTypes } from "~/enums";
import { addToast } from "~/signals";
import { JobState } from "~/pages/PageEnigma/enums";
import { ActiveJob } from "~/pages/PageEnigma/models";
import { Tooltip } from "~/components";
import { JobsApi } from "~/Classes/ApiManager";
import { PollRecentJobs } from "~/hooks/useActiveJobs/utilities";
interface Props {
movie: ActiveJob;
job: ActiveJob;
}
function getPercent(status: JobState, percentage: number) {
@@ -33,16 +26,16 @@ function getPercent(status: JobState, percentage: number) {
}
}
export function InProgressCard({ movie }: Props) {
export function InProgressCard({ job }: Props) {
const completePercent = getPercent(
movie.status.status as JobState,
movie.status.progress_percentage,
job.status.status as JobState,
job.status.progress_percentage,
);
const deleteJob = useCallback(async (movieJob: ActiveJob) => {
const deleteJob = useCallback(async (job: ActiveJob) => {
const jobsApi = new JobsApi();
const response = await jobsApi.DeleteJobByToken(movieJob.job_token);
const response = await jobsApi.DeleteJobByToken(job.job_token);
if (response.success) {
PollRecentJobs();
addToast(ToastTypes.SUCCESS, "File successfully deleted.");
@@ -58,32 +51,33 @@ export function InProgressCard({ movie }: Props) {
return (
<div className="flex w-full items-center justify-between rounded-lg p-2 text-start transition-all duration-150 hover:bg-white/10">
<div className="flex gap-4">
<div className="flex aspect-square h-14 w-14 items-center justify-center overflow-hidden rounded-lg border border-[#A9A9A9]/50 bg-black/60">
{(movie.status.status === JobState.STARTED ||
movie.status.status === JobState.PENDING) && (
<FontAwesomeIcon icon={faSpinnerThird} spin size={"lg"} />
<div className="flex aspect-square h-14 w-14 items-center justify-center rounded-lg border border-[#A9A9A9]/50 bg-black/60 text-white">
<FontAwesomeIcon icon={faSpinnerThird} className="animate-spin" />
{/* {(job.status.status === JobState.STARTED ||
job.status.status === JobState.PENDING) && (
<FontAwesomeIcon icon={faSpinnerThird} className="animate-spin" />
)}
{(movie.status.status === JobState.COMPLETE_FAILURE ||
movie.status.status === JobState.ATTEMPT_FAILED) && (
{(job.status.status === JobState.COMPLETE_FAILURE ||
job.status.status === JobState.ATTEMPT_FAILED) && (
<FontAwesomeIcon icon={faCircleExclamation} size={"lg"} />
)}
)} */}
</div>
<div className="flex flex-col justify-center gap-1">
<div className="font-medium">
{movie.request.maybe_model_title || "Untitled"}
{job.request.maybe_model_title || "Image Generation"}
</div>
<div className="text-sm capitalize text-white/60">
{movie.status.status.replaceAll("_", " ")}... {completePercent}%
{job.status.status.replaceAll("_", " ")}... {completePercent}%
</div>
</div>
</div>
{movie.status.status !== JobState.STARTED && (
{job.status.status !== JobState.STARTED && (
<div className="pr-5">
<Tooltip content="Cancel" position="left">
<button
onClick={() => deleteJob(movie)}
onClick={() => deleteJob(job)}
className="text-[15px] font-medium text-white/50 transition-all duration-150 hover:text-white/100"
>
<FontAwesomeIcon icon={faClose} />
@@ -24,4 +24,28 @@ export const downloadFile = ({
.catch(() => addToast(ToastTypes.ERROR, "Could not download file."));
};
export const downloadFileImage = ({
url,
title,
}: {
url: string;
title: string;
}) => {
// console.log(url);
fetch(url)
.then((resp) => resp.blob())
.then((blob) => {
const blobUrl = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.style.display = "none";
a.href = blobUrl;
a.download = `${title}.png`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
})
.catch(() => addToast(ToastTypes.ERROR, "Could not download file."));
};
//check
@@ -3,19 +3,12 @@ import { faCheck } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button, LoadingSpinner, CloseButton } from "~/components";
import { TabSelector } from "~/components/reusable/TabSelector";
import React, {
useState,
useEffect,
Fragment,
useCallback,
useMemo,
} from "react";
import React, { useState, useEffect, useCallback, useMemo } from "react";
import { LibraryModalApi } from "./LibraryModalApi";
import { FilterMediaClasses } from "../../../../enums";
import { BucketConfig } from "../../../../api/BucketConfig";
import { twMerge } from "tailwind-merge";
import { createPortal } from "react-dom";
import { Transition } from "@headlessui/react";
import { LightboxModal } from "~/components/reusable/LightboxModal";
import { GetCdnOrigin } from "~/api/GetCdnOrigin";
export interface LibraryItem {
id: string;
@@ -64,7 +57,8 @@ export const LibraryModal = React.memo(
);
const [isLightboxVisible, setIsLightboxVisible] = useState(false);
const [failedImageUrls] = useState<Set<string>>(new Set());
const bucketConfig = useMemo(() => new BucketConfig(), []);
const cdnOrigin = GetCdnOrigin();
const imageUrl = lightboxImage?.fullImage || "";
const api = useMemo(() => new LibraryModalApi(), []);
const formatDate = useCallback((date: string) => {
@@ -109,10 +103,11 @@ export const LibraryModal = React.memo(
const getImageUrl = useCallback(
(path: string | undefined | null) => {
if (!path) return null;
const url = bucketConfig.getGcsUrl(path);
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
const url = `${cdnOrigin}${normalizedPath}`;
return failedImageUrls.has(url) ? null : url;
},
[bucketConfig, failedImageUrls],
[cdnOrigin, failedImageUrls],
);
useEffect(() => {
@@ -125,11 +120,8 @@ export const LibraryModal = React.memo(
if (response.success && response.data) {
const newItems = response.data.map((item) => ({
id: item.token,
label: item.maybe_title || "Untitled",
thumbnail: getImageUrl(
item.cover_image.maybe_cover_image_public_bucket_path ||
item.public_bucket_path,
),
label: item.maybe_title || "Image Generation",
thumbnail: getImageUrl(item.public_bucket_path),
fullImage: getImageUrl(item.public_bucket_path),
createdAt: item.created_at,
}));
@@ -318,74 +310,16 @@ export const LibraryModal = React.memo(
</div>
</TransitionDialogue>
{/* Lightbox Modal */}
{lightboxImage &&
createPortal(
<Transition appear show={isLightboxVisible} as={Fragment}>
<div className="fixed inset-0 z-[100]">
<Transition.Child
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div
className="fixed inset-0 cursor-pointer bg-black/60"
onClick={handleCloseLightbox}
/>
</Transition.Child>
<div
className="fixed inset-0 flex items-center justify-center p-4"
onClick={handleCloseLightbox}
>
<Transition.Child
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<div
className="relative h-[90vh] w-[80vw] rounded-xl bg-[#2C2C2C]"
onClick={(e) => e.stopPropagation()}
>
<CloseButton
onClick={handleCloseLightbox}
className="absolute right-4 top-4 z-10"
/>
<div className="grid h-full grid-cols-3 gap-6">
<div className="col-span-2 flex h-full items-center justify-center overflow-hidden bg-black/40">
{!lightboxImage.fullImage ? (
<div className="flex h-full w-full items-center justify-center bg-gray-800">
<span className="text-white/60">
Image not available
</span>
</div>
) : (
<img
src={lightboxImage.fullImage}
alt={lightboxImage.label}
className="h-full w-full object-contain"
onError={() =>
handleImageError(lightboxImage.fullImage!)
}
crossOrigin="anonymous"
referrerPolicy="no-referrer"
/>
)}
</div>
<div className="py-5">Prompt</div>
</div>
</div>
</Transition.Child>
</div>
</div>
</Transition>,
document.body,
)}
<LightboxModal
isOpen={isLightboxVisible}
onClose={handleCloseLightbox}
imageUrl={imageUrl}
imageAlt={lightboxImage?.label || ""}
onImageError={() => imageUrl && handleImageError(imageUrl)}
title={lightboxImage?.label}
createdAt={lightboxImage?.createdAt}
downloadUrl={imageUrl}
/>
</>
);
},
@@ -1,10 +1,41 @@
import { useState, useEffect } from "react";
import { Transition } from "@headlessui/react";
import { Key, KeyGroup, Mouse, Plus } from "./ControlsTopButtons/Help/Help";
import { CloseButton } from "~/components";
const STORAGE_KEY = "onboarding_helper_dismissed";
const EXPIRATION_DAYS = 3;
export const OnboardingHelper = () => {
const [actionPerformed, setActionPerformed] = useState(false);
const [keyPressed, setKeyPressed] = useState(false);
const [isVisible, setIsVisible] = useState(true);
useEffect(() => {
// Check if the helper was dismissed
const dismissedData = localStorage.getItem(STORAGE_KEY);
if (dismissedData) {
const { timestamp } = JSON.parse(dismissedData);
const expirationTime = timestamp + EXPIRATION_DAYS * 24 * 60 * 60 * 1000; // 3 days in milliseconds
if (Date.now() < expirationTime) {
setIsVisible(false);
} else {
// Clear expired data
localStorage.removeItem(STORAGE_KEY);
}
}
}, []);
const handleClose = () => {
setIsVisible(false);
// Store dismissal timestamp
localStorage.setItem(
STORAGE_KEY,
JSON.stringify({
timestamp: Date.now(),
})
);
};
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
@@ -30,7 +61,7 @@ export const OnboardingHelper = () => {
return (
<Transition
show={!(actionPerformed && keyPressed)}
show={isVisible && !(actionPerformed && keyPressed)}
enter="transition-opacity duration-500"
enterFrom="opacity-0"
enterTo="opacity-100"
@@ -38,7 +69,7 @@ export const OnboardingHelper = () => {
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="glass pointer-events-none absolute left-1/2 top-48 z-10 flex -translate-x-1/2 transform items-center justify-center gap-2 rounded-2xl border-2 border-brand-primary !bg-brand-primary/10 px-5 py-4">
<div className="glass pointer-events-none absolute left-1/2 top-48 z-10 flex -translate-x-1/2 transform items-center justify-center gap-2 rounded-2xl border-2 border-brand-primary !bg-brand-primary/10 px-5 py-4">
<div className="flex items-center">
<p className="pr-3 text-lg font-semibold">Hold</p>
<Mouse button="left" />
@@ -56,6 +87,9 @@ export const OnboardingHelper = () => {
</div>
</div>
<p className="text-lg font-semibold">to move around the scene</p>
<div className="pointer-events-auto ml-2">
<CloseButton onClick={handleClose} />
</div>
</div>
</Transition>
);
@@ -0,0 +1,73 @@
import React from "react";
import { useSignals } from "@preact/signals-react/runtime";
import { poseMode, showPoseControls } from "../signals/selectedMode";
import { Button, Tooltip } from "~/components";
import {
faCheck,
faCheckDouble,
faPersonRunning,
} from "@fortawesome/pro-solid-svg-icons";
declare global {
interface Window {
__mouseControls?: {
toggleFKMode: () => void;
};
}
}
interface PoseModeSelectorProps {}
export const PoseModeSelector: React.FC<PoseModeSelectorProps> = () => {
useSignals();
const handleModeChange = () => {
if (poseMode.value === "select") {
poseMode.value = "pose";
} else {
poseMode.value = "select";
}
window.__mouseControls?.toggleFKMode();
};
if (showPoseControls.value === false) {
return null;
}
return (
<>
<div
className="fixed left-1/2 top-32 flex -translate-x-1/2 transform items-center justify-center gap-2"
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
>
<Tooltip
content={"Toggle pose mode (K)"}
position={"bottom"}
delay={300}
closeOnClick={true}
>
<>
{poseMode.value === "select" ? (
<Button
icon={faPersonRunning}
onClick={handleModeChange}
className="rounded-xl shadow-xl outline-none focus-visible:outline-none"
>
Enter Pose Mode
</Button>
) : (
<Button
icon={faCheck}
onClick={handleModeChange}
className="rounded-xl outline-none focus-visible:outline-none"
>
Done
</Button>
)}
</>
</Tooltip>
</div>
</>
);
};
@@ -0,0 +1,43 @@
import { Signal } from "@preact/signals-react";
import { useSignals } from "@preact/signals-react/runtime";
export const PrecisionSelector = ({
showSignal,
coordSignal,
valuesSignal,
selectedValueSignal
}: {
showSignal: Signal<boolean>,
coordSignal: Signal<{ x: number; y: number }>,
valuesSignal: Signal<number[]>
selectedValueSignal: Signal<number>
}) => {
useSignals();
const handleMouseLeave = () => {
showSignal.value = false;
}
const handleMouseEnterItem = (scale: number) => {
selectedValueSignal.value = scale;
console.log("selectedValueSignal", selectedValueSignal.value);
}
return (
<div
className="fixed z-50 bg-red-600 -translate-x-1/2 -translate-y-1/2 flex-col gap-[1px] shadow-md bg-ui-divider border-ui-divider border-2 rounded-md overflow-clip"
style={{
top: coordSignal.value.y,
left: coordSignal.value.x,
display: showSignal.value ? "flex" : "none"
}}
onMouseLeave={handleMouseLeave}
>
{valuesSignal.value.map((scale, index) => (
<span onMouseEnter={() => handleMouseEnterItem(scale)} key={index}
className="flex bg-ui-panel text-sm justify-center align-middle justify-items-center px-2 hover:bg-ui-controls"
>{scale}</span>
))}
</div>
)
}
@@ -430,8 +430,10 @@ export const PromptBox = () => {
screenshot: snapshot.file,
sceneMediaToken: "",
});
console.log("useSystemPrompt", useSystemPrompt);
await engineApi.enqueueImageGeneration({
disableSystemPrompt: !useSystemPrompt,
prompt: prompt,
snapshotMediaToken: snapshotResult.data || "",
additionalImages: referenceImages.map((image) => image.mediaToken),
@@ -0,0 +1,6 @@
import { Signal, signal } from "@preact/signals-react";
export const showPrecisionSelector = signal(false);
export const precisionSelectorMenuCoords = signal({ x: 0, y: 0 });
export const precisionSelectorValues: Signal<number[]> = signal([]);
export const precisionSelectedValue = signal(0);
@@ -1,3 +1,5 @@
import { signal } from "@preact/signals-react";
export const selectedMode = signal("move");
export const showPoseControls = signal(false);
export const poseMode = signal("select");
+3
View File
@@ -16,6 +16,7 @@ import normalizeCss from "./styles/normalize.css?url";
import tailwindCss from "./styles/tailwind.css?url";
import baseCss from "./styles/base.css?url";
import { Environment, Configs } from "./configs";
//import { api } from "@storyteller/api";
// The following import prevents a Font Awesome icon server-side rendering bug,
// where the icons flash from a very large icon down to a properly sized one:
@@ -71,6 +72,8 @@ export async function loader() {
uploadApiVideo = "http://localhost:12345";
break;
}
//console.log(">>>> API LIBRARY IMPORT TEST", api());
const env = {
// @ts-expect-error ProvessEnv is correct
+4 -6
View File
@@ -1,15 +1,14 @@
[build]
#command = "./script/netlify_build.sh"
#publish = "build/client"
command = "./apps/editor3d/script/netlify_build.sh"
publish = "apps/editor3d/dist/"
[build.environment]
REACT_APP_PUBLIC_POSTHOG_KEY = "phc_jBFgac0mVALAFk3negnSfYCcHgvkT00yBLQDmCDYNBb"
REACT_APP_PUBLIC_POSTHOG_UI = "https://us.i.posthog.com"
[dev]
command = "npm run dev"
framework = "vite"
#[dev]
#command = "npm run dev"
#framework = "vite"
# Set immutable caching for static files, because they have fingerprinted filenames
@@ -17,7 +16,6 @@ framework = "vite"
for = "/build/*"
[headers.values]
"Cache-Control" = "public, max-age=31560000, immutable"
[[redirects]]
+1 -3
View File
@@ -21,6 +21,7 @@
"test": "jest"
},
"dependencies": {
"@storyteller/api": "file:../../libs/api",
"@awesome.me/kit-fde2be5eb0": "^1.0.7",
"@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/free-brands-svg-icons": "^6.5.2",
@@ -44,14 +45,12 @@
"@zip.js/zip.js": "^2.7.51",
"ammo.js": "^0.0.10",
"ccapture.js": "github:aceslowman/ccapture.js#npm-fix",
"common": "file:common",
"csstype": "^3.1.3",
"dayjs": "^1.11.10",
"deep-equal": "^2.2.3",
"detect-gpu": "^5.0.38",
"http-proxy-middleware": "^3.0.0",
"isbot": "^4.1.0",
"kalidokit": "file:./vendor/kalidokit",
"lodash.debounce": "^4.0.8",
"posthog-js": "^1.141.0",
"react": "^18.2.0",
@@ -101,7 +100,6 @@
"eslint-plugin-storybook": "^0.8.0",
"husky": "^9.0.11",
"lint-staged": "^15.2.2",
"node-sass": "^9.0.0",
"postcss": "^8.4.37",
"postcss-import": "^16.0.1",
"postcss-nesting": "^12.1.0",
+10 -32
View File
@@ -2,48 +2,26 @@
set -euxo pipefail
# echo "Run build script"
# npm install
# npm run build
#
# #echo "Emit built files."
# #pwd
# #find .
#
# # echo "Copy test files into dist"
# # cp -r test/* dist/
# #
# # echo "Copy netlify configs into dist"
# # cp _headers dist/
# # cp _redirects dist/
# #
# # echo "Copy netlify 404.html page into dist"
# # cp "404.html" dist/
# #
# # echo "List files after build"
# # find dist/
echo "Run build script (TODO: Make strict)"
echo "Build Editor 3D"
nx build editor3d
echo "Change to project dir"
pushd apps/editor3d/
echo "List directory files"
pwd
ls -lA .
echo "List build directory files"
pwd
ls -lA build/
#echo "Copy netlify configs into dist"
#cp _headers dist/
#cp _redirects dist/
echo "Make output directory"
mkdir dist/
#echo "Copy netlify 404.html page into dist"
echo "Copy files to putput directory"
cp -r build/server/* dist/
cp -r build/client/* dist/
cp _headers dist/
#cp _redirects dist/
#cp "404.html" dist/
echo "List files after build"
echo "List final files in build"
find dist/
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
set -euxo pipefail
echo "Removing Node Modules..."
rm -rf ./node_modules/
rm -rf ./apps/editor2d/node_modules/
rm -rf ./apps/editor3d/node_modules/
+50
View File
@@ -0,0 +1,50 @@
{
"extends": [
"../../.eslintrc.json"
],
"ignorePatterns": [
"!**/*"
],
"overrides": [
{
"files": [
"*.ts",
"*.tsx",
"*.js",
"*.jsx"
],
"rules": {}
},
{
"files": [
"*.ts",
"*.tsx"
],
"rules": {}
},
{
"files": [
"*.js",
"*.jsx"
],
"rules": {}
},
{
"files": [
"*.json"
],
"parser": "jsonc-eslint-parser",
"rules": {
"@nx/dependency-checks": [
"error",
{
"ignoredFiles": [
"{projectRoot}/eslint.config.{js,cjs,mjs}",
"{projectRoot}/vite.config.{js,ts,mjs,mts}"
]
}
]
}
}
]
}
+11
View File
@@ -0,0 +1,11 @@
# api
This library was generated with [Nx](https://nx.dev).
## Building
Run `nx build api` to build the library.
## Running unit tests
Run `nx test api` to execute the unit tests via [Vitest](https://vitest.dev/).
+19
View File
@@ -0,0 +1,19 @@
{
"name": "@storyteller/api",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"development": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
},
"dependencies": {}
}
+1
View File
@@ -0,0 +1 @@
export * from './lib/api.js';
+7
View File
@@ -0,0 +1,7 @@
import { api } from './api.js';
describe('api', () => {
it('should work', () => {
expect(api()).toEqual('api');
})
})
+3
View File
@@ -0,0 +1,3 @@
export function api(): string {
return 'api library built and imported!';
}
+13
View File
@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}
+35
View File
@@ -0,0 +1,35 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": ".",
"rootDir": "src",
"outDir": "dist",
"tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo",
"emitDeclarationOnly": true,
"module": "nodenext",
"moduleResolution": "nodenext",
"forceConsistentCasingInFileNames": true,
"types": [
"node",
"vite/client"
]
},
"include": [
"src/**/*.ts"
],
"references": [],
"exclude": [
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx"
]
}
+36
View File
@@ -0,0 +1,36 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./out-tsc/vitest",
"types": [
"vitest/globals",
"vitest/importMeta",
"vite/client",
"node",
"vitest"
],
"module": "nodenext",
"moduleResolution": "nodenext",
"forceConsistentCasingInFileNames": true
},
"include": [
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}
+48
View File
@@ -0,0 +1,48 @@
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
import * as path from 'path';
export default defineConfig(() => ({
root: __dirname,
cacheDir: '../../node_modules/.vite/libs/api',
plugins: [dts({ entryRoot: 'src', tsconfigPath: path.join(__dirname, 'tsconfig.lib.json') })],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
// Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode
build: {
outDir: './dist',
emptyOutDir: true,
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
lib: {
// Could also be a dictionary or array of multiple entry points.
entry: 'src/index.ts',
name: '@storyteller/api',
fileName: 'index',
// Change this to the formats you want to support.
// Don't forget to update your package.json as well.
formats: ['es' as const]
},
rollupOptions: {
// External packages that should not be bundled into your library.
external: []
},
},
test: {
watch: false,
globals: true,
environment: 'node',
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: ['default'],
coverage: {
reportsDirectory: './test-output/vitest/coverage',
provider: 'v8' as const,
}
},
}));
+17 -1
View File
@@ -1,5 +1,6 @@
{
"$schema": "./node_modules/nx/schemas/nx-schema.json",
"npmScope": "storyteller",
"namedInputs": {
"default": [
"{projectRoot}/**/*",
@@ -9,7 +10,9 @@
"default",
"!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
"!{projectRoot}/tsconfig.spec.json",
"!{projectRoot}/src/test-setup.[jt]s"
"!{projectRoot}/src/test-setup.[jt]s",
"!{projectRoot}/.eslintrc.json",
"!{projectRoot}/eslint.config.mjs"
],
"sharedGlobals": []
},
@@ -65,6 +68,12 @@
"watchDepsTargetName": "watch-deps",
"staticServeTargetName": "static-serve"
}
},
{
"plugin": "@nx/eslint/plugin",
"options": {
"targetName": "eslint:lint"
}
}
],
"generators": {
@@ -83,5 +92,12 @@
"linter": "none"
}
}
},
"targetDefaults": {
"test": {
"dependsOn": [
"^build"
]
}
}
}
+1575 -3954
View File
File diff suppressed because it is too large Load Diff
+12 -1
View File
@@ -7,11 +7,14 @@
"dependencies": {
"@preact/signals-react": "^3.0.1",
"@remix-run/serve": "^2.15.0",
"pnpm": "^10.8.1",
"react": "18.2.0",
"react-dom": "18.2.0"
},
"devDependencies": {
"@netlify/remix-edge-adapter": "^3.4.3",
"@nx/eslint": "20.7.2",
"@nx/eslint-plugin": "20.7.2",
"@nx/js": "20.7.2",
"@nx/react": "20.7.2",
"@nx/remix": "^20.7.2",
@@ -26,20 +29,28 @@
"@types/node": "^20.0.0",
"@types/react": "19.0.0",
"@types/react-dom": "19.0.0",
"@typescript-eslint/eslint-plugin": "^7.16.0",
"@typescript-eslint/parser": "^7.16.0",
"@vitejs/plugin-react": "^4.2.0",
"@vitest/coverage-v8": "^3.0.5",
"@vitest/ui": "^3.0.0",
"eslint": "~8.57.0",
"eslint-config-prettier": "^10.0.0",
"jiti": "2.4.2",
"jsdom": "~22.1.0",
"jsonc-eslint-parser": "^2.1.0",
"nx": "20.7.2",
"tslib": "^2.3.0",
"typescript": "~5.7.2",
"vite": "^6.0.0",
"vite-plugin-banner": "^0.8.0",
"vite-plugin-dts": "~4.5.0",
"vite-plugin-static-copy": "^2.3.1",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.0.0"
},
"workspaces": [
"apps/*"
"apps/*",
"libs/*"
]
}
+4
View File
@@ -0,0 +1,4 @@
packages:
- 'apps/*'
- 'libs/*'
+314
View File
@@ -0,0 +1,314 @@
import os
import hashlib
import filecmp
from pathlib import Path
from datetime import datetime
import difflib
def get_file_hash(filepath):
"""Calculate MD5 hash of a file."""
md5_hash = hashlib.md5()
try:
with open(filepath, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
md5_hash.update(chunk)
return md5_hash.hexdigest()
except Exception:
return "Error calculating hash"
def format_size(size_bytes):
"""Format file size nicely."""
if size_bytes < 1000:
return f"{size_bytes}B"
elif size_bytes < 1000000:
return f"{size_bytes / 1000:.1f}KB"
else:
return f"{size_bytes / 1000000:.1f}MB"
def count_lines(filepath):
"""Count lines in a file safely."""
try:
with open(filepath, "rb") as f:
return sum(1 for _ in f)
except Exception:
return 0
def calculate_diff_ratio(file1, file2):
"""Calculate how different two files are using difflib."""
try:
with (
open(file1, "r", errors="replace") as f1,
open(file2, "r", errors="replace") as f2,
):
text1 = f1.readlines()
text2 = f2.readlines()
matcher = difflib.SequenceMatcher(None, text1, text2)
return 1.0 - matcher.ratio() # Higher value means more different
except Exception:
return 1.0 # If we can't compare, assume completely different
def get_relative_path(file_path, base_path):
"""Get relative path from base directory."""
return str(Path(file_path).relative_to(base_path))
def collect_files_recursive(folder_path):
"""Recursively collect all files in a folder."""
folder_path = Path(folder_path)
file_dict = {}
for path in folder_path.rglob("*"):
if path.is_file():
rel_path = get_relative_path(path, folder_path)
file_dict[rel_path] = path
return file_dict
def compare_folders_recursive(folder1, folder2, output_file=None):
"""Compare two folders recursively and generate a detailed report."""
# Automatically generate output filename if not specified
if output_file is None:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_file = f"folder_comparison_{timestamp}.md"
# Get folder paths
folder1_path = Path(folder1)
folder2_path = Path(folder2)
# Get shorthand names for display
folder1_name = folder1_path.name
folder2_name = folder2_path.name
# Get all files recursively
print(f"Collecting files from {folder1_name}...")
files1 = collect_files_recursive(folder1_path)
print(f"Collecting files from {folder2_name}...")
files2 = collect_files_recursive(folder2_path)
# All unique file paths across both folders
all_files = sorted(set(files1.keys()) | set(files2.keys()))
# Calculate column widths for alignment
max_filepath_len = max(len(file) for file in all_files) + 2
hash_col_width = 10 # For hash comparison
size_col_width = 20 # For size and line count
diff_col_width = 30 # For difference description
# Initialize report
report = [
f"# Recursive Folder Comparison Report: {folder1_name} vs {folder2_name}\n",
f"Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n",
"## File Comparison Table\n",
f"| {'File Path'.ljust(max_filepath_len)} | {'Status'.ljust(hash_col_width)} | {folder1_name.ljust(size_col_width)} | {folder2_name.ljust(size_col_width)} | {'Difference'.ljust(diff_col_width)} |",
f"|{'-' * max_filepath_len}|{'-' * hash_col_width}|{'-' * size_col_width}|{'-' * size_col_width}|{'-' * diff_col_width}|",
]
# Track file categories
identical_files = []
different_files = []
only_in_folder1 = []
only_in_folder2 = []
# Store detailed file comparisons for sorting later
file_comparisons = []
# Compare each file
print(f"Comparing {len(all_files)} unique files...")
for rel_path in all_files:
in_folder1 = rel_path in files1
in_folder2 = rel_path in files2
# Default values
status = "—"
size1_str = "—"
size2_str = "—"
diff_str = "—"
diff_ratio = 0
# Get details for folder1
if in_folder1:
file1 = files1[rel_path]
size1 = file1.stat().st_size
lines1 = count_lines(file1)
hash1 = get_file_hash(file1)
size1_str = f"{format_size(size1)} ({lines1} lines)"
else:
hash1 = "—"
size1 = 0
lines1 = 0
# Get details for folder2
if in_folder2:
file2 = files2[rel_path]
size2 = file2.stat().st_size
lines2 = count_lines(file2)
hash2 = get_file_hash(file2)
size2_str = f"{format_size(size2)} ({lines2} lines)"
else:
hash2 = "—"
size2 = 0
lines2 = 0
# Calculate difference and determine status
if not in_folder1:
status = "MISSING"
diff_str = f"Only in {folder2_name}"
only_in_folder2.append(rel_path)
diff_ratio = 1.0 # Completely different (missing)
elif not in_folder2:
status = "MISSING"
diff_str = f"Only in {folder1_name}"
only_in_folder1.append(rel_path)
diff_ratio = 1.0 # Completely different (missing)
else:
# Both files exist, compare them
if hash1 == hash2 and hash1 != "Error calculating hash":
status = "IDENTICAL"
diff_str = "Identical content"
identical_files.append(rel_path)
diff_ratio = 0.0
else:
status = "DIFFERENT"
size_diff = size2 - size1
lines_diff = lines2 - lines1
sign = "+" if size_diff > 0 else ""
sign_lines = "+" if lines_diff > 0 else ""
# Calculate how different the files are
diff_ratio = calculate_diff_ratio(file1, file2)
different_files.append((rel_path, diff_ratio))
# Format difference description
diff_pct = int(diff_ratio * 100)
diff_str = f"{sign}{format_size(size_diff)} ({sign_lines}{lines_diff} lines) {diff_pct}% different"
# Store comparison for sorting later
file_comparisons.append(
{
"rel_path": rel_path,
"status": status,
"size1_str": size1_str,
"size2_str": size2_str,
"diff_str": diff_str,
"diff_ratio": diff_ratio,
"in_folder1": in_folder1,
"in_folder2": in_folder2,
"hash1": hash1[:8] if len(hash1) > 8 else hash1,
"hash2": hash2[:8] if len(hash2) > 8 else hash2,
}
)
# Sort files by difference ratio (most different first)
file_comparisons.sort(key=lambda x: x["diff_ratio"], reverse=True)
# Generate the sorted table
for comp in file_comparisons:
hash_display = (
f"{comp['hash1']}:{comp['hash2']}"
if comp["status"] == "DIFFERENT"
else comp["status"]
)
report.append(
f"| {comp['rel_path'].ljust(max_filepath_len)} | "
f"{hash_display.ljust(hash_col_width)} | "
f"{comp['size1_str'].ljust(size_col_width)} | "
f"{comp['size2_str'].ljust(size_col_width)} | "
f"{comp['diff_str'].ljust(diff_col_width)} |"
)
# Sort different files by difference ratio for the detailed section
different_files.sort(key=lambda x: x[1], reverse=True)
# Add Files Missing From Each Folder sections
report.append("\n## Files Present in One Folder But Missing in the Other\n")
if only_in_folder1:
report.append(
f"\n### Files Only in {folder1_name} ({len(only_in_folder1)} files)"
)
for rel_path in sorted(only_in_folder1):
file1 = files1[rel_path]
size1 = file1.stat().st_size
lines1 = count_lines(file1)
hash1 = get_file_hash(file1)[:8]
report.append(
f"- `{rel_path}` ({format_size(size1)}, {lines1} lines, hash: {hash1})"
)
if only_in_folder2:
report.append(
f"\n### Files Only in {folder2_name} ({len(only_in_folder2)} files)"
)
for rel_path in sorted(only_in_folder2):
file2 = files2[rel_path]
size2 = file2.stat().st_size
lines2 = count_lines(file2)
hash2 = get_file_hash(file2)[:8]
report.append(
f"- `{rel_path}` ({format_size(size2)}, {lines2} lines, hash: {hash2})"
)
# Add detailed section for different files
if different_files:
report.append(f"\n## Files with Differences (Sorted by Most Different First)\n")
for rel_path, diff_ratio in different_files:
file1 = files1[rel_path]
file2 = files2[rel_path]
size1 = file1.stat().st_size
size2 = file2.stat().st_size
lines1 = count_lines(file1)
lines2 = count_lines(file2)
hash1 = get_file_hash(file1)[:8]
hash2 = get_file_hash(file2)[:8]
size_diff = size2 - size1
lines_diff = lines2 - lines1
diff_pct = int(diff_ratio * 100)
report.append(f"### `{rel_path}` ({diff_pct}% different)")
report.append(
f"- {folder1_name}: {format_size(size1)}, {lines1} lines, hash: {hash1}"
)
report.append(
f"- {folder2_name}: {format_size(size2)}, {lines2} lines, hash: {hash2}"
)
report.append(
f"- Difference: {size_diff:+d} bytes, {lines_diff:+d} lines\n"
)
# Add summary section
report.extend(
[
"\n## Summary",
f"- **Total Unique Files**: {len(all_files)}",
f"- **Files in {folder1_name}**: {len(files1)}",
f"- **Files in {folder2_name}**: {len(files2)}",
f"- **Identical Files**: {len(identical_files)}",
f"- **Different Files**: {len(different_files)}",
f"- **Files Only in {folder1_name}**: {len(only_in_folder1)}",
f"- **Files Only in {folder2_name}**: {len(only_in_folder2)}",
f"\n## Path Information",
f"- {folder1_name}: `{folder1}`",
f"- {folder2_name}: `{folder2}`",
]
)
# Write report to file
with open(output_file, "w") as f:
f.write("\n".join(report))
print(f"Recursive comparison report written to {output_file}")
return "\n".join(report)
# Execute the comparison
if __name__ == "__main__":
folder1 = "/Users/michaelchung/storyteller-rust/frontend/apps/editor2d/src/Classes/"
folder2 = "/Users/michaelchung/storyteller-rust/frontend/apps/editor3d/app/Classes/"
report = compare_folders_recursive(folder1, folder2)
@@ -0,0 +1,165 @@
# Recursive Folder Comparison Report: Classes vs Classes
Generated on: 2025-04-16 13:33:19
## File Comparison Table
| File Path | Status | Classes | Classes | Difference |
|------------------------------------|----------|--------------------|--------------------|------------------------------|
| ApiHelpers.ts | MISSING | — | 637B (17 lines) | Only in Classes |
| ApiManager/SegmentationApi.ts | MISSING | 1.9KB (84 lines) | — | Only in Classes |
| ApiManager/StudioGen2Api.ts | MISSING | — | 1.6KB (61 lines) | Only in Classes |
| EnvironmentVariables.ts | MISSING | — | 263B (10 lines) | Only in Classes |
| GetFrontendEnvironment.ts | MISSING | — | 1.9KB (65 lines) | Only in Classes |
| ApiManager/MediaUploadApi.ts | 707d2bf2:06a93d8c | 10.3KB (349 lines) | 10.0KB (339 lines) | -255B (-10 lines) 72% different |
| ApiManager/EngineApi.ts | c4850e80:85e20a81 | 1.0KB (39 lines) | 5.5KB (213 lines) | +4.5KB (+174 lines) 69% different |
| ApiManager/JobsApi.ts | f7eb8e22:2aa03349 | 2.0KB (77 lines) | 1.9KB (74 lines) | -56B (-3 lines) 28% different |
| ApiManager/ApiManager.ts | 44db0b07:67a3a046 | 5.5KB (235 lines) | 5.3KB (211 lines) | -200B (-24 lines) 14% different |
| ApiManager/BillingApi.ts | 20051893:f5b7af84 | 1.0KB (33 lines) | 1.0KB (33 lines) | -8B (0 lines) 6% different |
| ApiManager/VoiceDesignerApi.ts | e4d874c3:3635ba42 | 2.0KB (77 lines) | 2.1KB (78 lines) | +94B (+1 lines) 5% different |
| ApiManager/MediaFilesApi.ts | be67aab2:44342e0f | 10.6KB (364 lines) | 10.6KB (369 lines) | +56B (+5 lines) 5% different |
| ApiManager/PromptsApi.ts | d73d329a:c4824d2e | 648B (25 lines) | 657B (25 lines) | +9B (0 lines) 4% different |
| ApiManager/MiscApi.ts | 298f5d93:187ad676 | 799B (26 lines) | 799B (26 lines) | 0B (0 lines) 3% different |
| ApiManager/TtsApi.ts | 5feb5cad:6228b278 | 1.7KB (64 lines) | 1.7KB (64 lines) | +2B (0 lines) 3% different |
| ApiManager/index.ts | 55a9094b:1fe59701 | 564B (18 lines) | 597B (19 lines) | +33B (+1 lines) 2% different |
| ApiManager/UserBookmarksApi.ts | e3e4cb19:fd3afc78 | 5.4KB (201 lines) | 5.5KB (202 lines) | +56B (+1 lines) 2% different |
| ApiManager/WeightsApi.ts | c12a98df:8a7dbb68 | 7.8KB (295 lines) | 7.9KB (296 lines) | +39B (+1 lines) 2% different |
| ApiManager/VideoApi.ts | a9947328:cb947c07 | 1.9KB (70 lines) | 1.9KB (70 lines) | -11B (0 lines) 1% different |
| ApiManager/BetaKeysApi.ts | 8d4ec6f1:33f1e4b3 | 4.3KB (167 lines) | 4.3KB (167 lines) | -3B (0 lines) 1% different |
| ApiManager/UsersApi.ts | 65121217:72db0eae | 3.9KB (161 lines) | 3.9KB (161 lines) | -6B (0 lines) 0% different |
| ApiManager/AnalyticsApi.ts | IDENTICAL | 944B (38 lines) | 944B (38 lines) | Identical content |
| ApiManager/CommentsApi.ts | IDENTICAL | 2.3KB (91 lines) | 2.3KB (91 lines) | Identical content |
| ApiManager/FeaturedItemsApi.ts | IDENTICAL | 2.3KB (96 lines) | 2.3KB (96 lines) | Identical content |
| ApiManager/ModerationApi.ts | IDENTICAL | 960B (42 lines) | 960B (42 lines) | Identical content |
| ApiManager/UserRatingsApi.ts | IDENTICAL | 2.4KB (92 lines) | 2.4KB (92 lines) | Identical content |
| ApiManager/VoiceConversionApi.ts | IDENTICAL | 1.4KB (53 lines) | 1.4KB (53 lines) | Identical content |
## Files Present in One Folder But Missing in the Other
### Files Only in Classes (23 files)
- `ApiManager/SegmentationApi.ts` (1.9KB, 84 lines, hash: bb14fbf6)
- `ApiManager/enums/Api.ts` (254B, 7 lines, hash: ea867d98)
- `ApiManager/enums/Billing.ts` (242B, 11 lines, hash: e47c3eaa)
- `ApiManager/enums/Job.ts` (433B, 16 lines, hash: d148fd64)
- `ApiManager/enums/MediaFileClass.ts` (524B, 26 lines, hash: c90ce542)
- `ApiManager/enums/MediaFileType.ts` (1.7KB, 58 lines, hash: a2374fec)
- `ApiManager/enums/QueryFilters.ts` (579B, 29 lines, hash: 6d10a292)
- `ApiManager/enums/Tts.ts` (205B, 7 lines, hash: f6393d2e)
- `ApiManager/enums/UserFeatures.ts` (167B, 6 lines, hash: 05d49e51)
- `ApiManager/enums/Visibility.ts` (92B, 5 lines, hash: 297d031b)
- `ApiManager/enums/WeightCategory.ts` (180B, 8 lines, hash: f9be3d32)
- `ApiManager/enums/WeightType.ts` (208B, 11 lines, hash: 80436726)
- `ApiManager/models/BetaKeys.ts` (502B, 26 lines, hash: 7c718ac6)
- `ApiManager/models/Billing.tsx` (416B, 17 lines, hash: 8db6eb3b)
- `ApiManager/models/Dataset.ts` (588B, 23 lines, hash: 6af860ff)
- `ApiManager/models/Job.ts` (1.8KB, 72 lines, hash: 9d7b385a)
- `ApiManager/models/MediaFile.ts` (1.8KB, 59 lines, hash: 41778349)
- `ApiManager/models/Pagination.ts` (200B, 9 lines, hash: 837b24d3)
- `ApiManager/models/Prompts.ts` (424B, 16 lines, hash: 8ef1f366)
- `ApiManager/models/StatusAlert.ts` (175B, 9 lines, hash: fcee4684)
- `ApiManager/models/UserBookmark.ts` (1.6KB, 75 lines, hash: 8f70d1b1)
- `ApiManager/models/Users.ts` (1.3KB, 50 lines, hash: 5ed82bbd)
- `ApiManager/models/Weight.ts` (767B, 40 lines, hash: 3f9cccdb)
### Files Only in Classes (4 files)
- `ApiHelpers.ts` (637B, 17 lines, hash: 9b638c6c)
- `ApiManager/StudioGen2Api.ts` (1.6KB, 61 lines, hash: b2707195)
- `EnvironmentVariables.ts` (263B, 10 lines, hash: 29c93666)
- `GetFrontendEnvironment.ts` (1.9KB, 65 lines, hash: 7ee8b738)
## Files with Differences (Sorted by Most Different First)
### `ApiManager/MediaUploadApi.ts` (72% different)
- Classes: 10.3KB, 349 lines, hash: 707d2bf2
- Classes: 10.0KB, 339 lines, hash: 06a93d8c
- Difference: -255 bytes, -10 lines
### `ApiManager/EngineApi.ts` (69% different)
- Classes: 1.0KB, 39 lines, hash: c4850e80
- Classes: 5.5KB, 213 lines, hash: 85e20a81
- Difference: +4469 bytes, +174 lines
### `ApiManager/JobsApi.ts` (28% different)
- Classes: 2.0KB, 77 lines, hash: f7eb8e22
- Classes: 1.9KB, 74 lines, hash: 2aa03349
- Difference: -56 bytes, -3 lines
### `ApiManager/ApiManager.ts` (14% different)
- Classes: 5.5KB, 235 lines, hash: 44db0b07
- Classes: 5.3KB, 211 lines, hash: 67a3a046
- Difference: -200 bytes, -24 lines
### `ApiManager/BillingApi.ts` (6% different)
- Classes: 1.0KB, 33 lines, hash: 20051893
- Classes: 1.0KB, 33 lines, hash: f5b7af84
- Difference: -8 bytes, +0 lines
### `ApiManager/VoiceDesignerApi.ts` (5% different)
- Classes: 2.0KB, 77 lines, hash: e4d874c3
- Classes: 2.1KB, 78 lines, hash: 3635ba42
- Difference: +94 bytes, +1 lines
### `ApiManager/MediaFilesApi.ts` (5% different)
- Classes: 10.6KB, 364 lines, hash: be67aab2
- Classes: 10.6KB, 369 lines, hash: 44342e0f
- Difference: +56 bytes, +5 lines
### `ApiManager/PromptsApi.ts` (4% different)
- Classes: 648B, 25 lines, hash: d73d329a
- Classes: 657B, 25 lines, hash: c4824d2e
- Difference: +9 bytes, +0 lines
### `ApiManager/MiscApi.ts` (3% different)
- Classes: 799B, 26 lines, hash: 298f5d93
- Classes: 799B, 26 lines, hash: 187ad676
- Difference: +0 bytes, +0 lines
### `ApiManager/TtsApi.ts` (3% different)
- Classes: 1.7KB, 64 lines, hash: 5feb5cad
- Classes: 1.7KB, 64 lines, hash: 6228b278
- Difference: +2 bytes, +0 lines
### `ApiManager/index.ts` (2% different)
- Classes: 564B, 18 lines, hash: 55a9094b
- Classes: 597B, 19 lines, hash: 1fe59701
- Difference: +33 bytes, +1 lines
### `ApiManager/UserBookmarksApi.ts` (2% different)
- Classes: 5.4KB, 201 lines, hash: e3e4cb19
- Classes: 5.5KB, 202 lines, hash: fd3afc78
- Difference: +56 bytes, +1 lines
### `ApiManager/WeightsApi.ts` (2% different)
- Classes: 7.8KB, 295 lines, hash: c12a98df
- Classes: 7.9KB, 296 lines, hash: 8a7dbb68
- Difference: +39 bytes, +1 lines
### `ApiManager/VideoApi.ts` (1% different)
- Classes: 1.9KB, 70 lines, hash: a9947328
- Classes: 1.9KB, 70 lines, hash: cb947c07
- Difference: -11 bytes, +0 lines
### `ApiManager/BetaKeysApi.ts` (1% different)
- Classes: 4.3KB, 167 lines, hash: 8d4ec6f1
- Classes: 4.3KB, 167 lines, hash: 33f1e4b3
- Difference: -3 bytes, +0 lines
### `ApiManager/UsersApi.ts` (0% different)
- Classes: 3.9KB, 161 lines, hash: 65121217
- Classes: 3.9KB, 161 lines, hash: 72db0eae
- Difference: -6 bytes, +0 lines
## Summary
- **Total Unique Files**: 49
- **Files in Classes**: 45
- **Files in Classes**: 26
- **Identical Files**: 6
- **Different Files**: 16
- **Files Only in Classes**: 23
- **Files Only in Classes**: 4
## Path Information
- Classes: `/Users/michaelchung/storyteller-rust/frontend/apps/editor2d/src/Classes/`
- Classes: `/Users/michaelchung/storyteller-rust/frontend/apps/editor3d/app/Classes/`
@@ -0,0 +1,38 @@
import { ApiManager, ApiResponse } from "./ApiManager";
export class AnalyticsApi extends ApiManager {
public PostAnalytics({
maybeLastAction,
maybeLogToken,
}: {
maybeLastAction: string;
maybeLogToken: string;
}): Promise<ApiResponse<string>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/analytics/log_session`;
const body = {
maybe_last_action: maybeLastAction,
maybe_log_token: maybeLogToken,
};
return this.post<
{
maybe_last_action: string;
maybe_log_token: string;
},
{
success?: boolean;
log_token?: string;
BadInput?: string;
}
>({ endpoint, body })
.then((response) => ({
success: response.success ?? false,
data: response.log_token,
errorMessage: response.BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
}
@@ -0,0 +1,17 @@
// import { GetCdnOrigin } from "~/api/GetCdnOrigin";
// import environmentVariables from "./EnvironmentVariables";
// export async function get_media_url(mediaId: string) {
// //This is for prod when we have the proper info on the url.
// const api_base_url = environmentVariables.values.BASE_API;
// const url = `${api_base_url}/v1/media_files/file/${mediaId}`;
// const response = await fetch(url);
// const json = await JSON.parse(await response.text());
// const bucketPath = json["media_file"]["public_bucket_path"];
// const media_api_base_url = GetCdnOrigin();
// const media_url = `${media_api_base_url}${bucketPath}`;
// return media_url;
// }
@@ -0,0 +1,235 @@
// import { bool } from "@techstark/opencv-js";
import { API_TARGETS } from "../enums/Api";
type NonNullableObject<T extends object> = NonNullable<T>;
export interface ApiResponse<T, P = undefined> {
success: boolean;
errorMessage?: string;
data?: T;
pagination?: P;
}
export class ApiManager {
ApiTargets: Record<string, string> = {};
constructor() {
this.ApiTargets = {
BaseApi: API_TARGETS.BASE_API,
GoggleApi: API_TARGETS.GOOGLE_API,
FunnelApi: API_TARGETS.FUNNEL_API,
CdnApi: API_TARGETS.CDN_API,
GravatarApi: API_TARGETS.GRAVATAR_API,
};
}
public async fetch<B, T>(
endpoint: string,
{
method,
query,
body,
}: {
method: string;
query?: Record<string, string | boolean | number | undefined>;
body?: B;
},
): Promise<T> {
const queryInString =
query &&
Object.entries(query).reduce(
(allOptions, [key, value]) => {
if (!value) {
return allOptions;
}
allOptions[key] = value.toString();
return allOptions;
},
{} as Record<string, string>,
);
const endpointWithQueries = queryInString
? endpoint + "?" + new URLSearchParams(queryInString)
: endpoint;
const bodyInString = JSON.stringify(body);
const response = await fetch(endpointWithQueries, {
method,
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
credentials: "include",
body: bodyInString,
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
public async fetchMultipartFormData<T>(
endpoint: string,
{
method,
body,
}: {
method: string;
body: FormData;
},
): Promise<T> {
const response = await fetch(endpoint, {
method,
headers: {
Accept: "application/json",
},
credentials: "include",
body: body,
});
return response.json();
}
protected get<T>({
endpoint,
query,
}: {
endpoint: string;
query?: Record<string, string | boolean | number | undefined>;
}): Promise<T> {
return this.fetch<null, T>(endpoint, { method: "GET", query });
}
protected post<B, T>({
endpoint,
query,
body,
}: {
endpoint: string;
query?: Record<string, string | boolean | number | undefined>;
body?: B;
}): Promise<T> {
return this.fetch<B, T>(endpoint, {
method: "POST",
query,
body,
});
}
protected delete<B, T>({
endpoint,
query,
body,
}: {
endpoint: string;
query?: Record<string, string | boolean | number | undefined>;
body?: B;
}): Promise<T> {
return this.fetch<B, T>(endpoint, {
method: "DELETE",
query,
body,
});
}
protected async postFormVideo<T>({
endpoint,
formRecord,
uuid,
blob,
blobFileName,
}: {
endpoint: string;
formRecord: Record<string, string>;
uuid: string;
blob?: Blob | File;
blobFileName?: string;
}): Promise<T> {
const formData = new FormData();
formData.append("uuid_idempotency_token", uuid);
Object.entries(formRecord).forEach(([key, value]) => {
formData.append(key, value);
});
if (blob && blobFileName) {
formData.append("video", blob, blobFileName);
} else if (blob) {
formData.append("video", blob);
}
return this.fetchMultipartFormData<T>(endpoint, {
method: "POST",
body: formData,
});
}
protected async postForm<T>({
endpoint,
formRecord,
uuid,
blob,
blobFileName,
}: {
endpoint: string;
formRecord: Record<string, string>;
uuid: string;
blob?: Blob | File;
blobFileName?: string;
}): Promise<T> {
const formData = new FormData();
formData.append("uuid_idempotency_token", uuid);
Object.entries(formRecord).forEach(([key, value]) => {
formData.append(key, value);
});
if (blob && blobFileName) {
formData.append("file", blob, blobFileName);
} else if (blob) {
formData.append("file", blob);
}
return this.fetchMultipartFormData<T>(endpoint, {
method: "POST",
body: formData,
});
}
protected camelToSnakeCase(str: string) {
return str.replace(/([a-z0])([A-Z])/g, "$1_$2").toLowerCase();
}
protected parseQueryValues(
params: Record<string, string | string[] | boolean | number | undefined>,
): Record<string, string> {
return Object.entries(params).reduce(
(allParams, [key, value]) => {
if (!value) {
return allParams;
}
const snakeKey = this.camelToSnakeCase(key);
if (Array.isArray(value)) {
return { ...allParams, [snakeKey]: value.join(",") };
}
return { ...allParams, [snakeKey]: value.toString() };
},
{} as Record<string, string>,
);
}
protected parseBodyValues<T extends object, B extends object>(
params: NonNullableObject<T>,
): B {
return Object.entries(params).reduce((allParams, [key, value]) => {
if (!value) {
return allParams;
}
const snakeKey = this.camelToSnakeCase(key);
if (Array.isArray(value)) {
return { ...allParams, [snakeKey]: value };
}
return { ...allParams, [snakeKey]: value };
}, {} as B);
}
}
@@ -0,0 +1,167 @@
import { ApiManager, ApiResponse } from "../apis/ApiManager";
import { BetaKey } from "../models/BetaKeys";
import { Pagination } from "../models/Pagination";
export class BetaKeysApi extends ApiManager {
public CreateBetaKey({
maybeNote,
maybeReferrerUsername,
numberOfKeys,
uuidIdempotencyToken,
}: {
maybeNote?: string;
maybeReferrerUsername?: string;
numberOfKeys: number;
uuidIdempotencyToken: string;
}): Promise<ApiResponse<string[]>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/beta_keys/create`;
const body = {
maybe_note: maybeNote,
maybe_referrer_username: maybeReferrerUsername,
number_of_keys: numberOfKeys,
uuid_idempotency_token: uuidIdempotencyToken,
};
return this.post<
{
maybe_note?: string;
maybe_referrer_username?: string;
number_of_keys: number;
uuid_idempotency_token: string;
},
{
success?: boolean;
beta_keys?: string[];
BadInput?: string;
}
>({ endpoint, body })
.then((response) => ({
success: response.success ?? false,
data: response.beta_keys,
errorMessage: response.BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public ListBetaKeys({
sortAscending,
pageSize,
pageIndex,
maybeReferrerUsername,
onlyListRemaining,
}: {
sortAscending?: boolean;
pageSize?: number;
pageIndex?: number;
maybeReferrerUsername?: string;
onlyListRemaining?: boolean;
}): Promise<ApiResponse<BetaKey[], Pagination>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/beta_keys/list`;
const query = {
sort_ascending: sortAscending,
page_size: pageSize,
page_index: pageIndex,
maybe_referrer_username: maybeReferrerUsername,
only_list_remaining: onlyListRemaining,
};
return this.get<{
success?: boolean;
beta_keys?: BetaKey[];
pagination?: Pagination;
BadInput?: string;
}>({ endpoint, query })
.then((response) => ({
success: response.success ?? false,
data: response.beta_keys,
pagination: response.pagination,
errorMessage: response.BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public RedeemBetaKey({
betaKey,
}: {
betaKey: string;
}): Promise<ApiResponse<undefined>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/beta_keys/redeem`;
return this.post<
{
beta_key: string;
},
{
success?: boolean;
BadInput?: string;
}
>({ endpoint, body: { beta_key: betaKey } })
.then((response) => ({
success: response.success ?? false,
errorMessage: response.BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public UpdateBetaKeyIsDistributed({
betaKey,
isDistributed,
}: {
betaKey: string;
isDistributed: boolean;
}): Promise<ApiResponse<undefined>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/beta_keys/${betaKey}/distributed`;
return this.post<
{
is_distributed: boolean;
},
{
success?: boolean;
BadInput?: string;
}
>({ endpoint, body: { is_distributed: isDistributed } })
.then((response) => ({
success: response.success ?? false,
errorMessage: response.BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public UpdateBetaKeyNote({
token,
note,
}: {
token: string;
note?: string;
}): Promise<ApiResponse<undefined>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/beta_keys/${token}/note`;
return this.post<
{
note?: string;
},
{
success?: boolean;
BadInput?: string;
}
>({ endpoint, body: { note } })
.then((response) => ({
success: response.success ?? false,
errorMessage: response.BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
}
@@ -0,0 +1,33 @@
import { ApiManager, ApiResponse } from "../apis/ApiManager";
import { Subscription } from "../models/Billing";
import { LoyaltyProgram } from "../enums/Billing";
export class BillingApi extends ApiManager {
public async ListActiveSubscriptions(): Promise<
ApiResponse<{
active_subscriptions: Subscription[];
maybe_loyalty_program?: LoyaltyProgram;
}>
> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/billing/active_subscriptions`;
return await this.get<{
success: boolean;
active_subscriptions?: Subscription[];
maybe_loyalty_program?: LoyaltyProgram;
error_message?: string;
}>({ endpoint: endpoint })
.then((response) => ({
success: response.success,
data: {
active_subscriptions: response.active_subscriptions || [],
maybe_loyalty_program: response.maybe_loyalty_program,
},
}))
.catch((err) => {
return {
success: false,
errorMessage: err.message,
};
});
}
}
@@ -0,0 +1,91 @@
import { ApiManager, ApiResponse } from "../apis/ApiManager";
export class CommentsApi extends ApiManager {
public CreateComment({
commentMarkdown,
entityToken,
entityType,
uuidIdempotencyToken,
}: {
commentMarkdown: string;
entityToken: string;
entityType: string;
uuidIdempotencyToken: string;
}): Promise<ApiResponse<string>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/comments/new`;
const body = {
comment_markdown: commentMarkdown,
entity_token: entityToken,
entity_type: entityType,
uuid_idempotency_token: uuidIdempotencyToken,
};
return this.post<
{
comment_markdown: string;
entity_token: string;
entity_type: string;
uuid_idempotency_token: string;
},
{
success?: boolean;
comment_token?: string;
BadInput?: string;
}
>({ endpoint, body })
.then((response) => ({
success: response.success ?? false,
data: response.comment_token,
errorMessage: response.BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public DeleteComment({
commentToken,
}: {
commentToken: string;
}): Promise<ApiResponse<undefined>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/comments/delete/${commentToken}`;
return this.post<
{ as_mod: boolean },
{
success?: boolean;
BadInput?: string;
}
>({ endpoint, body: { as_mod: true } })
.then((response) => ({
success: response.success ?? false,
errorMessage: response.BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public ListCommentsByEntity({
entityType,
entityToken,
}: {
entityType: string;
entityToken: string;
}): Promise<ApiResponse<string[]>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/comments/list/${entityType}/${entityToken}`;
return this.get<{
success: boolean;
comments: string[];
}>({ endpoint })
.then((response) => ({
success: true,
data: response.comments,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
}
@@ -0,0 +1,213 @@
import { ApiManager, ApiResponse } from "./ApiManager";
export class EngineApi extends ApiManager {
public async ConvertTbxToGltf({
mediaFileToken,
uuidIdempotencyToken,
}: {
mediaFileToken: string;
uuidIdempotencyToken: string;
}): Promise<ApiResponse<string>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/conversion/enqueue_fbx_to_gltf`;
const body = {
media_file_token: mediaFileToken,
uuid_idempotency_token: uuidIdempotencyToken,
};
return this.post<
{ media_file_token: string; uuid_idempotency_token: string },
{ success?: boolean; inference_job_token?: string; BadInput?: string }
>({
endpoint,
body: body,
})
.then((response) => {
return {
success: response.success ?? false,
data: response.inference_job_token,
errorMessage: response.BadInput,
};
})
.catch((err) => {
return {
success: false,
errorMessage: err.message,
};
});
}
public async uploadSceneSnapshot({
screenshot,
sceneMediaToken,
}: {
screenshot: File; // base64 encoded PNG
sceneMediaToken?: string;
}): Promise<ApiResponse<string>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/image_studio/scene_snapshot`;
const formData = new FormData();
formData.append("snapshot", screenshot); // Changed from "screenshot" to "snapshot" to match API spec
if (sceneMediaToken) {
formData.append("scene_media_token", sceneMediaToken);
}
// for now ...
const uuidIdempotencyToken = crypto.randomUUID(); // Generate a new UUID
formData.append("uuid_idempotency_token", uuidIdempotencyToken); // Added uuid_idempotency_token
const response = await fetch(endpoint, {
method: "POST",
headers: {
Accept: "application/json",
},
credentials: "include",
body: formData,
});
const postResponse = await response.json();
console.log(postResponse);
let result: { success: boolean; data?: string; errorMessage?: string };
if (postResponse.success) {
result = {
success: true,
data: postResponse.snapshot_media_token,
errorMessage: undefined,
};
} else {
result = {
success: false,
errorMessage: postResponse.BadInput,
};
}
return result;
}
public async enqueueImageGeneration({
disableSystemPrompt,
prompt,
snapshotMediaToken,
additionalImages,
}: {
disableSystemPrompt: boolean;
prompt: string;
snapshotMediaToken: string;
additionalImages?: string[];
}): Promise<ApiResponse<string>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/image_studio/prompt`;
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,
};
const postResponse = await this.post<
{
uuid_idempotency_token: string;
disable_system_prompt: boolean;
prompt: string;
snapshot_media_token: string;
additional_images?: string[];
},
{ success?: boolean; job_token?: string; BadInput?: string }
>({
endpoint,
body,
});
// Check if the response is successful
const isSuccess = postResponse.success ?? false;
// Prepare the result object
const result = {
success: isSuccess,
data: isSuccess ? postResponse.job_token : undefined,
errorMessage: isSuccess ? undefined : postResponse.BadInput,
};
return result;
}
public async pollJobSession(jobToken: string): Promise<
ApiResponse<{
status: string;
result: {
generated_images?: string[];
error?: string;
};
}>
> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/jobs/session/${jobToken}`;
const response = await this.get<{
success?: boolean;
status?: string;
result?: {
generated_images?: string[];
error?: string;
};
BadInput?: string;
}>({
endpoint,
});
const success = response.success ?? false;
const status = response.status ?? "";
const result = response.result ?? {
generated_images: [],
error: undefined,
};
const errorMessage = response.BadInput;
return {
success,
data: { status, result },
errorMessage,
};
}
public async pollStudioSessionJobs(jobToken: string): Promise<
ApiResponse<{
status: string;
result: {
generated_images?: string[];
error?: string;
};
}>
> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/image_studio/session_jobs/${jobToken}`;
const response = await this.get<{
success?: boolean;
status?: string;
result?: {
generated_images?: string[];
error?: string;
};
BadInput?: string;
}>({
endpoint,
});
const success = response.success ?? false;
const status = response.status ?? "";
const result = response.result ?? {
generated_images: [],
error: undefined,
};
const errorMessage = response.BadInput;
return {
success,
data: { status, result },
errorMessage,
};
}
}
@@ -0,0 +1,96 @@
import { ApiManager, ApiResponse } from "../apis/ApiManager";
export class FeaturedItemsApi extends ApiManager {
public CreateFeaturedItem({
entityToken,
entityType,
}: {
entityToken: string;
entityType: string;
}): Promise<ApiResponse<undefined>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/featured_item/create`;
const body = {
entity_token: entityToken,
entity_type: entityType,
};
return this.post<
{
entity_token: string;
entity_type: string;
},
{
success?: boolean;
BadInput?: string;
}
>({ endpoint, body })
.then((response) => ({
success: response.success ?? false,
errorMessage: response.BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public DeleteFeaturedItem({
entityToken,
entityType,
}: {
entityToken: string;
entityType: string;
}): Promise<ApiResponse<undefined>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/featured_item/delete`;
const body = {
entity_token: entityToken,
entity_type: entityType,
};
return this.delete<
{
entity_token: string;
entity_type: string;
},
{
success?: boolean;
BadInput?: string;
}
>({ endpoint, body })
.then((response) => ({
success: response.success ?? false,
errorMessage: response.BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public CheckFeaturedItem({
entityType,
entityToken,
}: {
entityType: string;
entityToken: string;
}): Promise<ApiResponse<boolean>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/featured_item/is_featured/${entityType}/${entityToken}`;
return this.post<
undefined,
{
success?: boolean;
is_featured?: boolean;
BadInput?: string;
}
>({ endpoint })
.then((response) => ({
success: response.success ?? false,
data: response.is_featured,
errorMessage: response.BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
}
@@ -0,0 +1,93 @@
// libs/api-client/src/JobsApi.ts
import { ApiManager, ApiResponse } from "../apis/ApiManager";
import { Job, JobPreview } from "../models/Job";
import { JobState } from "../models/JobState";
export class JobsApi extends ApiManager {
public GetPreviewStatusByJobToken({
token,
}: {
token: string;
}): Promise<ApiResponse<JobPreview>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/workflows/preview_status/${token}`;
return this.get<ApiResponse<JobPreview>>({ endpoint })
.then((response) => ({
success: response.success,
data: response.data,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public GetJobByToken({
token,
}: {
token: string;
}): Promise<ApiResponse<JobState>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/jobs/job/${token}`;
return this.get<{
success: boolean;
state: JobState;
}>({ endpoint })
.then((response) => ({
success: response.success,
data: response.state,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public ListJobs(): Promise<ApiResponse<JobState[]>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/jobs/batch`;
return this.get<{
success: boolean;
job_states: JobState[];
}>({ endpoint })
.then((response) => ({
success: response.success,
data: response.job_states,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public ListRecentJobs(): Promise<ApiResponse<Job[]>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/jobs/session`;
return this.get<{
success: boolean;
jobs: Job[];
error_reason?: string;
}>({ endpoint })
.then((response) => ({
success: response.success,
data: response.jobs,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public DeleteJobByToken(jobToken: string): Promise<ApiResponse<undefined>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/jobs/job/${jobToken}`;
return this.delete<
undefined,
{
success: boolean;
}
>({ endpoint })
.then((response) => ({
success: response.success,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
}
@@ -0,0 +1,346 @@
import { ApiManager, ApiResponse } from "../apis/ApiManager";
import { FilterEngineCategories } from "../enums/QueryFilters";
import { Visibility } from "../enums/Visibility";
export class MediaUploadApi extends ApiManager {
private sessionToken: string = "";
private async getSessionTokenForUploadStudioShot() {
const endpoint = `${this.ApiTargets.BaseApi}/v1/session_token`;
return await this.get<{
maybe_signed_session: string;
}>({ endpoint })
.then((response) => {
return {
success: true,
data: response["maybe_signed_session"],
};
})
.catch((err) => {
return {
success: false,
errorMessage: err.message,
};
});
}
public async UploadStudioShot({
maybe_title,
uuid_idempotency_token,
blob,
fileName,
maybe_visibility,
}: {
maybe_title?: string;
uuid_idempotency_token: string;
blob: Blob | null;
fileName: string;
maybe_visibility?: Visibility;
}): Promise<ApiResponse<string>> {
// This function is a bit special it requires that
const endpoint = `${this.ApiTargets.uploadApi}/v1/media_files/upload/studio_shot`;
if (!this.sessionToken) {
console.log("Session Token");
const response = await this.getSessionTokenForUploadStudioShot();
if (response.success && response.data) {
this.sessionToken = response.data;
console.log(`Token ${this.sessionToken}`);
} else {
return {
success: false,
errorMessage: "Could not obtain session tokens.",
};
}
} else {
console.log(`Session Token: ${this.sessionToken}`);
}
const options: Record<string, string | number | undefined> = {
maybe_title,
maybe_frame_rate_fps: 60,
maybe_visibility: maybe_visibility?.toString(),
};
const formRecord = Object.entries(options).reduce(
(allOptions, [key, value]) => {
if (value === undefined) {
return allOptions;
}
return { ...allOptions, [key]: value.toString() };
},
{} as Record<string, string>,
);
const formData = new FormData();
formData.append("uuid_idempotency_token", uuid_idempotency_token);
Object.entries(formRecord).forEach(([key, value]) => {
formData.append(key, value);
});
if (blob && fileName) {
formData.append("file", blob, fileName);
} else if (blob) {
formData.append("file", blob);
}
const response = await fetch(endpoint, {
method: "POST",
headers: {
Accept: "application/json",
Session: this.sessionToken,
},
credentials: "include",
body: formData,
});
const result = await response.json();
if (result.success && result.media_file_token) {
return { success: true, data: result.media_file_token };
} else {
return { success: false, errorMessage: "Failed to Get Media Token" };
}
}
private async Upload({
endpoint,
uuid,
blob,
fileName,
options,
}: {
endpoint: string;
blob: Blob | File;
fileName: string;
uuid: string;
options: Record<string, string | number | undefined>;
}): Promise<ApiResponse<string>> {
const formRecord = Object.entries(options).reduce(
(allOptions, [key, value]) => {
if (value === undefined) {
return allOptions;
}
return { ...allOptions, [key]: value.toString() };
},
{} as Record<string, string>,
);
return await this.postForm<{
success: boolean;
media_file_token?: string;
BadInput?: string;
}>({ endpoint, formRecord, blob, blobFileName: fileName, uuid })
.then((response) => ({
success: Boolean(response.success ?? false),
data: response.media_file_token,
errorMessage: response.BadInput,
}))
.catch((err) => {
return {
success: false,
errorMessage: err.message,
};
});
}
public async UploadAudio({
blob,
fileName,
uuid,
maybe_title,
maybe_visibility = Visibility.Public,
}: {
blob: Blob;
fileName: string;
uuid: string;
maybe_title?: string | undefined;
maybe_visibility?: Visibility | undefined;
}): Promise<ApiResponse<string>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/audio`;
const options: Record<string, string | number | undefined> = {
maybe_title,
maybe_visibility: maybe_visibility?.toString(),
};
return this.Upload({ endpoint, blob, fileName, uuid, options });
}
public async UploadImage({
blob,
fileName,
uuid,
maybe_title,
maybe_visibility = Visibility.Public,
is_intermediate_system_file = EIntermediateFile.true
}: {
blob: Blob;
fileName: string;
uuid: string;
maybe_title?: string | undefined;
maybe_visibility?: Visibility | undefined;
is_intermediate_system_file?: EIntermediateFile;
}): Promise<ApiResponse<string>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/image`;
const options: Record<string, string | number | undefined> = {
is_intermediate_system_file: EIntermediateFile[is_intermediate_system_file],
maybe_title,
maybe_visibility: maybe_visibility?.toString(),
};
return this.Upload({ endpoint, blob, fileName, uuid, options });
}
public async UploadNewEngineAsset({
file,
fileName,
uuid,
engine_category,
maybe_animation_type,
maybe_duration_millis,
maybe_title,
maybe_visibility = Visibility.Public,
}: {
file: File;
fileName: string;
uuid: string;
engine_category: FilterEngineCategories;
maybe_animation_type?: string;
maybe_duration_millis?: number;
maybe_title?: string;
maybe_visibility?: Visibility;
}): Promise<ApiResponse<string>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/new_engine_asset`;
const options: Record<string, string | number | undefined> = {
engine_category,
maybe_title,
maybe_visibility: maybe_visibility?.toString(),
maybe_animation_type,
maybe_duration_millis,
};
return this.Upload({ endpoint, blob: file, fileName, uuid, options });
}
public async UploadNewScene({
blob,
fileName,
uuid,
maybe_title,
maybe_visibility = Visibility.Public,
}: {
blob: Blob;
fileName: string;
uuid: string;
maybe_title?: string;
maybe_visibility?: Visibility;
}): Promise<ApiResponse<string>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/new_scene`;
const options: Record<string, string | number | undefined> = {
maybe_title,
maybe_visibility: maybe_visibility?.toString(),
};
return this.Upload({ endpoint, blob, fileName, uuid, options });
}
public async UploadNewVideo({
blob,
fileName,
uuid,
maybe_title,
maybe_visibility = Visibility.Public,
maybe_style_name,
maybe_scene_source_media_file_token,
}: {
blob: Blob;
fileName: string;
uuid: string;
maybe_title?: string;
maybe_visibility?: Visibility;
maybe_style_name?: string;
maybe_scene_source_media_file_token?: string;
}): Promise<ApiResponse<string>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/new_video`;
const options: Record<string, string | number | undefined> = {
is_intermediate_system_file: "true",
maybe_title,
maybe_visibility: maybe_visibility?.toString(),
maybe_style_name,
maybe_scene_source_media_file_token,
};
return this.Upload({ endpoint, blob, fileName, uuid, options });
}
public async UploadPmx({
file,
fileName,
uuid,
engine_category,
maybe_animation_type,
maybe_duration_millis,
maybe_title,
maybe_visibility = Visibility.Public,
}: {
file: File;
fileName: string;
uuid: string;
engine_category?: string;
maybe_animation_type?: string;
maybe_duration_millis?: number;
maybe_title?: string;
maybe_visibility?: Visibility;
}): Promise<ApiResponse<string>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/pmx`;
const options: Record<string, string | number | undefined> = {
is_intermediate_system_file: "true",
engine_category,
maybe_animation_type,
maybe_duration_millis,
maybe_title,
maybe_visibility,
};
return this.Upload({ endpoint, blob: file, fileName, uuid, options });
}
public async UploadSavedScene({
blob,
fileName,
uuid,
mediaToken,
}: {
blob: Blob;
fileName: string;
uuid: string;
mediaToken: string;
}): Promise<ApiResponse<string>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/saved_scene/${mediaToken}`;
const options: Record<string, string | number | undefined> = {};
return this.Upload({ endpoint, blob, fileName, uuid, options });
}
// this is used to send the scene snapshot to the vst upload endpoint this creates a snapshot of the scene where in we can remix from the og.
public async UploadSceneSnapshotMediaFileForm({
blob,
uuid,
maybe_title, // title of the scene at the time
maybe_scene_source_media_file_token, // original token that started it all
}: {
blob: Blob;
uuid: string;
maybe_title: string;
maybe_scene_source_media_file_token: string | undefined;
}): Promise<ApiResponse<string>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/scene_snapshot`;
const options: Record<string, string | number | undefined> = {
maybe_title: maybe_title,
maybe_scene_source_media_file_token: maybe_scene_source_media_file_token,
};
const fileName = `${maybe_title}-${uuid}`;
return this.Upload({ endpoint, blob, fileName, uuid, options }); // token comes back and send it VST
}
}
@@ -0,0 +1,26 @@
import { ApiManager, ApiResponse } from "../apis/ApiManager";
import { StatusAlert } from "../models/StatusAlert";
export class MiscApi extends ApiManager {
public GetStatusAlertCheck(): Promise<ApiResponse<StatusAlert>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/session`;
return this.get<{
success: boolean;
maybe_alert?: Omit<StatusAlert, "refresh_interval_millis">;
refresh_interval_millis: number;
}>({ endpoint: endpoint })
.then((response) => ({
success: response.success,
data: {
...response.maybe_alert,
refresh_interval_millis: response.refresh_interval_millis,
},
}))
.catch((err) => {
return {
success: false,
errorMessage: err.mesasge,
};
});
}
}
@@ -0,0 +1,42 @@
import { ApiManager, ApiResponse } from "../apis/ApiManager";
export class ModerationApi extends ApiManager {
public async UpdateFeatureFlagsByToken({
usernameOrToken,
flags,
}: {
usernameOrToken: string;
flags: string[];
}): Promise<
ApiResponse<{
inference_job_token?: string;
inference_job_token_type?: string;
}>
> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/moderation/user_feature_flags/${usernameOrToken}`;
const body = {
action: {
AddFlags: {
flags,
},
},
};
return this.post<
{ action: { AddFlags: { flags: string[] } } },
{ success?: boolean; BadInput?: string }
>({
endpoint,
body: body,
})
.then((response) => ({
success: response.success ?? false,
errorMessage: response.BadInput,
}))
.catch((err) => ({
success: false,
errorMessage: err.message,
}));
}
}
@@ -0,0 +1,25 @@
import { ApiManager, ApiResponse } from "../apis/ApiManager";
import { Prompts } from "../models/Prompts";
export class PromptsApi extends ApiManager {
public GetPromptsByToken({
token,
}: {
token: string;
}): Promise<ApiResponse<Prompts>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/prompts/${token}`;
return this.get<{
success: boolean;
prompt: Prompts;
error_reason?: string;
}>({ endpoint })
.then((response) => ({
success: response.success,
data: response.prompt,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
}
@@ -0,0 +1,84 @@
import { ApiManager } from "../apis/ApiManager";
import { v4 as uuidv4 } from "uuid";
export interface Coordinates {
coordinates: [number, number];
include: boolean;
}
export interface ObjectData {
style: {
color: SegmentationColor;
};
object_id: number;
points: Coordinates[];
}
export type SegmentationColor = [number, number, number];
export interface RequestFrame {
timestamp: number;
objects: ObjectData[];
}
export interface ResponseFrame {
b64_image_data: number;
idx: number;
timestamp: number;
objects: ObjectData[];
preview_image_url: string;
}
export interface SegmentationRequest {
session_id: string;
fps: number;
frames: RequestFrame[];
propagate: boolean;
}
export interface SegmentationResponse {
session_id: string;
fps: number;
masked_video_cdn_url: string;
frames: ResponseFrame[];
propagate: boolean;
}
export class SegmentationApi extends ApiManager {
// returns uuid
public async createSession(
blobVideo: File | Blob,
): Promise<{ session_id: string }> {
const endpoint = `https://hax.storyteller.ai/segmentation/new_session`;
const id = uuidv4().toString();
return this.postFormVideo<{ session_id: "fix-mimetype" }>({
endpoint: endpoint,
formRecord: {
session_id: "fix-mimetype",
},
uuid: id,
blobFileName: id,
blob: blobVideo,
});
}
public addPointsToSession(
session_id: string,
fps: number,
frames: RequestFrame[],
propagate: boolean,
): Promise<SegmentationResponse> {
const endpoint = `https://hax.storyteller.ai/segmentation/generate_masks`;
const segmentationRequest: SegmentationRequest = {
session_id: session_id,
fps: fps,
frames: frames,
propagate: propagate,
};
return this.post<SegmentationRequest, SegmentationResponse>({
endpoint: endpoint,
body: segmentationRequest,
});
}
}
@@ -0,0 +1,64 @@
import { ApiManager, ApiResponse } from "../apis/ApiManager";
import { GenerateTtsAudioErrorType } from "../enums/Tts";
import { Visibility } from "../enums/Visibility";
export interface GenerateTtsAudioRequest {
creator_set_visibility?: Visibility;
uuid_idempotency_token: string;
tts_model_token: string;
inference_text: string;
is_storyteller_demo?: boolean;
}
export interface GenerateTtsAudioResponse {
success?: boolean;
inference_job_token?: string;
inference_job_token_type?: string;
BadInput?: string;
}
export class TtsApi extends ApiManager {
public async GenerateTtsAudio(request: GenerateTtsAudioRequest): Promise<
ApiResponse<{
inference_job_token?: string;
inference_job_token_type?: string;
}>
> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/tts/inference`;
const body = {
...request,
...(request.creator_set_visibility
? {}
: { creator_set_visibility: Visibility.Public }),
};
return this.post<GenerateTtsAudioRequest, GenerateTtsAudioResponse>({
endpoint,
body: body,
})
.then((response) => {
if (!response.success) {
return {
success: false,
errorMessage: response.BadInput,
data: {},
};
}
return {
success: true,
data: {
inference_job_token: response.inference_job_token,
inference_job_token_type: response.inference_job_token_type,
},
};
})
.catch(() => {
return {
success: false,
errorMessage: GenerateTtsAudioErrorType.UnknownError,
data: {},
};
});
}
}
@@ -0,0 +1,201 @@
import { ApiManager, ApiResponse } from "../apis/ApiManager";
import {
UserBookmarkBatch,
UserBookmarkByEntity,
UserBookmarkByUser,
} from "../models/UserBookmark";
import { Pagination } from "../models/Pagination";
export enum ScopedEntityTypes {
USER = "user",
TTS_MODEL = "tts-model",
TTS_RESULT = "tts-result",
W2L_TEMPLATE = "w2l-template",
W2L_RESULT = "w2l-result",
MEDIA_FILE = "media_file",
MODEL_WEIGHT = "model_weight",
VOICE_CONVERSION_MODEL = "voice_conversion_model",
ZS_VOICE = "z_voice",
}
export enum ScopedWeightTypes {
HIFIGAN_TTl2 = "hifigan_tt2",
RVC_V2 = "rvc_v2",
SD_1_5 = "sd_1.5",
SDXL = "sdxl",
SO_VITS_SVC = "so_vits_svc",
TT2 = "tt2",
LORA = "loRA",
VALL_E = "vall_e",
COMFY_UI = "comfy_ui",
}
export enum ScopedMediaFileType {
IMAGE_GENERATION = "image_generation",
TEXT_TO_SPEECH = "text_to_speech",
VOCODER = "vocoder",
VOICE_CONVERSION = "voice_conversion",
WORKFLOW_CONFIG = "workflow_config",
}
interface ListUserBookmarksByUserRequest {
username: string;
sort_ascending?: boolean;
page_size?: number;
page_index?: number;
maybe_scoped_entity_type?: ScopedEntityTypes[];
maybe_scoped_weight_type?: ScopedWeightTypes[];
maybe_scoped_media_file_type?: ScopedMediaFileType[];
}
export class UserBookmarksApi extends ApiManager {
public CreateUserBookmark({
entityToken,
entityType,
}: {
entityToken: string;
entityType: string;
}): Promise<
ApiResponse<{
new_bookmark_count_for_entity?: number;
user_bookmark_token?: string;
}>
> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/user_bookmarks/create`;
const body = {
entity_token: entityToken,
entity_type: entityType,
};
return this.post<
{
entity_token: string;
entity_type: string;
},
{
success?: boolean;
new_bookmark_count_for_entity?: number;
user_bookmark_token: string;
BadInput?: string;
}
>({ endpoint, body })
.then((response) => ({
success: response.success ?? false,
data: {
new_bookmark_count_for_entity: response.new_bookmark_count_for_entity,
user_bookmark_token: response.user_bookmark_token,
},
errorMessage: response.BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public DeleteUserBookmark({
entityToken,
}: {
entityToken: string;
}): Promise<ApiResponse<undefined>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/user_bookmarks/delete/${entityToken}`;
return this.delete<
{ as_mod: boolean },
{
success?: boolean;
BadInput?: string;
}
>({ endpoint, body: { as_mod: true } })
.then((response) => ({
success: response.success ?? false,
errorMessage: response.BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public ListUserBookmarks(): Promise<ApiResponse<UserBookmarkBatch[]>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/user_bookmarks/batch`;
return this.get<{
success?: boolean;
bookmarks?: UserBookmarkBatch[];
BadInput?: string;
}>({ endpoint })
.then((response) => ({
success: response.success ?? false,
data: response.bookmarks,
errorMessage: response.BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public ListUserBookmarksByUser({
username,
sort_ascending,
page_size,
page_index,
maybe_scoped_entity_type,
maybe_scoped_weight_type,
maybe_scoped_media_file_type,
}: ListUserBookmarksByUserRequest): Promise<
ApiResponse<UserBookmarkByUser[], Pagination>
> {
const user = username;
const endpoint = `${this.ApiTargets.BaseApi}/v1/user_bookmarks/list/${user}`;
const query = {
sort_ascending,
page_size,
page_index,
maybe_scoped_entity_type: maybe_scoped_entity_type
? maybe_scoped_entity_type.join(",")
: undefined,
maybe_scoped_weight_type: maybe_scoped_weight_type
? maybe_scoped_weight_type.join(",")
: undefined,
maybe_scoped_media_file_type: maybe_scoped_media_file_type
? maybe_scoped_media_file_type.join(",")
: undefined,
};
return this.get<{
success?: boolean;
pagination?: Pagination;
results: UserBookmarkByUser[];
}>({ endpoint, query })
.then((response) => ({
success: response.success ?? false,
data: response.results,
pagination: response.pagination,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public ListUserBookmarksByEntity({
entityType,
entityToken,
}: {
entityType: string;
entityToken: string;
}): Promise<ApiResponse<UserBookmarkByEntity[]>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/user_bookmarks/list/${entityType}/${entityToken}`;
return this.get<{
success: boolean;
user_bookmarks: UserBookmarkByEntity[];
}>({ endpoint })
.then((response) => ({
success: true,
data: response.user_bookmarks,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
}
@@ -0,0 +1,92 @@
import { ApiManager, ApiResponse } from "../apis/ApiManager";
export interface UserRating {
entity_token: string;
entity_type: string;
rating_value: string;
}
export class UserRatingApi extends ApiManager {
public ListUserRatings(): Promise<ApiResponse<UserRating[]>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/user_rating/batch`;
return this.get<{
success?: boolean;
ratings?: UserRating[];
BadInput?: string;
}>({ endpoint })
.then((response) => ({
success: response.success ?? false,
data: response.ratings,
errorMessage: response.BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public PostUserRating({
entityToken,
entityType,
ratingValue,
}: {
entityToken: string;
entityType: string;
ratingValue: string;
}): Promise<ApiResponse<number>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/user_rating/rate`;
const body = {
entity_token: entityToken,
entity_type: entityType,
rating_value: ratingValue,
};
return this.post<
{
entity_token: string;
entity_type: string;
rating_value: string;
},
{
success?: boolean;
new_positive_rating_count_for_entity?: number;
BadInput?: string;
}
>({ endpoint, body })
.then((response) => ({
success: response.success ?? false,
data: response.new_positive_rating_count_for_entity,
errorMessage: response.BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public ListUserRatingByEntity({
entityType,
entityToken,
}: {
entityType: string;
entityToken: string;
}): Promise<ApiResponse<string>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/user_rating/view/${entityType}/${entityToken}`;
return this.get<{
success?: boolean;
maybe_rating_value?: string;
BadInput?: string;
}>({ endpoint })
.then((response) => {
return {
success: response.success ?? false,
data: response.maybe_rating_value,
errorMessage: response.BadInput,
};
})
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
}
@@ -0,0 +1,161 @@
import { ApiManager, ApiResponse } from "../apis/ApiManager";
import { UserInfo } from "../models/Users";
export class UsersApi extends ApiManager {
public GetSession(): Promise<
ApiResponse<{
loggedIn: boolean;
user?: UserInfo;
}>
> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/session`;
return this.get<{
success: boolean;
logged_in: boolean;
user?: UserInfo;
error_message?: string;
}>({ endpoint: endpoint })
.then((response) => ({
success: response.success,
data: {
loggedIn: response.logged_in,
user: response.user,
},
}))
.catch((err) => {
return {
success: false,
errorMessage: err.mesasge,
};
});
}
public GetUserProfile(username: string): Promise<
ApiResponse<{
user?: UserInfo;
}>
> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/user/${username}/profile`;
return this.get<{
success: boolean;
user?: UserInfo;
error_message?: string;
}>({ endpoint: endpoint })
.then((response) => ({
success: response.success,
data: {
user: response.user,
},
}))
.catch((err) => {
return {
success: false,
error_message: err.message,
};
});
}
public async Login({
usernameOrEmail,
password,
}: {
usernameOrEmail: string;
password: string;
}): Promise<ApiResponse<{ signedSession?: string }>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/login`;
const body = {
username_or_email: usernameOrEmail,
password: password,
};
return await this.post<
{ username_or_email: string; password: string },
{
success: boolean;
signed_session?: string;
error_message?: string;
error_type?: string;
}
>({
endpoint: endpoint,
body: body,
})
.then((response) => ({
success: response.success,
data: { signedSession: response.signed_session },
errorMessage: response.error_message,
}))
.catch((err) => {
return {
success: false,
error_message: err.message,
};
});
}
public Logout(): Promise<ApiResponse<null>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/logout`;
return this.post<null, { success: boolean; error_message?: string }>({
endpoint: endpoint,
})
.then((response) => ({
success: response.success,
}))
.catch((err) => {
return {
success: false,
errorMessage: err.message,
};
});
}
public async Signup({
username,
email,
password,
passwordConfirmation,
}: {
username: string;
email: string;
password: string;
passwordConfirmation: string;
}): Promise<ApiResponse<{ signedSession?: string }>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/create_account`;
const body = {
email_address: email,
password,
password_confirmation: passwordConfirmation,
username,
};
return await this.post<
{
username: string;
email_address: string;
password: string;
password_confirmation: string;
},
{
success: boolean;
signed_session?: string;
error_fields?: Record<string, string>;
error_message?: string;
error_type?: string;
}
>({
endpoint: endpoint,
body: body,
})
.then((response) => {
return {
success: response.success,
data: { signedSession: response.signed_session },
errorMessage: Object.values(response.error_fields ?? {}).join(","),
};
})
.catch((err) => {
return {
success: false,
error_message: err.message,
};
});
}
}
@@ -0,0 +1,70 @@
import { ApiManager, ApiResponse } from "../apis/ApiManager";
import { Visibility } from "../enums/Visibility";
interface GenerateVideoStyleTransferRequest {
creator_set_visibility: string;
disable_lcm: boolean;
enable_lipsync: boolean;
frame_skip: number;
global_ipa_media_token: string;
input_depth_file: string;
input_file: string;
input_normal_file: string;
input_outline_file: string;
negative_prompt: string;
prompt: string;
remove_watermark: boolean;
style: string;
travel_prompt: string;
trim_end_millis: number;
trim_start_millis: number;
use_cinematic: boolean;
use_face_detailer: boolean;
use_strength: number;
use_upscaler: boolean;
uuid_idempotency_token: string;
}
export class VideoApi extends ApiManager {
public async EnqueueStudio({
enqueueVideo,
}: {
enqueueVideo: GenerateVideoStyleTransferRequest;
}): Promise<
ApiResponse<{
inference_job_token?: string;
inference_job_token_type?: string;
}>
> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/workflows/enqueue_studio`;
const body = {
...enqueueVideo,
...(enqueueVideo.creator_set_visibility
? {}
: { creator_set_visibility: Visibility.Public }),
};
return await this.post<
GenerateVideoStyleTransferRequest,
{
success?: boolean;
inference_job_token?: string;
inference_job_token_type?: string;
BadInput?: string;
}
>({ endpoint, body: body })
.then((response) => ({
success: Boolean(response.success ?? false),
data: {
inference_job_token: response.inference_job_token,
inference_job_token_type: response.inference_job_token_type,
},
errorMessage: response.BadInput,
}))
.catch((err) => {
console.log(err.message);
return { success: false, error_reason: err.message };
});
}
}

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