Compare commits

...
Author SHA1 Message Date
bflatastic 3860f784a3 shared toaster component 2025-04-18 14:57:07 +07:00
BFlat e31a2c0a91 fix nx project discoverability (#527) 2025-04-18 14:10:40 +07:00
Kasisnu ea85abdc62 image-gen: make unused bools in task response optional 2025-04-18 10:34:52 +05:30
Brandon Thomas 4f3c93635f don't log debug level in prod 2025-04-18 00:07:51 -04:00
ArEnScandMichael Chung 82b9b90ff9 Added the apis to the api folder it loads correctly now (#525)
Co-authored-by: Michael Chung <michaelchung@michaels-mbp-2.home>
2025-04-17 22:49:27 -04:00
bflatastic ad5ea26a81 commented out test component it was pushing the ui down 2025-04-18 08:59:53 +07:00
bflatastic 6121d118f0 reduced topbar right side gap 2025-04-18 08:54:40 +07:00
bflatastic 28ca7be96c activity update 2025-04-18 08:50:55 +07:00
Brandon Thomas a2a7f7d274 could also be a bearer token issue 2025-04-17 21:39:45 -04:00
Brandon Thomas e1255a55a0 specific error message for cookie expiration 2025-04-17 21:30:16 -04:00
9f629ee5e2 Nx working test (#524)
It works now.

---------

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

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

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

**gallery (library modal):**
- use the new lightbox component
- the new lightbox has a download button
- use cdn images
2025-04-17 02:48:25 +07:00
Kasisnu ec3ac242b0 image-gen: generate sentinel token in rust 2025-04-16 16:19:32 +05:30
Brandon Thomas fbe7fa3c77 don't leak logged in session jobs when logging out 2025-04-15 23:26:58 -04:00
Brandon Thomas 418d824c35 session jobs for anonymous users 2025-04-15 22:43:53 -04:00
Tanish Manku 2e6823834f Create the shapes centered on the canvas (#515)
- Also match all the shape sizes
2025-04-15 17:24:29 -07:00
120 changed files with 6332 additions and 548 deletions
@@ -83,16 +83,16 @@ pub struct Generation {
pub created_at: String,
pub deleted_at: Option<String>,
pub url: String,
pub seed: i64,
pub can_download: bool,
pub download_status: String,
pub is_favorite: bool,
pub is_liked: bool,
pub is_public: bool,
pub is_archived: bool,
pub is_featured: bool,
pub has_feedback: bool,
pub like_count: i32,
pub seed: Option<i64>,
pub can_download: Option<bool>,
pub download_status: Option<String>,
pub is_favorite: Option<bool>,
pub is_liked: Option<bool>,
pub is_public: Option<bool>,
pub is_archived: Option<bool>,
pub is_featured: Option<bool>,
pub has_feedback: Option<bool>,
pub like_count: Option<i32>,
pub cloudflare_metadata: Option<serde_json::Value>,
pub cf_thumbnail_url: Option<String>,
pub encodings: Encodings,
@@ -2,4 +2,5 @@ pub mod credentials;
pub mod image_gen;
pub mod job;
pub mod upload;
pub mod sentinel_refresh;
pub mod sentinel_refresh;
mod sora_error;
@@ -668,8 +668,4 @@ pub fn get_random_document_key() -> String {
pub fn get_random_window_key() -> String {
return WINDOW_KEY[rand::rng().random_range(0..WINDOW_KEY.len())].to_string();
}
pub fn get_random_cached_script() -> String {
return CACHED_SCRIPTS[rand::rng().random_range(0..CACHED_SCRIPTS.len())].to_string();
}
}
@@ -48,7 +48,6 @@ pub struct GenerateSentinelRefreshRequest {
fn generate_date() -> String {
let now_utc = Utc::now();
let today_utc = now_utc.date_naive();
let offset = FixedOffset::west_opt(5 * 60 * 60).unwrap();
let now_with_offset = Utc::now().with_timezone(&offset);
@@ -3,6 +3,8 @@ 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.
@@ -39,8 +41,9 @@ pub struct SentinelResponse {
}
pub async fn generate_token(problem: String) -> AnyhowResult<String> {
let request = SentinelRequest::new(problem);
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)
@@ -57,12 +60,10 @@ pub async fn generate_token(problem: String) -> AnyhowResult<String> {
#[cfg(test)]
mod tests {
use super::*;
use crate::sentinel_refresh::generate::request::GenerateSentinelRefreshRequest;
#[tokio::test]
async fn test_generate_token() {
let (_request, base64_request) = GenerateSentinelRefreshRequest::new().with_fourth_and_tenth();
let token = generate_token(base64_request.to_string()).await.unwrap();
let token = generate_token().await.unwrap();
println!("{}", token);
}
}
@@ -0,0 +1,48 @@
use std::error::Error;
use std::fmt::{Display, Formatter};
#[derive(Debug)]
pub enum SoraError {
/// Unauthorized, cookie and/or bearer token expired. We'll need to ask for a refreshed login.
///
/// Example message, e.g. from the upload endpoint:
/// {
/// "error": {
/// "message": "Your authentication token has expired. Please try signing in again.",
/// "type": "invalid_request_error",
/// "param": null,
/// "code": "token_expired"
/// }
/// }
UnauthorizedCookieOrBearerExpired,
/// Another error occurred.
OtherBadStatus(anyhow::Error),
/// Reqwest Error
ReqwestError(reqwest::Error),
}
impl Error for SoraError {}
impl Display for SoraError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
SoraError::UnauthorizedCookieOrBearerExpired => {
write!(f, "Unauthorized: cookie and/or bearer token expired")
}
SoraError::OtherBadStatus(err) => {
write!(f, "Other error: {}", err)
}
SoraError::ReqwestError(err) => {
write!(f, "Reqwest error: {}", err)
}
}
}
}
impl From<reqwest::Error> for SoraError {
fn from(err: reqwest::Error) -> SoraError {
SoraError::ReqwestError(err)
}
}
@@ -3,6 +3,7 @@ use errors::AnyhowResult;
use reqwest::multipart::{Form, Part};
use reqwest::Client;
use serde::Deserialize;
use crate::sora_error::SoraError;
const SORA_UPLOAD_MEDIA_URL: &str = "https://sora.com/backend/uploads";
@@ -25,6 +26,17 @@ Response:
"size_bytes": 118501,
"thumbnail_url": "https://videos.openai.com/vg-assets/assets%2Fclient_upload%2Fmedia%2Fuser-9wf6JFdgRSJLjvSJ53LNAbV8%2Fmedia_01jqt9vt20erx9zvryf3v1pecx.jpg?st=2025-04-02T02%3A39%3A01Z&se=2025-04-08T03%3A39%3A01Z&sks=b&skt=2025-04-02T02%3A39%3A01Z&ske=2025-04-08T03%3A39%3A01Z&sktid=a48cca56-e6da-484e-a814-9c849652bcb3&skoid=3d249c53-07fa-4ba4-9b65-0bf8eb4ea46a&skv=2019-02-02&sv=2018-11-09&sr=b&sp=r&spr=https%2Chttp&sig=9aU9xKI0BkhRtrTb0RD05%2BT6prBnmXk1I7nV2nTsFbY%3D&az=oaivgprodscus"
}
Error response:
Failed to upload scene media to Sora: Upload failed with status 401 Unauthorized: {
"error": {
"message": "Your authentication token has expired. Please try signing in again.",
"type": "invalid_request_error",
"param": null,
"code": "token_expired"
}
}
*/
#[derive(Debug, Deserialize)]
@@ -50,7 +62,13 @@ pub (crate) struct SoraMediaUploadRequest<'a> {
pub credentials: &'a SoraCredentials,
}
pub (crate) async fn upload_media_http_request(file_bytes: Vec<u8>, filename: String, mime_type: &str, credentials: &SoraCredentials) -> AnyhowResult<SoraMediaUploadResponse> {
pub (crate) async fn upload_media_http_request(
file_bytes: Vec<u8>,
filename: String,
mime_type: &str,
credentials: &SoraCredentials,
) -> Result<SoraMediaUploadResponse, SoraError> {
// Create multipart form
let part = Part::bytes(file_bytes) // NB: Reqwest needs to own the bytes.
.file_name(filename) // NB: Reqwest needs to own the bytes
@@ -70,12 +88,29 @@ pub (crate) async fn upload_media_http_request(file_bytes: Vec<u8>, filename: St
// Check response status
if !response.status().is_success() {
let status = response.status();
let text = response.text().await?;
return Err(anyhow::anyhow!("Upload failed with status {}: {}", status, text));
let error = classify_error(response).await;
return Err(error);
}
// Parse response
let upload_response = response.json::<SoraMediaUploadResponse>().await?;
Ok(upload_response)
}
async fn classify_error(response: reqwest::Response) -> SoraError {
let status = response.status();
let message = match response.text().await {
Ok(text) => text,
Err(err) => return SoraError::ReqwestError(err),
};
let cookie_expired =
message.contains("Your authentication token has expired. Please try signing in again.")
|| message.contains("token_expired");
if cookie_expired {
SoraError::UnauthorizedCookieOrBearerExpired
} else {
SoraError::OtherBadStatus(anyhow::anyhow!("Upload failed with status {}: {}", status, message))
}
}
@@ -4,7 +4,6 @@
/// NB: `sqlx::query` is spammy and dumps all queries as "info"-level log lines.
/// NB: `hyper::proto::h1::io` is incredibly spammy and logs every chunk of bytes in very large files being downloaded.
pub const DEFAULT_RUST_LOG: &str = concat!(
"debug,",
"actix_web=info,",
"sqlx::query=warn,",
"hyper::proto::h1::io=warn,",
@@ -21,9 +21,14 @@ use crate::helpers::boolean_converters::i8_to_bool;
use crate::payloads::generic_inference_args::generic_inference_args::{GenericInferenceArgs, PolymorphicInferenceArgs};
use crate::queries::generic_inference::web::job_status::{GenericInferenceJobStatus, RequestDetails, ResultDetails, UserDetails};
pub enum SessionUser<'a> {
User(&'a UserToken),
Anonymous(&'a AnonymousVisitorTrackingToken),
}
pub struct ListSessionJobsForUserArgs<'a> {
// TODO(bt,2024-04-23): Support AnonymousVisitorToken
pub user_token: &'a UserToken,
pub user: SessionUser<'a>,
pub maybe_include_job_statuses: Option<&'a HashSet<JobStatusPlus>>,
pub maybe_exclude_job_statuses: Option<&'a HashSet<JobStatusPlus>>,
}
@@ -63,14 +68,29 @@ pub async fn list_session_jobs_from_connection(
// Furthermore, even though the join query's `created_at` lacks an index, sort
// by id will accomplish this quickly.
{
query_builder.push(r#"
INNER JOIN (
SELECT id
FROM generic_inference_jobs
WHERE maybe_creator_user_token =
"#);
query_builder.push_bind(args.user_token.to_string());
match args.user {
SessionUser::User(user_token) => {
query_builder.push(r#"
INNER JOIN (
SELECT id
FROM generic_inference_jobs
WHERE maybe_creator_user_token =
"#);
query_builder.push_bind(user_token.to_string());
}
SessionUser::Anonymous(avt_token) => {
query_builder.push(r#"
INNER JOIN (
SELECT id
FROM generic_inference_jobs
WHERE maybe_creator_anonymous_visitor_token =
"#);
query_builder.push_bind(avt_token.to_string());
query_builder.push(r#"
AND maybe_creator_user_token IS NULL
"#);
}
}
query_builder.push(r#"
AND created_at > DATE_SUB(NOW(), INTERVAL 36 HOUR)
@@ -16,9 +16,12 @@ use log::warn;
use mysql_queries::queries::media_files::get::get_media_file::get_media_file;
use openai_sora_client::credentials::SoraCredentials;
use openai_sora_client::image_gen::SoraError;
use openai_sora_client::sentinel_refresh::generate::token::generate_token;
use openai_sora_client::sentinel_refresh::refresh_sentinel;
use serde::Deserialize;
use serde::Serialize;
use shared_service_components::sora_redis_credentials::set_sora_credential_field_in_redis::set_sora_credential_field_in_redis;
use shared_service_components::sora_redis_credentials::keys::RedisSoraCredentialSubkey;
use sqlx::MySqlPool;
use tempdir::TempDir;
use utoipa::ToSchema;
@@ -97,6 +100,7 @@ pub enum EnqueueImageGenRequestError {
NotAuthorized,
ServerError,
RateLimited,
TooManyConcurrentTasks,
}
impl ResponseError for EnqueueImageGenRequestError {
@@ -106,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,
}
}
@@ -115,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())
@@ -288,12 +294,12 @@ pub async fn enqueue_studio_image_generation_handler(http_request: HttpRequest,
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
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);
@@ -301,27 +307,54 @@ pub async fn enqueue_studio_image_generation_handler(http_request: HttpRequest,
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
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);
}
}
}
}
}
@@ -330,27 +363,33 @@ 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
@@ -370,8 +409,8 @@ 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),
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(),
@@ -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(),
};
+17
View File
@@ -27,4 +27,21 @@ Run dev server:
nx dev editor3d
```
```
call when starting on main or a new branch:
./clean_modules
The names for @frontend and @storyteller come from the package.json file in the libs folder.
import { Login } from "@frontend/login";
import { api } from "@storyteller/api";
building:
npx nx build editor3d
npx nx build editor2d
dev:
npx nx dev editor3d
npx nx dev editor2d
```
+8 -1
View File
@@ -4,7 +4,14 @@
"version": "0.0.0",
"type": "module",
"nx": {
"name": "editor2d"
"name": "editor2d",
"targets": {
"build": {
"dependsOn": [
"^build"
]
}
}
},
"scripts": {
"dev": "echo 'editor2d' && vite",
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

@@ -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);
@@ -41,9 +41,9 @@ export const ToolbarTopLeft = () => {
)}
>
<img
src="/brand/mira-logo.png"
src="/brand/artcraft-logo.png"
alt="logo"
className="h-[30px] select-none pb-1"
className="h-[28px] select-none"
/>
<TabSelector
@@ -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,
@@ -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"
aria-label="navigation"
>
<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"
/>
@@ -52,7 +52,7 @@ export const TopBar = ({ pageName }: Props) => {
<SceneTitleInput pageName={pageName} />
</div>
<div className="flex justify-end gap-5">
<div className="flex justify-end gap-3.5">
<div className="flex gap-2">
<Button
variant="secondary"
@@ -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();
@@ -351,6 +359,17 @@ export class MouseControls {
} else {
this.cameraViewControls.movementSpeed = 1.15;
}
if (event.key === "Escape") {
if (poseMode.value === "pose") {
this.toggleFKMode();
return;
} else if (this.selected && this.selected.length > 0) {
this.removeTransformControls();
hideObjectPanel();
showPoseControls.value = false;
}
}
}
// Sets new mouse location usually used in raycasts.
@@ -454,7 +473,8 @@ export class MouseControls {
this.selected = [];
this.setSelected(this.selected);
this.removeTransformControls();
//hideObjectPanel();
hideObjectPanel();
showPoseControls.value = false;
}
if (this.sceneManager) {
@@ -1,18 +1,16 @@
import React, { useEffect } from "react";
import { useSignals } from "@preact/signals-react/runtime";
import { LoadingDots, Toaster, TopBar } from "~/components";
import { LoadingDots, TopBar } from "~/components";
import { Controls3D } from "./comps/Controls3D";
import { ControlsTopButtons } from "./comps/ControlsTopButtons";
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,
sidePanelWidth,
sidePanelVisible,
dndSidePanelWidth,
dndTimelineHeight,
editorLoader,
cameraAspectRatio,
@@ -24,12 +22,13 @@ 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";
// import { api } from "@storyteller/api";
// import { AnalyticsApi } from "@storyteller/api";
export const PageEditor = () => {
useSignals();
//console.log(api());
//To prevent the click event from propagating to the canvas: TODO: HANDLE THIS BETTER?
const handleOverlayClick = (event: React.MouseEvent<HTMLDivElement>) => {
event.stopPropagation();
@@ -43,13 +42,6 @@ export const PageEditor = () => {
};
}, []);
const dndWidth =
dndSidePanelWidth.value > -1
? dndSidePanelWidth.value
: sidePanelWidth.value;
const width = sidePanelVisible.value
? pageWidth.value - dndWidth - 84
: pageWidth.value - 84;
const height =
dndTimelineHeight.value > -1
? pageHeight.value - dndTimelineHeight.value - 56
@@ -98,7 +90,6 @@ export const PageEditor = () => {
>
<div className="relative w-full overflow-hidden bg-transparent">
<SceneContainer>
<Toaster />
<EditorCanvas />
<PreviewFrameImage />
</SceneContainer>
@@ -106,6 +97,9 @@ export const PageEditor = () => {
{/* Focal Length Display */}
<FocalLengthDisplay />
{/* Pose Mode Selector */}
<PoseModeSelector />
{/* Top controls */}
<div
className="absolute left-0 top-0 w-full"
@@ -6,9 +6,8 @@ import { useBackgroundLoadingMedia } from "~/hooks/useBackgroundLoadingMedia";
import { useQueueHandler } from "./hooks/useQueueHandler";
import { ErrorDialog, LoadingDots } from "~/components";
import { GenerateModals } from "~/pages/PageEnigma/comps/GenerateModals/GenerateModals";
import { Toaster } from "@storyteller/ui-toaster";
import { EditorLoadingBar } from "./comps/EditorLoadingBar";
import { Wizard } from "~/pages/PageEnigma/Wizard/Wizard";
import { useSignals } from "@preact/signals-react/runtime";
import { useEffect, useState } from "react";
@@ -16,6 +15,13 @@ 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 +77,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,11 +99,18 @@ export const PageEnigma = ({ sceneToken }: { sceneToken?: string }) => {
<EngineProvider sceneToken={sceneToken}>
<PageEnigmaComponent />
<DragComponent />
<PrecisionSelector
showSignal={showPrecisionSelector}
coordSignal={precisionSelectorMenuCoords}
valuesSignal={precisionSelectorValues}
selectedValueSignal={precisionSelectedValue}
/>
<GenerateModals />
<ErrorDialog />
<Wizard />
</EngineProvider>
<EditorLoadingBar />
<Toaster offsetTop={70} offsetRight={12} />
</TrackProvider>
);
};
@@ -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>
@@ -4,12 +4,11 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { PopoverMenu } from "~/components/reusable/Popover/Popover";
import { CompletedCard } from "./CompletedCard";
import { InProgressCard } from "./InProgressCard";
import { H3, P, Tooltip } from "~/components";
import { activeImageGenerationJobs, toasts, userMovies } from "~/signals";
import { useEffect, useState } from "react";
import { H3, Tooltip } from "~/components";
import { useEffect, useState, useRef, useCallback } from "react";
import { JobsApi } from "~/Classes/ApiManager/JobsApi";
import { toast } from "sonner";
import { toast } from "@storyteller/ui-toaster";
import { ActiveJob } from "~/pages/PageEnigma/models";
// TODO ensure we de-dupe all this extra code.
interface CompletedItem {
token: string;
@@ -18,17 +17,6 @@ interface CompletedItem {
updated_at: string;
}
interface ActiveJob {
job_token: string;
request: {
maybe_model_title: string;
};
status: {
status: string;
progress_percentage: number;
};
}
// {
// "job_token": "jinf_421nnr6rj8wy2sj0s11v9fns32s",
// "request": {
@@ -71,94 +59,130 @@ interface ActiveJob {
export function Activity() {
useSignals();
const [completedItems, setCompletedItems] = useState<CompletedItem[]>([]);
const [loading, setLoading] = useState(true);
const [jobs, setJobs] = useState<ActiveJob[]>([]);
const processedJobs = useRef(new Set<string>());
const isFirstLoad = useRef(true);
const toastTimeoutRef = useRef<NodeJS.Timeout | null>(null);
// Debounced toast function
const showToast = useCallback((items: CompletedItem[]) => {
if (toastTimeoutRef.current) {
clearTimeout(toastTimeoutRef.current);
}
toastTimeoutRef.current = setTimeout(() => {
const count = items.length;
toast.success(
count === 1
? `${items[0].maybe_title} completed successfully`
: `${count} images completed successfully`,
);
toastTimeoutRef.current = null;
}, 100); // Small delay to catch potential double calls
}, []);
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 => ({
let mounted = true;
async function fetchJobs() {
try {
const jobsApi = new JobsApi();
const response = await jobsApi.ListRecentJobs();
if (!mounted || !response.success || !response.data) return;
// Handle active jobs
const activeJobs = response.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"
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);
}
progress_percentage: job.status?.progress_percentage || 0,
},
created_at: job.created_at || new Date().toISOString(),
updated_at: job.updated_at || new Date().toISOString(),
maybe_result: job.maybe_result || null,
}) as ActiveJob,
);
// 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);
setLoading(false);
// Handle completed jobs
const newlyCompleted = response.data.filter(
(job) =>
job.status?.status?.toLowerCase() === "complete_success" &&
!processedJobs.current.has(job.job_token),
);
if (newlyCompleted.length > 0) {
// Add to processed set
newlyCompleted.forEach((job) =>
processedJobs.current.add(job.job_token),
);
// Create completed items
const newItems = newlyCompleted.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 only if not first load
if (!isFirstLoad.current) {
showToast(newItems);
}
} catch (error) {
toast.error("Error Fetching Recent Jobs");
console.error("Failed to fetch recent jobs:", error);
// Update state
setCompletedItems((prev) => {
const combined = [...newItems, ...prev];
return combined
.filter(
(item, index, self) =>
index === self.findIndex((t) => t.token === item.token),
)
.sort(
(a, b) =>
new Date(b.updated_at).getTime() -
new Date(a.updated_at).getTime(),
)
.slice(0, 50);
});
}
};
await fetchJobs();
setJobs(activeJobs);
isFirstLoad.current = false;
} catch (error) {
console.error("Failed to fetch jobs:", error);
}
}
// Initial fetch
fetchJobs();
// Set up polling
const interval = setInterval(fetchJobs, 5000);
// Cleanup
return () => {
mounted = false;
clearInterval(interval);
if (toastTimeoutRef.current) {
clearTimeout(toastTimeoutRef.current);
}
};
// Initial load
// Initial call to pollData
pollData();
// Set up polling interval (every 5 seconds)
const interval = setInterval(async () => {
await pollData();
}, 5000);
return () => clearInterval(interval);
}, []);
}, [showToast]);
return (
<Tooltip content="Activity" position="bottom" closeOnClick={true}>
@@ -170,25 +194,27 @@ 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.length > 0 ? (
<FontAwesomeIcon icon={faSpinnerThird} className="animate-spin" />
) : (
<FontAwesomeIcon icon={faBell} />
)}
{jobs.length > 0 && (
<div className="absolute -right-1 -top-1 h-3 w-3 animate-pulse rounded-full bg-brand-primary-400" />
)}
</div>
}
>
<div className="max-h-[480px] overflow-y-auto">
{/* In progress */}
{jobs && jobs.length > 0 && (
<div>
{jobs.map((job) => (
<InProgressCard key={job.job_token} movie={job} />
))}
</div>
)}
{jobs.length > 0 && (
<div>
{jobs.map((job) => (
<InProgressCard key={job.job_token} job={job} />
))}
</div>
)}
{!userMovies.value && !activeImageGenerationJobs.value ? (
{isFirstLoad.current ? (
<div className="flex h-48 w-full flex-col justify-center gap-4 p-4 text-center align-middle">
<FontAwesomeIcon
icon={faSpinnerThird}
@@ -198,30 +224,18 @@ export function Activity() {
/>
<H3 className="text-gray-300">Retrieving Activities</H3>
</div>
) : userMovies.value &&
userMovies.value.length === 0 &&
!activeImageGenerationJobs.value ? (
) : jobs.length === 0 && completedItems.length === 0 ? (
<div className="flex h-48 w-full flex-col justify-center gap-4 p-4 text-center align-middle">
<H3 className="text-gray-300">No activities yet</H3>
<P className="text-gray-400">
Try generating something new from the featured scenes
</P>
<h3 className="text-lg text-gray-300">No activities yet</h3>
</div>
) : (
<div>
{/* Completed */}
<div className="flex flex-col">
{completedItems.map((item) => (
<CompletedCard
key={item.token}
movie={item}
setMovieId={(item_id) => {
console.log("item_id", item_id);
}}
/>
<CompletedCard key={item.token} job={item} />
))}
</div>
</div>
</div>
)}
</div>
</PopoverMenu>
@@ -1,98 +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 { downloadFileImage } 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 mediaPath = job?.public_bucket_path || "";
const downloadLink = `${cdnOrigin}${mediaPath}`;
const imageUrl = downloadLink;
const fullImageUrl = downloadLink;
const downloadLink = `${cdnOrigin}${movie.public_bucket_path}`;
// console.log(downloadLink);
// console.log("This is the completed item", movie);
// console.log("item completed card", movie);
// to do use the thumbnail image
const imageUrl = downloadLink; // 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";
downloadFileImage({ 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>
<FontAwesomeIcon icon={faDownToLine} />
</a>
</div>
</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} />
@@ -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}
/>
</>
);
},
@@ -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>
)
}
@@ -48,6 +48,7 @@ import {
updateCamera,
focalLengthDragging,
} from "~/pages/PageEnigma/signals/camera";
import { toast } from "@storyteller/ui-toaster";
interface ReferenceImage {
id: string;
@@ -430,7 +431,7 @@ export const PromptBox = () => {
screenshot: snapshot.file,
sceneMediaToken: "",
});
console.log("useSystemPrompt", useSystemPrompt);
await engineApi.enqueueImageGeneration({
disableSystemPrompt: !useSystemPrompt,
@@ -451,6 +452,7 @@ export const PromptBox = () => {
await new Promise((resolve) => setTimeout(resolve, 2000));
} finally {
setisEnqueueing(false);
toast.success("Image added to queue");
}
}
setisEnqueueing(false);
@@ -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");
+8 -2
View File
@@ -4,7 +4,14 @@
"sideEffects": false,
"type": "module",
"nx": {
"name": "editor3d"
"name": "editor3d",
"targets": {
"build": {
"dependsOn": [
"^build"
]
}
}
},
"scripts": {
"build": "remix vite:build",
@@ -21,7 +28,6 @@
"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",
@@ -2,8 +2,11 @@
set -euxo pipefail
echo "Install packages"
npm install
echo "Build Editor 3D"
nx build editor3d
nx build editor3d --verbose
echo "Change to project dir"
pushd apps/editor3d/
+14 -17
View File
@@ -1,34 +1,31 @@
{
"include": [
"env.d.ts",
"**/*.ts",
"**/*.tsx",
"netlify-edge-plugin.ts"
],
"extends": "../../tsconfig.base.json",
"include": ["env.d.ts", "**/*.ts", "**/*.tsx", "netlify-edge-plugin.ts"],
"compilerOptions": {
"lib": [
"DOM",
"DOM.Iterable",
"ESNext"
],
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"isolatedModules": true,
"esModuleInterop": true,
"jsx": "react-jsx",
"module": "ESNext",
"moduleResolution": "node",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"target": "ESNext",
"strict": true,
"allowJs": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"paths": {
"~/*": [
"./app/*"
]
"~/*": ["apps/editor3d/app/*"]
},
// Vite takes care of building everything, not tsc.
"noEmit": true
}
},
"references": [
{
"path": "../../libs/login"
},
{
"path": "../../libs/components/toaster"
}
]
}
+9
View File
@@ -14,6 +14,15 @@ export default defineConfig({
// 'kalidokit': `${path.resolve(__dirname, 'src')}/`,
// },
//},
//resolve: {
// alias: [
// {
// find: /@frontend\/login/,
// //replacement: path.resolve(__dirname, 'node_modules', '@frontend', 'login'),
// replacement: path.resolve(__dirname, '../', '../', 'libs', 'login'),
// },
// ],
//},
//rollupOptions: {
// // make sure to externalize deps that shouldn't be bundled
// // into your library
+1
View File
@@ -3,6 +3,7 @@
set -euxo pipefail
echo "Removing Node Modules..."
npx nx reset
rm -rf ./node_modules/
rm -rf ./apps/editor2d/node_modules/
rm -rf ./apps/editor3d/node_modules/
+16
View File
@@ -1 +1,17 @@
export * from './lib/api.js';
export * from "./lib/AnalyticsApi.js";
export * from "./lib/BillingApi.js";
export * from "./lib/CommentsApi.js";
export * from "./lib/EngineApi.js";
export * from "./lib/FeaturedItemsApi.js";
export * from "./lib/JobsApi.js";
export * from "./lib/MediaFilesApi.js";
export * from "./lib/MediaUploadApi.js";
export * from "./lib/MiscApi.js";
export * from "./lib/ModerationApi.js";
export * from "./lib/PromptsApi.js";
export * from "./lib/TtsApi.js";
export * from "./lib/UserBookmarksApi.js";
export * from "./lib/UsersApi.js";
export * from "./lib/VideoApi.js";
export * from "./lib/WeightsApi.js";
+38
View File
@@ -0,0 +1,38 @@
import { ApiManager, ApiResponse } from "./ApiManager.js";
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 };
});
}
}
+17
View File
@@ -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;
// }
+235
View File
@@ -0,0 +1,235 @@
// import { bool } from "@techstark/opencv-js";
import { API_TARGETS } from "./enums/Api.js";
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);
}
}
+33
View File
@@ -0,0 +1,33 @@
import { ApiManager, ApiResponse } from "./ApiManager.js";
import { Subscription } from "./models/Billing.js";
import { LoyaltyProgram } from "./enums/Billing.js";
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,
};
});
}
}
+91
View File
@@ -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 };
});
}
}
+213
View File
@@ -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 };
});
}
}
+93
View File
@@ -0,0 +1,93 @@
// libs/api-client/src/JobsApi.ts
import { ApiManager, ApiResponse } from "../../../api/src/lib/ApiManager.js";
import { Job, JobPreview } from "./models/Job.js";
import { JobState } from "./models/JobState.js";
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 };
});
}
}
+364
View File
@@ -0,0 +1,364 @@
import { MediaFile } from "./models/MediaFile.js";
import { Pagination, PaginationInfinite } from "./models/Pagination.js";
import { ApiManager, ApiResponse } from "./ApiManager.js";
import { Visibility } from "./enums/Visibility.js";
import {
FilterEngineCategories,
FilterMediaClasses,
FilterMediaType,
} from "./enums/QueryFilters.js";
interface ListMediaQuery {
sort_ascending?: boolean;
page_size?: number;
cursor?: string;
cursor_is_reversed?: boolean;
filter_media_classes?: FilterMediaClasses[];
filter_media_type?: FilterMediaType[];
filter_engine_categories?: FilterEngineCategories[];
}
interface ListUserMediaQuery {
username: string;
sort_ascending?: boolean;
page_size?: number;
page_index?: number;
filter_media_classes?: FilterMediaClasses[];
filter_media_type?: FilterMediaType[];
filter_engine_categories?: FilterEngineCategories[];
}
interface SearchFeaturedMediaQuery {
search_term: string;
filter_media_classes?: FilterMediaClasses[];
filter_media_type?: FilterMediaType[];
filter_engine_categories?: FilterEngineCategories[];
}
export class MediaFilesApi extends ApiManager {
public async DeleteMediaFileByToken({
mediaFileToken,
asMod = true,
setDelete = true,
}: {
mediaFileToken: string;
asMod?: boolean;
setDelete?: boolean;
}): Promise<ApiResponse<MediaFile>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/file/${mediaFileToken}`;
const body = { as_mod: asMod, set_delete: setDelete };
return await this.delete<
{
as_mod: boolean;
set_delete: boolean;
},
{
success?: boolean;
BadInput?: string;
}
>({ endpoint, body })
.then((response) => ({
success: response.success ?? false,
errorMessage: response.BadInput,
}))
.catch((err) => {
return {
success: false,
errorMessage: err.message,
};
});
}
public async ListMediaFilesByTokens({
mediaTokens,
}: {
mediaTokens: string[];
}): Promise<ApiResponse<MediaFile[]>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/batch`;
return await this.get<{
success: boolean;
media_files: MediaFile[];
error_reason?: string;
}>({ endpoint, query: { tokens: mediaTokens.join(",") } })
.then((response) => ({
success: response.success,
data: response.media_files,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public async GetMediaFileByToken({
mediaFileToken,
}: {
mediaFileToken: string;
}): Promise<ApiResponse<MediaFile>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/file/${mediaFileToken}`;
return await this.get<{
success: boolean;
media_file: MediaFile;
}>({ endpoint })
.then((response) => ({
success: response.success,
data: response.media_file,
}))
.catch((err) => {
return {
success: false,
errorMessage: err.message,
};
});
}
public async ListMediaFiles(
query: ListMediaQuery,
): Promise<ApiResponse<MediaFile[], PaginationInfinite>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/list`;
const queryWithStrings = {
...query,
filter_media_classes: query.filter_media_classes
? query.filter_media_classes.join(",")
: undefined,
filter_media_type: query.filter_media_type
? query.filter_media_type.join(",")
: undefined,
filter_engine_categories: query.filter_engine_categories
? query.filter_engine_categories.join(",")
: undefined,
};
return await this.get<{
success: boolean;
results: MediaFile[];
pagination?: PaginationInfinite;
}>({ endpoint, query: queryWithStrings })
.then((response) => ({
success: response.success,
data: response.results ?? [],
pagination: response.pagination,
}))
.catch((err) => {
return {
success: false,
errorMessage: err.message,
};
});
}
public async ListFeaturedMediaFiles(
query: ListMediaQuery,
): Promise<ApiResponse<MediaFile[], PaginationInfinite>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/list_featured`;
const queryWithStrings = {
...query,
filter_media_classes: query.filter_media_classes
? query.filter_media_classes.join(",")
: undefined,
filter_media_type: query.filter_media_type
? query.filter_media_type.join(",")
: undefined,
filter_engine_categories: query.filter_engine_categories
? query.filter_engine_categories.join(",")
: undefined,
};
return await this.get<{
success: boolean;
results: MediaFile[];
pagination: PaginationInfinite;
}>({ endpoint, query: queryWithStrings })
.then((response) => ({
success: true,
data: response.results,
pagination: response.pagination,
}))
.catch((err) => {
return {
success: false,
errorMessage: err.message,
};
});
}
public async ListUserMediaFiles(
query: ListUserMediaQuery,
): Promise<ApiResponse<MediaFile[], Pagination>> {
const userName = query.username;
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/list/user/${userName}`;
const queryWithStrings = {
...query,
include_user_uploads: true,
filter_media_classes: query.filter_media_classes
? query.filter_media_classes.join(",")
: undefined,
filter_media_type: query.filter_media_type
? query.filter_media_type.join(",")
: undefined,
filter_engine_categories: query.filter_engine_categories
? query.filter_engine_categories.join(",")
: undefined,
};
return await this.get<{
success: boolean;
results: MediaFile[];
pagination?: Pagination;
}>({ endpoint, query: queryWithStrings })
.then((response) => ({
success: response.success,
data: response.results ?? [],
pagination: response.pagination,
}))
.catch((err) => {
return {
success: false,
errorMessage: err.message,
};
});
}
public async SearchFeaturedMediaFiles(
query: SearchFeaturedMediaQuery,
): Promise<ApiResponse<MediaFile[], Pagination>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/search_featured`;
const queryWithStrings = {
search_term: query.search_term,
filter_media_classes: query.filter_media_classes
? query.filter_media_classes.join(",")
: undefined,
filter_media_type: query.filter_media_type
? query.filter_media_type.join(",")
: undefined,
filter_engine_categories: query.filter_engine_categories
? query.filter_engine_categories.join(",")
: undefined,
};
return await this.get<{
success: boolean;
results: MediaFile[];
pagination: Pagination;
}>({ endpoint, query: queryWithStrings })
.then((response) => ({
success: true,
data: response.results,
pagination: response.pagination,
}))
.catch((err) => ({
success: false,
errorMessage: err.message,
}));
}
public async SearchUserMediaFiles(
query: SearchFeaturedMediaQuery,
): Promise<ApiResponse<MediaFile[], Pagination>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/search_session`;
const queryWithStrings = {
search_term: query.search_term,
filter_media_classes: query.filter_media_classes
? query.filter_media_classes.join(",")
: undefined,
filter_media_type: query.filter_media_type
? query.filter_media_type.join(",")
: undefined,
filter_engine_categories: query.filter_engine_categories
? query.filter_engine_categories.join(",")
: undefined,
};
return await this.get<{
success: boolean;
results: MediaFile[];
pagination: Pagination;
}>({ endpoint, query: queryWithStrings })
.then((response) => ({
success: true,
data: response.results,
pagination: response.pagination,
}))
.catch((err) => ({
success: false,
errorMessage: err.message,
}));
}
public async RenameMediaFileByToken({
mediaToken,
name,
}: {
mediaToken: string;
name: string;
}): Promise<ApiResponse<undefined>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/rename/${mediaToken}`;
const body = { name };
return await this.post<
{ name: 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 async UpdateCoverImage({
mediaFileToken,
imageToken,
}: {
mediaFileToken: string;
imageToken: string;
}): Promise<ApiResponse<undefined>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/cover_image/${mediaFileToken}`;
return await this.post<
{ cover_image_media_file_token: string },
{
success?: boolean;
BadInput?: string;
}
>({ endpoint, body: { cover_image_media_file_token: imageToken } })
.then((response) => ({
success: response.success ?? false,
errorMessage: response.BadInput,
}))
.catch((err) => {
return {
success: false,
errorMessage: err.message,
};
});
}
public async UpdateVisibility({
mediaFileToken,
visibility,
}: {
mediaFileToken: string;
visibility: Visibility;
}): Promise<ApiResponse<undefined>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/visibility/${mediaFileToken}`;
return await this.post<
{ creator_set_visibility: string },
{
success?: boolean;
BadInput?: string;
}
>({ endpoint, body: { creator_set_visibility: visibility } })
.then((response) => ({
success: response.success ?? false,
errorMessage: response.BadInput,
}))
.catch((err) => {
return {
success: false,
errorMessage: err.message,
};
});
}
}
+341
View File
@@ -0,0 +1,341 @@
import { ApiManager, ApiResponse } from "./ApiManager.js";
import { FilterEngineCategories } from "./enums/QueryFilters.js";
import { Visibility } from "./enums/Visibility.js";
import { EIntermediateFile } from "./enums/EIntermediateFile.js";
export class MediaUploadApi extends ApiManager {
private sessionToken = "";
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
}
}
+26
View File
@@ -0,0 +1,26 @@
import { ApiManager, ApiResponse } from "./ApiManager.js";
import { StatusAlert } from "./models/StatusAlert.js";
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,
}));
}
}
+25
View File
@@ -0,0 +1,25 @@
import { ApiManager, ApiResponse } from "./ApiManager.js";
import { Prompts } from "./models/Prompts.js";
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 "./ApiManager.js";
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,
});
}
}
+64
View File
@@ -0,0 +1,64 @@
import { ApiManager, ApiResponse } from "./ApiManager.js";
import { GenerateTtsAudioErrorType } from "./enums/Tts.js";
import { Visibility } from "./enums/Visibility.js";
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 "./ApiManager.js";
import {
UserBookmarkBatch,
UserBookmarkByEntity,
UserBookmarkByUser,
} from "./models/UserBookmark.js";
import { Pagination } from "./models/Pagination.js";
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 "./ApiManager.js";
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 };
});
}
}
+161
View File
@@ -0,0 +1,161 @@
import { ApiManager, ApiResponse } from "./ApiManager.js";
import { UserInfo } from "./models/Users.js";
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,
};
});
}
}
+70
View File
@@ -0,0 +1,70 @@
import { ApiManager, ApiResponse } from "./ApiManager.js";
import { Visibility } from "./enums/Visibility.js";
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 };
});
}
}
@@ -0,0 +1,53 @@
import { ApiManager, ApiResponse } from "./ApiManager.js";
interface VoiceConversionParams {
autoPredictF0?: boolean;
creatorSetVisibility?: string;
isStorytellerDemo?: boolean;
overrideF0Method?: string;
sourceMediaUploadToken: string;
transpose?: number;
uuidIdempotencyToken: string;
voiceConversionModelToken: string;
}
interface VoiceConversionRequest {
auto_predict_f0: boolean;
creator_set_visibility: string;
is_storyteller_demo: boolean;
override_f0_method: string;
source_media_upload_token: string;
transpose: number;
uuid_idempotency_token: string;
voice_conversion_model_token: string;
}
export class VoiceConversionApi extends ApiManager {
public ConvertVoice(
params: VoiceConversionParams,
): Promise<ApiResponse<string>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/voice_conversion/inference`;
const body = this.parseBodyValues<
VoiceConversionParams,
VoiceConversionRequest
>(params);
return this.post<
VoiceConversionRequest,
{
success?: boolean;
inference_job_token?: string;
BadInput?: string;
}
>({ endpoint, body })
.then((response) => ({
success: response.success ?? false,
data: response.inference_job_token,
errorMessage: response.BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
}
@@ -0,0 +1,77 @@
import { ApiManager, ApiResponse } from "./ApiManager.js";
import { Pagination } from "./models/Pagination.js";
import { ZsDataset } from "./models/Dataset.js";
interface VoiceDesignerParams {
text: string;
uuidIdempotencyToken: string;
voiceToken: string;
}
interface VoiceDesignerRequest {
text: string;
uuid_idempotency_token: string;
voice_token: string;
}
export class VoiceDesignerApi extends ApiManager {
public EnqueueTts(
params: VoiceDesignerParams,
): Promise<
ApiResponse<{ inference_job_token?: string; voice_token?: string }>
> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/voice_designer/enqueue_tts`;
const body = this.parseBodyValues<
VoiceDesignerParams,
VoiceDesignerRequest
>(params);
return this.post<
VoiceDesignerRequest,
{
success?: boolean;
inference_job_token?: string;
voice_token: string;
BadInput?: string;
}
>({ endpoint, body })
.then((response) => ({
success: response.success ?? false,
...(response.success
? {
data: {
inference_job_token: response.inference_job_token,
voice_token: response.voice_token,
},
}
: {}),
errorMessage: response.BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public ListDatasetsByUser({
username,
}: {
username: string;
}): Promise<ApiResponse<ZsDataset[]>> {
const user = username;
const endpoint = `${this.ApiTargets.BaseApi}/v1/voice_designer/user/${user}/list`;
return this.get<{
success: boolean;
datasets: ZsDataset[];
pagination: Pagination;
}>({ endpoint })
.then((response) => ({
success: true,
data: response.datasets,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
}
+295
View File
@@ -0,0 +1,295 @@
import { ApiManager, ApiResponse } from "./ApiManager.js";
import { Weight } from "./models/Weight.js";
import { Pagination, PaginationInfinite } from "./models/Pagination.js";
import {
FilterEngineCategories,
FilterMediaClasses,
FilterMediaType,
} from "./enums/QueryFilters.js";
import { Visibility } from "./enums/Visibility.js";
export enum ScopedWeightType {
HIFIGAN_TT2 = "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 ScopedWeightCategory {
IMAGE_GENERATION = "image_generation",
TEXT_TO_SPEECH = "text_to_speech",
VOCODER = "vocoder",
VOICE_CONVERSION = "voice_conversion",
WORKFLOW_CONFIG = "workflow_config",
}
interface WeightsRequest {
pageSize?: number;
weightType?: ScopedWeightType[];
weightCategory?: ScopedWeightCategory[];
//TODO: what is scoped??
scopedWeightType?: ScopedWeightType[];
scopedWeightCategory?: ScopedWeightCategory[];
}
export interface ListWeightsByUserRequest extends WeightsRequest {
username: string;
pageIndex?: number;
sortAscending?: boolean;
}
export interface ListWeightsRequest extends WeightsRequest {
cursor?: string;
cursorIsReversed?: boolean;
}
export interface ListFeaturedWeightsRequest {
sortAscending?: boolean;
pageSize?: number;
cursor?: string;
cursorIsReversed?: boolean;
filterMediaClasses?: FilterMediaClasses[];
filterMediaType?: FilterMediaType[];
filterEngineCategories?: FilterEngineCategories[];
}
export interface SearchWeightParams {
ietfLanguageSubtag: string;
searchTerm: string;
weightCategory: ScopedWeightCategory;
weightType: ScopedWeightType;
}
export interface SearchWeightRequest {
ietf_language_subtag: string;
search_term: string;
weight_category: string;
weight_type: string;
}
interface GetWeightByTokenResponse extends Weight {
success: boolean;
}
export interface UpdateWeightByTokenParams {
weightToken: string;
coverImageMediaFileToken: string;
descriptionMarkdown: string;
title: string;
visibility: Visibility;
}
export interface UpdateWeightByTokenRequest {
cover_image_media_file_token: string;
description_markdown: string;
title: string;
visibility: Visibility;
}
export class WeightsApi extends ApiManager {
public ListWeightsByUser({
username,
...params
}: ListWeightsByUserRequest): Promise<ApiResponse<Weight[], Pagination>> {
const user = username;
const endpoint = `${this.ApiTargets.BaseApi}/v1/weights/by_user/${user}`;
const query = this.parseQueryValues(params);
return this.get<{
success: boolean;
results: Weight[];
pagination: Pagination;
}>({ endpoint, query })
.then((response) => ({
success: true,
data: response.results,
pagination: response.pagination,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public ListWeights({
...params
}: ListWeightsRequest): Promise<ApiResponse<Weight[], PaginationInfinite>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/weights/list`;
const query = this.parseQueryValues(params);
return this.get<{
success: boolean;
results: Weight[];
pagination: PaginationInfinite;
}>({ endpoint, query })
.then((response) => ({
success: true,
data: response.results,
pagination: response.pagination,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public ListWeightsFeatured({
...params
}: ListFeaturedWeightsRequest): Promise<
ApiResponse<Weight[], PaginationInfinite>
> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/weights/list_featured`;
const query = this.parseQueryValues(params);
return this.get<{
success: boolean;
results: Weight[];
pagination: PaginationInfinite;
}>({ endpoint, query })
.then((response) => ({
success: true,
data: response.results,
pagination: response.pagination,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public ListWeightsPinned(): Promise<ApiResponse<Weight[]>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/weights/list_pinned`;
return this.get<{
success: boolean;
results: Weight[];
}>({ endpoint })
.then((response) => ({
success: true,
data: response.results,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public SearchWeights(
params: SearchWeightParams,
): Promise<ApiResponse<Weight[]>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/weights/search`;
const body = this.parseBodyValues<SearchWeightParams, SearchWeightRequest>(
params,
);
return this.post<
SearchWeightRequest,
{
success: boolean;
weights: Weight[];
}
>({ endpoint, body })
.then((response) => ({
success: true,
data: response.weights,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public GetWeightByToken({
weightToken,
}: {
weightToken: string;
}): Promise<ApiResponse<Weight>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/weights/weight/${weightToken}`;
return this.get<GetWeightByTokenResponse>({ endpoint })
.then(({ success, ...mediaFile }) => ({
success: success,
data: mediaFile,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public UpdateWeightByToken({
weightToken,
...params
}: UpdateWeightByTokenParams): Promise<ApiResponse<undefined>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/weights/weight/${weightToken}`;
const body = this.parseBodyValues<
Omit<UpdateWeightByTokenParams, "weightToken">,
UpdateWeightByTokenRequest
>(params);
return this.post<
UpdateWeightByTokenRequest,
{ success?: boolean; BadInput?: string }
>({ endpoint, body })
.then(({ success, BadInput }) => ({
success: success ?? false,
errorMessage: BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public DeleteWeightByToken({
weightToken,
}: {
weightToken: string;
}): Promise<ApiResponse<undefined>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/weights/weight/${weightToken}`;
const body = {
as_mod: true,
set_delete: true,
};
return this.delete<
{ as_mod: boolean; set_delete: boolean },
{ success?: boolean; BadInput?: string }
>({ endpoint, body })
.then(({ success, BadInput }) => ({
success: success ?? false,
errorMessage: BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
public UpdateWeightCoverImageByToken({
weightToken,
coverImageMediaFileToken,
}: {
weightToken: string;
coverImageMediaFileToken: string;
}): Promise<ApiResponse<undefined>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/weights/${weightToken}/cover_image`;
const body = {
cover_image_media_file_token: coverImageMediaFileToken,
};
return this.post<
{ cover_image_media_file_token: string },
{ success?: boolean; BadInput?: string }
>({ endpoint, body })
.then(({ success, BadInput }) => ({
success: success ?? false,
errorMessage: BadInput,
}))
.catch((err) => {
return { success: false, errorMessage: err.message };
});
}
}
+7
View File
@@ -0,0 +1,7 @@
export enum API_TARGETS {
BASE_API = "https://api.storyteller.ai",
GOOGLE_API = "https://storage.googleapis.com",
FUNNEL_API = "https://style.storyteller.ai",
CDN_API = "https://cdn.storyteller.ai",
GRAVATAR_API = "https://www.gravatar.com",
}
@@ -0,0 +1,11 @@
export enum SubscriptionNamespace {
FAKEYOU = "fakeyou",
}
export enum SubscriptionProduct {
PLUS = "fakeyou_plus",
PRO = "fakeyou_pro",
ELITE = "fakeyou_elite",
}
export enum LoyaltyProgram {
CONTRIBUTOR = "fakeyou_contributor",
}
@@ -0,0 +1,4 @@
export enum EIntermediateFile {
"true",
"false"
}
+16
View File
@@ -0,0 +1,16 @@
export enum JobStatus {
PENDING = "pending",
STARTED = "started",
ATTEMPT_FAILED = "attempt_failed",
COMPLETE_SUCCESS = "complete_success",
COMPLETE_FAILURE = "complete_failure",
DEAD = "dead",
CANCELLED_BY_USER = "cancelled_by_user",
CANCELLED_BY_SYSTEM = "cancelled_by_system",
}
export enum JobType {
TextToSpeech = "text_to_speech",
VoiceConversion = "voice_conversion",
VideoStyleTransfer = "workflow",
}
@@ -0,0 +1,26 @@
export enum MediaFileClass {
/// Unknown (default value)
/// This will be present until we migrate all old files.
Unknown = "unknown",
/// Audio files: wav, mp3, etc.
Audio = "audio",
/// Image files: png, jpeg, etc.
Image = "image",
/// Video files: mp4, etc.
Video = "video",
/// Engine "animations"
Animation = "animation",
/// Engine "characters"
Character = "character",
/// Engine "prop" items
Prop = "prop",
/// Engine scenes (internal and external scenes)
Scene = "scene",
}
@@ -0,0 +1,58 @@
export enum MediaFileType {
Audio = "audio",
Video = "video",
Image = "image",
// BVH is a very popular file format for motion capture.
// It is compatible with our engine.
BVH = "bvh",
// GLTF is the text-based format of GLB. Unforunately it requires a
// second file to include the visual data, so we strongly discourage
// the use of this format in favor of GLB.
GLTF = "gltf",
// GLB is the format we convert FBX files into. It's a very popular
// open source 3D file format.
GLB = "glb",
// MMD is the format for anime characters.
// open source 3D file format.
MMD = "mmd",
// FBX isn't supported by Storyteller Engine, but we can get
// uploads in this format. It's a very popular 3D graphics format.
FBX = "fbx",
// Full Storyteller Engine scenes (RON = Rusty Object Notation)
// This is a temporary format that will eventually go away.
Scene = "scene",
None = "none",
}
export enum MediaFileSubtype {
/// Animation file from Mixamo
/// Primarily used for FBX and GLB.
Mixamo = "mixamo",
/// Animation file from MocapNet
/// Primarily used for BVH.
MocapNet = "mocap_net",
/// Generic animation case
/// Used for BVH files, but can also pertain to animation-only files of other types.
AnimationOnly = "animation_only",
// TODO(bt,2024-03-08): Migrate records and code, then remove
/// DEPRECATED: Use `SceneImport` instead.
Scene = "scene",
/// Generic 3D scene file.
/// Can pertain to glTF, glB, FBX, etc.
SceneImport = "scene_import",
/// Native Storyteller scene format.
/// Typically stored in a `.scn.ron` file.
StorytellerScene = "storyteller_scene",
}
@@ -0,0 +1,29 @@
export enum FilterMediaClasses {
AUDIO = "audio",
IMAGE = "image",
VIDEO = "video",
}
export enum FilterMediaType {
SCENE_JSON = "scene_json",
GLB = "glb",
GLTF = "gltf",
PMX = "pmx",
PMD = "pmd",
VMD = "vmd",
}
export enum FilterEngineCategories {
ANIMATION = "animation",
AUDIO = "audio",
CHARACTER = "character",
CREATURE = "creature",
EXPRESSION = "expression",
IMAGE_PLANE = "image_plane",
LOCATION = "location",
OBJECT = "object",
SCENE = "scene",
SET_DRESSING = "set_dressing",
SKYBOX = "skybox",
VIDEO_PLANE = "video_plane",
}
+7
View File
@@ -0,0 +1,7 @@
export enum GenerateTtsAudioErrorType {
BadRequest = "bad_request",
NotFound = "not_found",
TooManyRequests = "too_many_requests",
ServerError = "server_error",
UnknownError = "unknown_error",
}
@@ -0,0 +1,6 @@
export enum USER_FEATURE_FLAGS {
EXPLORE_MEDIA = "explore_media",
STUDIO = "studio",
UPLOAD_3D = "upload_3d",
VIDEO_STYLE_TRANSFER = "video_style_transfer",
}
@@ -0,0 +1,5 @@
export enum Visibility {
Public = "public",
Hidden = "hidden",
Private = "private",
}
@@ -0,0 +1,8 @@
export enum WeightCategory {
TTS = "text_to_speech",
VOCODER = "vocoder",
VC = "voice_conversion",
ZS = "zero_shot",
SD = "image_generation",
WF = "workflow_config",
}
@@ -0,0 +1,11 @@
export enum WeightType {
TT2 = "tt2",
HIFIGAN_TT2 = "hifigan_tt2",
RVCv2 = "rvc_v2",
SD_15 = "sd_1.5",
SDXL = "sdxl",
SVC = "so_vits_svc",
LORA = "loRA",
VALL_E = "vall_e",
NONE = "none",
}
+18
View File
@@ -0,0 +1,18 @@
// https://storyteller-docs.netlify.app/#/
export * from "./AnalyticsApi";
export * from "./BetaKeysApi";
export * from "./BillingApi";
export * from "./CommentsApi";
export * from "./EngineApi";
export * from "./FeaturedItemsApi";
export * from "./JobsApi";
export * from "./MediaFilesApi";
export * from "./MediaUploadApi";
export * from "./MiscApi";
export * from "./ModerationApi";
export * from "./PromptsApi";
export * from "./TtsApi";
export * from "./UserBookmarksApi";
export * from "./UsersApi";
export * from "./VideoApi";
export * from "./WeightsApi";
@@ -0,0 +1,26 @@
interface DefaultAvatar {
color_index: number;
image_index: number;
}
interface User {
default_avatar: DefaultAvatar;
display_name: string;
gravatar_hash: string;
user_token: string;
username: string;
}
export interface BetaKey {
created_at: string;
creator: User;
is_distributed: boolean;
key_value: string;
maybe_note?: string;
maybe_note_html?: string;
maybe_redeemed_at?: string;
maybe_redeemer?: User;
maybe_referrer?: User;
product: string;
token: string;
}
@@ -0,0 +1,17 @@
import {
LoyaltyProgram,
SubscriptionNamespace,
SubscriptionProduct,
} from "../enums/Billing.js";
export interface ActiveSubscriptions {
// user's list of subscriptions
active_subscriptions: Subscription[];
// special programs that the user could be in
maybe_loyalty_program?: LoyaltyProgram;
}
export interface Subscription {
namespace: SubscriptionNamespace;
product_slug: SubscriptionProduct;
}
@@ -0,0 +1,23 @@
export interface ZsDataset {
created_at: string; // ISO 8601 date-time string
creator: UserDetailsLight;
creator_set_visibility: string; // Enum or specific string values
dataset_token: string;
ietf_language_tag: string;
ietf_primary_language_subtag: string;
title: string;
updated_at: string; // ISO 8601 date-time string
}
interface UserDetailsLight {
default_avatar: UserDefaultAvatarInfo;
display_name: string;
gravatar_hash: string;
user_token: string;
username: string;
}
interface UserDefaultAvatarInfo {
color_index: number;
image_index: number;
}
+72
View File
@@ -0,0 +1,72 @@
import { JobStatus } from "../enums/Job.js";
export interface Job {
created_at: string;
job_token: string;
maybe_result: JobMaybeResult;
request: JobRequest;
status: JobStatusResponse;
updated_at: string;
}
export interface JobMaybeResult {
entity_token: string;
entity_type: string;
// maybe_public_bucket_media_path: string;
maybe_successfully_completed_at: string;
media_links: {
cdn_url: string;
};
}
export interface JobRequest {
inference_category: string;
maybe_model_title: string;
maybe_model_token: string;
maybe_model_type: string;
maybe_raw_inference_text: string;
maybe_style_name: string;
}
export interface JobStatusResponse {
attempt_count: number;
maybe_assigned_cluster: string;
maybe_assigned_worker: string;
maybe_extra_status_description: string;
maybe_failure_category: string;
maybe_first_started_at: string;
requires_keepalive: boolean;
progress_percentage: number;
status: JobStatus;
}
export interface JobPreview {
success: boolean;
state: {
job_token: string;
request: Record<string, unknown>;
status: {
status: string;
maybe_extra_status_description: string | null;
maybe_assigned_worker: string;
maybe_assigned_cluster: string;
maybe_first_started_at: string;
attempt_count: number;
requires_keepalive: boolean;
maybe_failure_category: string;
progress_percentage: number;
};
maybe_result: {
expected_stages: number;
currently_active_stage: number;
per_stage_frame_count: number;
stages: Array<{
stage_progress: number;
expected_frame_count: number;
stage_complete: boolean;
frames: string[];
}>;
};
created_at: string;
updated_at: string;
};
}
@@ -0,0 +1,35 @@
interface MaybeResult {
entity_token: string;
entity_type: string;
maybe_public_bucket_media_path: string;
maybe_successfully_completed_at: string;
}
interface Request {
inference_category: string;
maybe_model_title?: string;
maybe_model_token: string;
maybe_model_type: string;
maybe_raw_inference_text: string;
maybe_style_name: string;
}
interface Status {
attempt_count: number;
maybe_assigned_cluster?: string;
maybe_assigned_worker?: string;
maybe_extra_status_description?: string;
maybe_failure_category?: string;
maybe_first_started_at: string;
requires_keepalive: boolean;
status: string;
}
export interface JobState {
created_at: string;
job_token: string;
maybe_result?: MaybeResult;
request: Request;
status: Status;
updated_at: string;
}
@@ -0,0 +1,59 @@
import { MediaFileClass } from "../enums/MediaFileClass";
import { MediaFileType, MediaFileSubtype } from "../enums/MediaFileType";
import { WeightCategory } from "../enums/WeightCategory";
import { WeightType } from "../enums/WeightType";
import { UserInfo } from "./Users";
export interface MediaFile {
token: string;
media_type: MediaFileType;
media_class: MediaFileClass | null;
maybe_animation_type: string | null;
maybe_media_subtype: MediaFileSubtype | null;
maybe_engine_extension: string | null;
maybe_batch_token: string;
maybe_original_filename: string | null;
maybe_creator_user: UserInfo | null;
maybe_prompt_token: string | null;
origin: {
origin_category: string;
product_category: string;
maybe_model: { title: string } | null;
};
origin_category: string;
origin_product_category: string;
maybe_origin_model_type: null | string;
maybe_origin_model_token: null | string;
maybe_duration_millis: number | null;
maybe_style_name: null | string;
public_bucket_path: string;
public_bucket_url: string;
media_links: {
cdn_url: string;
thumbnail_template: string;
};
cover_image: {
maybe_cover_image_public_bucket_path: null | string;
default_cover: {
image_index: number;
color_index: number;
};
};
creator_set_visibility: string;
maybe_title: null | string;
maybe_text_transcript: null | string;
stats: {
positive_rating_count: number;
bookmark_count: number;
};
maybe_model_weight_info: {
title: string;
weight_token: string;
weight_category: WeightCategory;
weight_type: WeightType;
maybe_weight_creator: UserInfo;
maybe_cover_image_public_bucket_path: string;
};
created_at: string;
updated_at: string;
}
@@ -0,0 +1,9 @@
export interface Pagination {
current: number;
total_page_count: number;
}
export interface PaginationInfinite {
cursor_is_reversed: boolean;
maybe_next?: string;
maybe_previous?: string;
}
@@ -0,0 +1,16 @@
export interface Prompts {
created_at: string;
lcm_disabled: boolean;
lipsync_enabled: boolean;
maybe_inference_duration_millis: number;
maybe_moderator_fields: string;
maybe_negative_prompt: string;
maybe_positive_prompt: string;
maybe_strength: number;
maybe_style_name: string;
prompt_type: string;
token: string;
use_cinematic: boolean;
used_face_detailer: boolean;
used_upscaler: boolean;
}
@@ -0,0 +1,9 @@
export interface StatusAlert {
maybe_alert?: MaybeAlert;
refresh_interval_millis: number;
}
interface MaybeAlert {
maybe_category?: string;
maybe_message?: string;
}
@@ -0,0 +1,75 @@
export interface UserBookmarkBatch {
entity_token: string;
entity_type: string;
is_bookmarked: boolean;
maybe_bookmark_token: string;
}
interface DefaultAvatar {
color_index: number;
image_index: number;
}
interface User {
default_avatar: DefaultAvatar;
display_name: string;
gravatar_hash: string;
user_token: string;
username: string;
}
export interface UserBookmarkByEntity {
created_at: string;
token: string;
updated_at: string;
user: User;
}
interface DefaultAvatar {
color_index: number;
image_index: number;
}
interface Creator {
default_avatar: DefaultAvatar;
display_name: string;
gravatar_hash: string;
user_token: string;
username: string;
}
interface MediaFileData {
maybe_creator: Creator;
media_type: string; // Adjust type as per your specific schema
public_bucket_path: string;
}
interface WeightData {
maybe_cover_image_public_bucket_path: string;
maybe_creator: Creator;
title: string;
weight_category: string; // Adjust type as per your specific schema
weight_type: string; // Adjust type as per your specific schema
}
interface Stats {
bookmark_count: number;
positive_rating_count: number;
}
interface Details {
entity_token: string;
entity_type: string; // Adjust type as per your specific schema
maybe_media_file_data?: MediaFileData;
maybe_summary_text?: string;
maybe_thumbnail_url?: string;
maybe_weight_data?: WeightData;
stats: Stats;
}
export interface UserBookmarkByUser {
created_at: string;
details: Details;
token: string;
updated_at: string;
}
+50
View File
@@ -0,0 +1,50 @@
import { USER_FEATURE_FLAGS } from "../../libs/api/src/lib/enums/UserFeatures";
export interface CoreInfo {
default_avatar: {
color_index: number;
image_index: number;
};
display_name: string;
gravatar_hash: string;
user_token: string;
username: string;
}
export interface UserInfo {
user_token: string;
username: string;
display_name: string;
email_gravatar_hash: string;
core_info: CoreInfo;
// Rollout feature flags
can_access_studio: boolean;
maybe_feature_flags: USER_FEATURE_FLAGS[];
// Usage
can_use_tts: boolean;
can_use_w2l: boolean;
can_delete_own_tts_results: boolean;
can_delete_own_w2l_results: boolean;
can_delete_own_account: boolean;
// Contribution
can_upload_tts_models: boolean;
can_upload_w2l_templates: boolean;
can_delete_own_tts_models: boolean;
can_delete_own_w2l_templates: boolean;
// Moderation
can_approve_w2l_templates: boolean;
can_edit_other_users_profiles: boolean;
can_edit_other_users_tts_models: boolean;
can_edit_other_users_w2l_templates: boolean;
can_delete_other_users_tts_models: boolean;
can_delete_other_users_tts_results: boolean;
can_delete_other_users_w2l_templates: boolean;
can_delete_other_users_w2l_results: boolean;
can_ban_users: boolean;
can_delete_users: boolean;
}
@@ -0,0 +1,40 @@
interface CoverImage {
default_cover: {
color_index: number;
image_index: number;
};
maybe_cover_image_public_bucket_path: string;
}
interface DefaultAvatar {
color_index: number;
image_index: number;
}
interface Creator {
default_avatar: DefaultAvatar;
display_name: string;
gravatar_hash: string;
user_token: string;
username: string;
}
interface Stats {
bookmark_count: number;
positive_rating_count: number;
}
export interface Weight {
cover_image: CoverImage;
created_at: string;
creator: Creator;
creator_set_visibility: string;
file_checksum_sha2: string;
file_size_bytes: number;
stats: Stats;
title: string;
updated_at: string;
weight_category: string;
weight_token: string;
weight_type: string;
}
+12
View File
@@ -0,0 +1,12 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
@@ -0,0 +1,7 @@
# @storyteller/ui-toaster
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test @storyteller/ui-toaster` to execute the unit tests via [Vitest](https://vitest.dev/).
@@ -0,0 +1,17 @@
{
"name": "@storyteller/ui-toaster",
"version": "0.0.1",
"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"
}
}
}
@@ -0,0 +1,2 @@
export * from './lib/toaster';
@@ -0,0 +1,7 @@
/*
* Replace this with your own classes
*
* e.g.
* .container {
* }
*/
@@ -0,0 +1,12 @@
import { render } from '@testing-library/react';
import Toaster from './toaster';
describe('Toaster', () => {
it('should render successfully', () => {
const { baseElement } = render(<Toaster />);
expect(baseElement).toBeTruthy();
});
});
@@ -0,0 +1,49 @@
import toast, { Toaster as RHToaster } from "react-hot-toast";
interface ToasterProps {
position?: "top-right" | "top-left" | "bottom-right" | "bottom-left";
offsetTop?: number;
offsetBottom?: number;
offsetLeft?: number;
offsetRight?: number;
zIndex?: number;
}
export function Toaster({
position = "top-right",
offsetTop = 12,
offsetBottom = 12,
offsetLeft = 12,
offsetRight = 12,
zIndex = 9,
}: ToasterProps) {
return (
<RHToaster
position={position}
toastOptions={{
success: {
style: {
background: "#ffffff",
},
},
error: {
style: {
background: "#ffffff",
},
},
}}
containerStyle={{
top: offsetTop,
left: offsetLeft,
bottom: offsetBottom,
right: offsetRight,
zIndex: zIndex,
}}
containerClassName="text-[15px] font-medium"
/>
);
}
export default Toaster;
export { toast };
@@ -0,0 +1,13 @@
{
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../../tsconfig.base.json"
}
@@ -0,0 +1,45 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"types": [
"node",
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts",
"vite/client"
],
"rootDir": "src",
"jsx": "react-jsx",
"tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo"
},
"exclude": [
"out-tsc",
"dist",
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx",
"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"
],
"include": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.ts",
"src/**/*.tsx"
]
}
@@ -0,0 +1,34 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./out-tsc/vitest",
"types": [
"vitest/globals",
"vitest/importMeta",
"vite/client",
"node",
"vitest"
],
"jsx": "react-jsx"
},
"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"
}
]
}
@@ -0,0 +1,49 @@
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import dts from 'vite-plugin-dts';
import * as path from 'path';
export default defineConfig(() => ({
root: __dirname,
cacheDir: '../../../node_modules/.vite/libs/components/toaster',
plugins: [react(), 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/ui-toaster',
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: ['react','react-dom','react/jsx-runtime']
},
},
test: {
watch: false,
globals: true,
environment: 'jsdom',
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,
}
},
}));
+12
View File
@@ -0,0 +1,12 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}

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