mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-24 18:59:28 -04:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aebf78f765 | ||
|
|
326fa8c4a4 | ||
|
|
51bdd4eff8 | ||
|
|
a64ca7d4ac | ||
|
|
ec3ac242b0 | ||
|
|
fbe7fa3c77 | ||
|
|
418d824c35 | ||
|
|
2e6823834f |
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
+67
-28
@@ -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(),
|
||||
|
||||
+17
-7
@@ -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(),
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<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"
|
||||
/>
|
||||
|
||||
@@ -17,7 +17,11 @@ import { SceneManager, SceneObject } from "./scene_manager_api";
|
||||
import { FreeCam } from "./free_cam";
|
||||
import Stats from "three/examples/jsm/libs/stats.module.js";
|
||||
import { FKHelper } from "./KinHelpers/FKHelper";
|
||||
import { selectedMode } from "../signals/selectedMode";
|
||||
import {
|
||||
selectedMode,
|
||||
poseMode,
|
||||
showPoseControls,
|
||||
} from "../signals/selectedMode";
|
||||
|
||||
export enum KinMode {
|
||||
FK,
|
||||
@@ -122,6 +126,10 @@ export class MouseControls {
|
||||
scene: this.scene,
|
||||
onDragChange: this.onFKControlsDragging.bind(this),
|
||||
});
|
||||
|
||||
// Expose handlePoseModeChange to window
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(window as any).__mouseControls = this;
|
||||
}
|
||||
|
||||
onFKControlsDragging(dragging: boolean) {
|
||||
@@ -185,7 +193,11 @@ export class MouseControls {
|
||||
this.setSelected(this.selected);
|
||||
this.publishSelect();
|
||||
|
||||
// this.editor.update_properties()
|
||||
if (currentObject.userData.isCharacter) {
|
||||
showPoseControls.value = true;
|
||||
}
|
||||
|
||||
// Normal selection behavior
|
||||
if (currentObject.userData["locked"] !== true && this.control) {
|
||||
this.scene.add(this.control);
|
||||
this.control.attach(currentObject);
|
||||
@@ -235,6 +247,9 @@ export class MouseControls {
|
||||
this.fkHelper.clear();
|
||||
this.kinMode = KinMode.NONE;
|
||||
this.reattachTransformControls();
|
||||
if (poseMode.value === "pose") {
|
||||
poseMode.value = "select";
|
||||
}
|
||||
console.log("FK mode off");
|
||||
return;
|
||||
}
|
||||
@@ -255,6 +270,9 @@ export class MouseControls {
|
||||
this.hideTransformControls();
|
||||
this.kinMode = KinMode.FK;
|
||||
this.fkHelper.setTarget(this.selected[0]);
|
||||
if (poseMode.value === "select") {
|
||||
poseMode.value = "pose";
|
||||
}
|
||||
console.log("FK mode on");
|
||||
return;
|
||||
}
|
||||
@@ -305,30 +323,20 @@ export class MouseControls {
|
||||
|
||||
if ((event.ctrlKey || event.metaKey) && !this.isProcessing) {
|
||||
if (event.key === "Z" || event.key === "z") {
|
||||
if (event.shiftKey) {
|
||||
// redo
|
||||
this.isProcessing = true;
|
||||
await this.sceneManager?.redo();
|
||||
this.isProcessing = false;
|
||||
} else {
|
||||
// undo
|
||||
this.isProcessing = true;
|
||||
await this.sceneManager?.undo();
|
||||
this.isProcessing = false;
|
||||
}
|
||||
return;
|
||||
// redo
|
||||
this.isProcessing = true;
|
||||
await this.sceneManager?.redo();
|
||||
this.isProcessing = false;
|
||||
} else if (event.key === "c") {
|
||||
// redo
|
||||
this.isProcessing = true;
|
||||
await this.sceneManager?.copy();
|
||||
this.isProcessing = false;
|
||||
return;
|
||||
} else if (event.key === "v") {
|
||||
// redo
|
||||
this.isProcessing = true;
|
||||
await this.sceneManager?.paste();
|
||||
this.isProcessing = false;
|
||||
return;
|
||||
} else if (event.key === "0") {
|
||||
// Stats Menu
|
||||
this.enable_stats();
|
||||
@@ -454,7 +462,8 @@ export class MouseControls {
|
||||
this.selected = [];
|
||||
this.setSelected(this.selected);
|
||||
this.removeTransformControls();
|
||||
//hideObjectPanel();
|
||||
hideObjectPanel();
|
||||
showPoseControls.value = false;
|
||||
}
|
||||
|
||||
if (this.sceneManager) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ControlPanelSceneObject } from "./comps/ControlPanelSceneObject";
|
||||
import { PreviewEngineCamera } from "./comps/PreviewEngineCamera";
|
||||
import { PreviewFrameImage } from "./comps/PreviewFrameImage";
|
||||
import { pageHeight, pageWidth } from "~/signals";
|
||||
import { PoseModeSelector } from "./comps/PoseModeSelector";
|
||||
|
||||
import {
|
||||
timelineHeight,
|
||||
@@ -24,7 +25,6 @@ import { Outliner } from "./comps/Outliner";
|
||||
import { CameraAspectRatio } from "./enums";
|
||||
import { PromptBox } from "./comps/PromptBox";
|
||||
import { OnboardingHelper } from "./comps/OnboardingHelper";
|
||||
import { focalLengthDragging } from "./signals/camera";
|
||||
import { FocalLengthDisplay } from "./comps/FocalLengthDisplay/FocalLengthDisplay";
|
||||
|
||||
export const PageEditor = () => {
|
||||
@@ -106,6 +106,9 @@ export const PageEditor = () => {
|
||||
{/* Focal Length Display */}
|
||||
<FocalLengthDisplay />
|
||||
|
||||
{/* Pose Mode Selector */}
|
||||
<PoseModeSelector />
|
||||
|
||||
{/* Top controls */}
|
||||
<div
|
||||
className="absolute left-0 top-0 w-full"
|
||||
|
||||
@@ -16,6 +16,8 @@ import * as gpu from "detect-gpu";
|
||||
import { UsersApi } from "~/Classes/ApiManager";
|
||||
import { PageEnigmaComponent } from "~/pages/PageEnigma/PageEnigmaComponent";
|
||||
import { TurnOnGpu } from "~/pages/PageEnigma/TurnOnGpu";
|
||||
import { PrecisionSelector } from "./comps/PrecisionSelector/PrecisionSelector";
|
||||
import { precisionSelectedValue, precisionSelectorMenuCoords, precisionSelectorValues, showPrecisionSelector } from "./signals/precisionSelectorMenu";
|
||||
|
||||
export const PageEnigma = ({ sceneToken }: { sceneToken?: string }) => {
|
||||
useSignals();
|
||||
@@ -71,7 +73,7 @@ export const PageEnigma = ({ sceneToken }: { sceneToken?: string }) => {
|
||||
case "apple gpu (Apple GPU)":
|
||||
// TODO(bt,2025-04-08): We may want to disable this heuristic.
|
||||
// We're getting lack of hardware acceleration using Tauri on Mac and Linux.
|
||||
isValid = true;
|
||||
isValid = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -93,6 +95,12 @@ export const PageEnigma = ({ sceneToken }: { sceneToken?: string }) => {
|
||||
<EngineProvider sceneToken={sceneToken}>
|
||||
<PageEnigmaComponent />
|
||||
<DragComponent />
|
||||
<PrecisionSelector
|
||||
showSignal={showPrecisionSelector}
|
||||
coordSignal={precisionSelectorMenuCoords}
|
||||
valuesSignal={precisionSelectorValues}
|
||||
selectedValueSignal={precisionSelectedValue}
|
||||
/>
|
||||
<GenerateModals />
|
||||
<ErrorDialog />
|
||||
<Wizard />
|
||||
|
||||
+13
-1
@@ -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()}
|
||||
|
||||
+101
@@ -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>
|
||||
)
|
||||
}
|
||||
+1
-1
@@ -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>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { activeImageGenerationJobs, toasts, userMovies } from "~/signals";
|
||||
import { useEffect, useState } from "react";
|
||||
import { JobsApi } from "~/Classes/ApiManager/JobsApi";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { ActiveJob } from "~/pages/PageEnigma/models";
|
||||
// TODO ensure we de-dupe all this extra code.
|
||||
interface CompletedItem {
|
||||
token: string;
|
||||
@@ -18,17 +18,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": {
|
||||
@@ -80,65 +69,78 @@ export function Activity() {
|
||||
try {
|
||||
const jobsApi = new JobsApi();
|
||||
const jobsResponse = await jobsApi.ListRecentJobs();
|
||||
|
||||
|
||||
if (jobsResponse.success && jobsResponse.data) {
|
||||
// Process active jobs
|
||||
const activeJobs = jobsResponse.data
|
||||
.filter(job => {
|
||||
.filter((job) => {
|
||||
const status = job.status?.status?.toLowerCase();
|
||||
return status && status !== "complete_success" && status !== "failed";
|
||||
return (
|
||||
status && status !== "complete_success" && status !== "failed"
|
||||
);
|
||||
})
|
||||
.map(job => ({
|
||||
.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
|
||||
}
|
||||
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");
|
||||
|
||||
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));
|
||||
|
||||
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 => ({
|
||||
.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()
|
||||
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`;
|
||||
const message =
|
||||
count === 1
|
||||
? `${newCompletedItems[0].maybe_title} completed successfully`
|
||||
: `${count} images completed successfully`;
|
||||
toast.success(message);
|
||||
}
|
||||
|
||||
// Update completed items with a maximum limit of 50 items
|
||||
setCompletedItems(prev => {
|
||||
setCompletedItems((prev) => {
|
||||
const combined = [...newCompletedItems, ...prev];
|
||||
// Remove duplicates based on token
|
||||
const uniqueItems = Array.from(
|
||||
new Map(combined.map(item => [item.token, item])).values()
|
||||
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())
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(b.updated_at).getTime() -
|
||||
new Date(a.updated_at).getTime(),
|
||||
)
|
||||
.slice(0, 50);
|
||||
});
|
||||
setJobs(activeJobs);
|
||||
setJobs(activeJobs as ActiveJob[]);
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -158,6 +160,7 @@ export function Activity() {
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -170,23 +173,26 @@ export function Activity() {
|
||||
align="end"
|
||||
triggerIcon={
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faBell} />
|
||||
{activeImageGenerationJobs.value &&
|
||||
activeImageGenerationJobs.value.length > 0 && (
|
||||
<div className="bg-blue-500 absolute -right-1 -top-1 h-3 w-3 animate-pulse rounded-full" />
|
||||
)}
|
||||
{jobs && jobs.length > 0 ? (
|
||||
<FontAwesomeIcon icon={faSpinnerThird} className="animate-spin" />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faBell} />
|
||||
)}
|
||||
{jobs && jobs.length > 0 && (
|
||||
<div className="bg-blue-500 absolute -right-1 -top-1 h-3 w-3 animate-pulse rounded-full" />
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="max-h-[480px] overflow-y-auto">
|
||||
{/* In progress */}
|
||||
{jobs && jobs.length > 0 && (
|
||||
<div>
|
||||
{jobs.map((job) => (
|
||||
<InProgressCard key={job.job_token} movie={job} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
{jobs.map((job) => (
|
||||
<InProgressCard key={job.job_token} job={job} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!userMovies.value && !activeImageGenerationJobs.value ? (
|
||||
<div className="flex h-48 w-full flex-col justify-center gap-4 p-4 text-center align-middle">
|
||||
@@ -212,16 +218,10 @@ export function Activity() {
|
||||
{/* 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>
|
||||
|
||||
+65
-79
@@ -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>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
+19
-25
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+43
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
import os
|
||||
import hashlib
|
||||
import filecmp
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
import difflib
|
||||
|
||||
|
||||
def get_file_hash(filepath):
|
||||
"""Calculate MD5 hash of a file."""
|
||||
md5_hash = hashlib.md5()
|
||||
try:
|
||||
with open(filepath, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(4096), b""):
|
||||
md5_hash.update(chunk)
|
||||
return md5_hash.hexdigest()
|
||||
except Exception:
|
||||
return "Error calculating hash"
|
||||
|
||||
|
||||
def format_size(size_bytes):
|
||||
"""Format file size nicely."""
|
||||
if size_bytes < 1000:
|
||||
return f"{size_bytes}B"
|
||||
elif size_bytes < 1000000:
|
||||
return f"{size_bytes / 1000:.1f}KB"
|
||||
else:
|
||||
return f"{size_bytes / 1000000:.1f}MB"
|
||||
|
||||
|
||||
def count_lines(filepath):
|
||||
"""Count lines in a file safely."""
|
||||
try:
|
||||
with open(filepath, "rb") as f:
|
||||
return sum(1 for _ in f)
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
||||
def calculate_diff_ratio(file1, file2):
|
||||
"""Calculate how different two files are using difflib."""
|
||||
try:
|
||||
with (
|
||||
open(file1, "r", errors="replace") as f1,
|
||||
open(file2, "r", errors="replace") as f2,
|
||||
):
|
||||
text1 = f1.readlines()
|
||||
text2 = f2.readlines()
|
||||
matcher = difflib.SequenceMatcher(None, text1, text2)
|
||||
return 1.0 - matcher.ratio() # Higher value means more different
|
||||
except Exception:
|
||||
return 1.0 # If we can't compare, assume completely different
|
||||
|
||||
|
||||
def get_relative_path(file_path, base_path):
|
||||
"""Get relative path from base directory."""
|
||||
return str(Path(file_path).relative_to(base_path))
|
||||
|
||||
|
||||
def collect_files_recursive(folder_path):
|
||||
"""Recursively collect all files in a folder."""
|
||||
folder_path = Path(folder_path)
|
||||
file_dict = {}
|
||||
|
||||
for path in folder_path.rglob("*"):
|
||||
if path.is_file():
|
||||
rel_path = get_relative_path(path, folder_path)
|
||||
file_dict[rel_path] = path
|
||||
|
||||
return file_dict
|
||||
|
||||
|
||||
def compare_folders_recursive(folder1, folder2, output_file=None):
|
||||
"""Compare two folders recursively and generate a detailed report."""
|
||||
# Automatically generate output filename if not specified
|
||||
if output_file is None:
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
output_file = f"folder_comparison_{timestamp}.md"
|
||||
|
||||
# Get folder paths
|
||||
folder1_path = Path(folder1)
|
||||
folder2_path = Path(folder2)
|
||||
|
||||
# Get shorthand names for display
|
||||
folder1_name = folder1_path.name
|
||||
folder2_name = folder2_path.name
|
||||
|
||||
# Get all files recursively
|
||||
print(f"Collecting files from {folder1_name}...")
|
||||
files1 = collect_files_recursive(folder1_path)
|
||||
print(f"Collecting files from {folder2_name}...")
|
||||
files2 = collect_files_recursive(folder2_path)
|
||||
|
||||
# All unique file paths across both folders
|
||||
all_files = sorted(set(files1.keys()) | set(files2.keys()))
|
||||
|
||||
# Calculate column widths for alignment
|
||||
max_filepath_len = max(len(file) for file in all_files) + 2
|
||||
hash_col_width = 10 # For hash comparison
|
||||
size_col_width = 20 # For size and line count
|
||||
diff_col_width = 30 # For difference description
|
||||
|
||||
# Initialize report
|
||||
report = [
|
||||
f"# Recursive Folder Comparison Report: {folder1_name} vs {folder2_name}\n",
|
||||
f"Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n",
|
||||
"## File Comparison Table\n",
|
||||
f"| {'File Path'.ljust(max_filepath_len)} | {'Status'.ljust(hash_col_width)} | {folder1_name.ljust(size_col_width)} | {folder2_name.ljust(size_col_width)} | {'Difference'.ljust(diff_col_width)} |",
|
||||
f"|{'-' * max_filepath_len}|{'-' * hash_col_width}|{'-' * size_col_width}|{'-' * size_col_width}|{'-' * diff_col_width}|",
|
||||
]
|
||||
|
||||
# Track file categories
|
||||
identical_files = []
|
||||
different_files = []
|
||||
only_in_folder1 = []
|
||||
only_in_folder2 = []
|
||||
|
||||
# Store detailed file comparisons for sorting later
|
||||
file_comparisons = []
|
||||
|
||||
# Compare each file
|
||||
print(f"Comparing {len(all_files)} unique files...")
|
||||
for rel_path in all_files:
|
||||
in_folder1 = rel_path in files1
|
||||
in_folder2 = rel_path in files2
|
||||
|
||||
# Default values
|
||||
status = "—"
|
||||
size1_str = "—"
|
||||
size2_str = "—"
|
||||
diff_str = "—"
|
||||
diff_ratio = 0
|
||||
|
||||
# Get details for folder1
|
||||
if in_folder1:
|
||||
file1 = files1[rel_path]
|
||||
size1 = file1.stat().st_size
|
||||
lines1 = count_lines(file1)
|
||||
hash1 = get_file_hash(file1)
|
||||
size1_str = f"{format_size(size1)} ({lines1} lines)"
|
||||
else:
|
||||
hash1 = "—"
|
||||
size1 = 0
|
||||
lines1 = 0
|
||||
|
||||
# Get details for folder2
|
||||
if in_folder2:
|
||||
file2 = files2[rel_path]
|
||||
size2 = file2.stat().st_size
|
||||
lines2 = count_lines(file2)
|
||||
hash2 = get_file_hash(file2)
|
||||
size2_str = f"{format_size(size2)} ({lines2} lines)"
|
||||
else:
|
||||
hash2 = "—"
|
||||
size2 = 0
|
||||
lines2 = 0
|
||||
|
||||
# Calculate difference and determine status
|
||||
if not in_folder1:
|
||||
status = "MISSING"
|
||||
diff_str = f"Only in {folder2_name}"
|
||||
only_in_folder2.append(rel_path)
|
||||
diff_ratio = 1.0 # Completely different (missing)
|
||||
elif not in_folder2:
|
||||
status = "MISSING"
|
||||
diff_str = f"Only in {folder1_name}"
|
||||
only_in_folder1.append(rel_path)
|
||||
diff_ratio = 1.0 # Completely different (missing)
|
||||
else:
|
||||
# Both files exist, compare them
|
||||
if hash1 == hash2 and hash1 != "Error calculating hash":
|
||||
status = "IDENTICAL"
|
||||
diff_str = "Identical content"
|
||||
identical_files.append(rel_path)
|
||||
diff_ratio = 0.0
|
||||
else:
|
||||
status = "DIFFERENT"
|
||||
size_diff = size2 - size1
|
||||
lines_diff = lines2 - lines1
|
||||
sign = "+" if size_diff > 0 else ""
|
||||
sign_lines = "+" if lines_diff > 0 else ""
|
||||
|
||||
# Calculate how different the files are
|
||||
diff_ratio = calculate_diff_ratio(file1, file2)
|
||||
different_files.append((rel_path, diff_ratio))
|
||||
|
||||
# Format difference description
|
||||
diff_pct = int(diff_ratio * 100)
|
||||
diff_str = f"{sign}{format_size(size_diff)} ({sign_lines}{lines_diff} lines) {diff_pct}% different"
|
||||
|
||||
# Store comparison for sorting later
|
||||
file_comparisons.append(
|
||||
{
|
||||
"rel_path": rel_path,
|
||||
"status": status,
|
||||
"size1_str": size1_str,
|
||||
"size2_str": size2_str,
|
||||
"diff_str": diff_str,
|
||||
"diff_ratio": diff_ratio,
|
||||
"in_folder1": in_folder1,
|
||||
"in_folder2": in_folder2,
|
||||
"hash1": hash1[:8] if len(hash1) > 8 else hash1,
|
||||
"hash2": hash2[:8] if len(hash2) > 8 else hash2,
|
||||
}
|
||||
)
|
||||
|
||||
# Sort files by difference ratio (most different first)
|
||||
file_comparisons.sort(key=lambda x: x["diff_ratio"], reverse=True)
|
||||
|
||||
# Generate the sorted table
|
||||
for comp in file_comparisons:
|
||||
hash_display = (
|
||||
f"{comp['hash1']}:{comp['hash2']}"
|
||||
if comp["status"] == "DIFFERENT"
|
||||
else comp["status"]
|
||||
)
|
||||
report.append(
|
||||
f"| {comp['rel_path'].ljust(max_filepath_len)} | "
|
||||
f"{hash_display.ljust(hash_col_width)} | "
|
||||
f"{comp['size1_str'].ljust(size_col_width)} | "
|
||||
f"{comp['size2_str'].ljust(size_col_width)} | "
|
||||
f"{comp['diff_str'].ljust(diff_col_width)} |"
|
||||
)
|
||||
|
||||
# Sort different files by difference ratio for the detailed section
|
||||
different_files.sort(key=lambda x: x[1], reverse=True)
|
||||
|
||||
# Add Files Missing From Each Folder sections
|
||||
report.append("\n## Files Present in One Folder But Missing in the Other\n")
|
||||
|
||||
if only_in_folder1:
|
||||
report.append(
|
||||
f"\n### Files Only in {folder1_name} ({len(only_in_folder1)} files)"
|
||||
)
|
||||
for rel_path in sorted(only_in_folder1):
|
||||
file1 = files1[rel_path]
|
||||
size1 = file1.stat().st_size
|
||||
lines1 = count_lines(file1)
|
||||
hash1 = get_file_hash(file1)[:8]
|
||||
report.append(
|
||||
f"- `{rel_path}` ({format_size(size1)}, {lines1} lines, hash: {hash1})"
|
||||
)
|
||||
|
||||
if only_in_folder2:
|
||||
report.append(
|
||||
f"\n### Files Only in {folder2_name} ({len(only_in_folder2)} files)"
|
||||
)
|
||||
for rel_path in sorted(only_in_folder2):
|
||||
file2 = files2[rel_path]
|
||||
size2 = file2.stat().st_size
|
||||
lines2 = count_lines(file2)
|
||||
hash2 = get_file_hash(file2)[:8]
|
||||
report.append(
|
||||
f"- `{rel_path}` ({format_size(size2)}, {lines2} lines, hash: {hash2})"
|
||||
)
|
||||
|
||||
# Add detailed section for different files
|
||||
if different_files:
|
||||
report.append(f"\n## Files with Differences (Sorted by Most Different First)\n")
|
||||
for rel_path, diff_ratio in different_files:
|
||||
file1 = files1[rel_path]
|
||||
file2 = files2[rel_path]
|
||||
size1 = file1.stat().st_size
|
||||
size2 = file2.stat().st_size
|
||||
lines1 = count_lines(file1)
|
||||
lines2 = count_lines(file2)
|
||||
hash1 = get_file_hash(file1)[:8]
|
||||
hash2 = get_file_hash(file2)[:8]
|
||||
size_diff = size2 - size1
|
||||
lines_diff = lines2 - lines1
|
||||
diff_pct = int(diff_ratio * 100)
|
||||
|
||||
report.append(f"### `{rel_path}` ({diff_pct}% different)")
|
||||
report.append(
|
||||
f"- {folder1_name}: {format_size(size1)}, {lines1} lines, hash: {hash1}"
|
||||
)
|
||||
report.append(
|
||||
f"- {folder2_name}: {format_size(size2)}, {lines2} lines, hash: {hash2}"
|
||||
)
|
||||
report.append(
|
||||
f"- Difference: {size_diff:+d} bytes, {lines_diff:+d} lines\n"
|
||||
)
|
||||
|
||||
# Add summary section
|
||||
report.extend(
|
||||
[
|
||||
"\n## Summary",
|
||||
f"- **Total Unique Files**: {len(all_files)}",
|
||||
f"- **Files in {folder1_name}**: {len(files1)}",
|
||||
f"- **Files in {folder2_name}**: {len(files2)}",
|
||||
f"- **Identical Files**: {len(identical_files)}",
|
||||
f"- **Different Files**: {len(different_files)}",
|
||||
f"- **Files Only in {folder1_name}**: {len(only_in_folder1)}",
|
||||
f"- **Files Only in {folder2_name}**: {len(only_in_folder2)}",
|
||||
f"\n## Path Information",
|
||||
f"- {folder1_name}: `{folder1}`",
|
||||
f"- {folder2_name}: `{folder2}`",
|
||||
]
|
||||
)
|
||||
|
||||
# Write report to file
|
||||
with open(output_file, "w") as f:
|
||||
f.write("\n".join(report))
|
||||
|
||||
print(f"Recursive comparison report written to {output_file}")
|
||||
return "\n".join(report)
|
||||
|
||||
|
||||
# Execute the comparison
|
||||
if __name__ == "__main__":
|
||||
folder1 = "/Users/michaelchung/storyteller-rust/frontend/apps/editor2d/src/Classes/"
|
||||
folder2 = "/Users/michaelchung/storyteller-rust/frontend/apps/editor3d/app/Classes/"
|
||||
|
||||
report = compare_folders_recursive(folder1, folder2)
|
||||
@@ -0,0 +1,165 @@
|
||||
# Recursive Folder Comparison Report: Classes vs Classes
|
||||
|
||||
Generated on: 2025-04-16 13:33:19
|
||||
|
||||
## File Comparison Table
|
||||
|
||||
| File Path | Status | Classes | Classes | Difference |
|
||||
|------------------------------------|----------|--------------------|--------------------|------------------------------|
|
||||
| ApiHelpers.ts | MISSING | — | 637B (17 lines) | Only in Classes |
|
||||
| ApiManager/SegmentationApi.ts | MISSING | 1.9KB (84 lines) | — | Only in Classes |
|
||||
| ApiManager/StudioGen2Api.ts | MISSING | — | 1.6KB (61 lines) | Only in Classes |
|
||||
| EnvironmentVariables.ts | MISSING | — | 263B (10 lines) | Only in Classes |
|
||||
| GetFrontendEnvironment.ts | MISSING | — | 1.9KB (65 lines) | Only in Classes |
|
||||
| ApiManager/MediaUploadApi.ts | 707d2bf2:06a93d8c | 10.3KB (349 lines) | 10.0KB (339 lines) | -255B (-10 lines) 72% different |
|
||||
| ApiManager/EngineApi.ts | c4850e80:85e20a81 | 1.0KB (39 lines) | 5.5KB (213 lines) | +4.5KB (+174 lines) 69% different |
|
||||
| ApiManager/JobsApi.ts | f7eb8e22:2aa03349 | 2.0KB (77 lines) | 1.9KB (74 lines) | -56B (-3 lines) 28% different |
|
||||
| ApiManager/ApiManager.ts | 44db0b07:67a3a046 | 5.5KB (235 lines) | 5.3KB (211 lines) | -200B (-24 lines) 14% different |
|
||||
| ApiManager/BillingApi.ts | 20051893:f5b7af84 | 1.0KB (33 lines) | 1.0KB (33 lines) | -8B (0 lines) 6% different |
|
||||
| ApiManager/VoiceDesignerApi.ts | e4d874c3:3635ba42 | 2.0KB (77 lines) | 2.1KB (78 lines) | +94B (+1 lines) 5% different |
|
||||
| ApiManager/MediaFilesApi.ts | be67aab2:44342e0f | 10.6KB (364 lines) | 10.6KB (369 lines) | +56B (+5 lines) 5% different |
|
||||
| ApiManager/PromptsApi.ts | d73d329a:c4824d2e | 648B (25 lines) | 657B (25 lines) | +9B (0 lines) 4% different |
|
||||
| ApiManager/MiscApi.ts | 298f5d93:187ad676 | 799B (26 lines) | 799B (26 lines) | 0B (0 lines) 3% different |
|
||||
| ApiManager/TtsApi.ts | 5feb5cad:6228b278 | 1.7KB (64 lines) | 1.7KB (64 lines) | +2B (0 lines) 3% different |
|
||||
| ApiManager/index.ts | 55a9094b:1fe59701 | 564B (18 lines) | 597B (19 lines) | +33B (+1 lines) 2% different |
|
||||
| ApiManager/UserBookmarksApi.ts | e3e4cb19:fd3afc78 | 5.4KB (201 lines) | 5.5KB (202 lines) | +56B (+1 lines) 2% different |
|
||||
| ApiManager/WeightsApi.ts | c12a98df:8a7dbb68 | 7.8KB (295 lines) | 7.9KB (296 lines) | +39B (+1 lines) 2% different |
|
||||
| ApiManager/VideoApi.ts | a9947328:cb947c07 | 1.9KB (70 lines) | 1.9KB (70 lines) | -11B (0 lines) 1% different |
|
||||
| ApiManager/BetaKeysApi.ts | 8d4ec6f1:33f1e4b3 | 4.3KB (167 lines) | 4.3KB (167 lines) | -3B (0 lines) 1% different |
|
||||
| ApiManager/UsersApi.ts | 65121217:72db0eae | 3.9KB (161 lines) | 3.9KB (161 lines) | -6B (0 lines) 0% different |
|
||||
| ApiManager/AnalyticsApi.ts | IDENTICAL | 944B (38 lines) | 944B (38 lines) | Identical content |
|
||||
| ApiManager/CommentsApi.ts | IDENTICAL | 2.3KB (91 lines) | 2.3KB (91 lines) | Identical content |
|
||||
| ApiManager/FeaturedItemsApi.ts | IDENTICAL | 2.3KB (96 lines) | 2.3KB (96 lines) | Identical content |
|
||||
| ApiManager/ModerationApi.ts | IDENTICAL | 960B (42 lines) | 960B (42 lines) | Identical content |
|
||||
| ApiManager/UserRatingsApi.ts | IDENTICAL | 2.4KB (92 lines) | 2.4KB (92 lines) | Identical content |
|
||||
| ApiManager/VoiceConversionApi.ts | IDENTICAL | 1.4KB (53 lines) | 1.4KB (53 lines) | Identical content |
|
||||
|
||||
## Files Present in One Folder But Missing in the Other
|
||||
|
||||
|
||||
### Files Only in Classes (23 files)
|
||||
- `ApiManager/SegmentationApi.ts` (1.9KB, 84 lines, hash: bb14fbf6)
|
||||
- `ApiManager/enums/Api.ts` (254B, 7 lines, hash: ea867d98)
|
||||
- `ApiManager/enums/Billing.ts` (242B, 11 lines, hash: e47c3eaa)
|
||||
- `ApiManager/enums/Job.ts` (433B, 16 lines, hash: d148fd64)
|
||||
- `ApiManager/enums/MediaFileClass.ts` (524B, 26 lines, hash: c90ce542)
|
||||
- `ApiManager/enums/MediaFileType.ts` (1.7KB, 58 lines, hash: a2374fec)
|
||||
- `ApiManager/enums/QueryFilters.ts` (579B, 29 lines, hash: 6d10a292)
|
||||
- `ApiManager/enums/Tts.ts` (205B, 7 lines, hash: f6393d2e)
|
||||
- `ApiManager/enums/UserFeatures.ts` (167B, 6 lines, hash: 05d49e51)
|
||||
- `ApiManager/enums/Visibility.ts` (92B, 5 lines, hash: 297d031b)
|
||||
- `ApiManager/enums/WeightCategory.ts` (180B, 8 lines, hash: f9be3d32)
|
||||
- `ApiManager/enums/WeightType.ts` (208B, 11 lines, hash: 80436726)
|
||||
- `ApiManager/models/BetaKeys.ts` (502B, 26 lines, hash: 7c718ac6)
|
||||
- `ApiManager/models/Billing.tsx` (416B, 17 lines, hash: 8db6eb3b)
|
||||
- `ApiManager/models/Dataset.ts` (588B, 23 lines, hash: 6af860ff)
|
||||
- `ApiManager/models/Job.ts` (1.8KB, 72 lines, hash: 9d7b385a)
|
||||
- `ApiManager/models/MediaFile.ts` (1.8KB, 59 lines, hash: 41778349)
|
||||
- `ApiManager/models/Pagination.ts` (200B, 9 lines, hash: 837b24d3)
|
||||
- `ApiManager/models/Prompts.ts` (424B, 16 lines, hash: 8ef1f366)
|
||||
- `ApiManager/models/StatusAlert.ts` (175B, 9 lines, hash: fcee4684)
|
||||
- `ApiManager/models/UserBookmark.ts` (1.6KB, 75 lines, hash: 8f70d1b1)
|
||||
- `ApiManager/models/Users.ts` (1.3KB, 50 lines, hash: 5ed82bbd)
|
||||
- `ApiManager/models/Weight.ts` (767B, 40 lines, hash: 3f9cccdb)
|
||||
|
||||
### Files Only in Classes (4 files)
|
||||
- `ApiHelpers.ts` (637B, 17 lines, hash: 9b638c6c)
|
||||
- `ApiManager/StudioGen2Api.ts` (1.6KB, 61 lines, hash: b2707195)
|
||||
- `EnvironmentVariables.ts` (263B, 10 lines, hash: 29c93666)
|
||||
- `GetFrontendEnvironment.ts` (1.9KB, 65 lines, hash: 7ee8b738)
|
||||
|
||||
## Files with Differences (Sorted by Most Different First)
|
||||
|
||||
### `ApiManager/MediaUploadApi.ts` (72% different)
|
||||
- Classes: 10.3KB, 349 lines, hash: 707d2bf2
|
||||
- Classes: 10.0KB, 339 lines, hash: 06a93d8c
|
||||
- Difference: -255 bytes, -10 lines
|
||||
|
||||
### `ApiManager/EngineApi.ts` (69% different)
|
||||
- Classes: 1.0KB, 39 lines, hash: c4850e80
|
||||
- Classes: 5.5KB, 213 lines, hash: 85e20a81
|
||||
- Difference: +4469 bytes, +174 lines
|
||||
|
||||
### `ApiManager/JobsApi.ts` (28% different)
|
||||
- Classes: 2.0KB, 77 lines, hash: f7eb8e22
|
||||
- Classes: 1.9KB, 74 lines, hash: 2aa03349
|
||||
- Difference: -56 bytes, -3 lines
|
||||
|
||||
### `ApiManager/ApiManager.ts` (14% different)
|
||||
- Classes: 5.5KB, 235 lines, hash: 44db0b07
|
||||
- Classes: 5.3KB, 211 lines, hash: 67a3a046
|
||||
- Difference: -200 bytes, -24 lines
|
||||
|
||||
### `ApiManager/BillingApi.ts` (6% different)
|
||||
- Classes: 1.0KB, 33 lines, hash: 20051893
|
||||
- Classes: 1.0KB, 33 lines, hash: f5b7af84
|
||||
- Difference: -8 bytes, +0 lines
|
||||
|
||||
### `ApiManager/VoiceDesignerApi.ts` (5% different)
|
||||
- Classes: 2.0KB, 77 lines, hash: e4d874c3
|
||||
- Classes: 2.1KB, 78 lines, hash: 3635ba42
|
||||
- Difference: +94 bytes, +1 lines
|
||||
|
||||
### `ApiManager/MediaFilesApi.ts` (5% different)
|
||||
- Classes: 10.6KB, 364 lines, hash: be67aab2
|
||||
- Classes: 10.6KB, 369 lines, hash: 44342e0f
|
||||
- Difference: +56 bytes, +5 lines
|
||||
|
||||
### `ApiManager/PromptsApi.ts` (4% different)
|
||||
- Classes: 648B, 25 lines, hash: d73d329a
|
||||
- Classes: 657B, 25 lines, hash: c4824d2e
|
||||
- Difference: +9 bytes, +0 lines
|
||||
|
||||
### `ApiManager/MiscApi.ts` (3% different)
|
||||
- Classes: 799B, 26 lines, hash: 298f5d93
|
||||
- Classes: 799B, 26 lines, hash: 187ad676
|
||||
- Difference: +0 bytes, +0 lines
|
||||
|
||||
### `ApiManager/TtsApi.ts` (3% different)
|
||||
- Classes: 1.7KB, 64 lines, hash: 5feb5cad
|
||||
- Classes: 1.7KB, 64 lines, hash: 6228b278
|
||||
- Difference: +2 bytes, +0 lines
|
||||
|
||||
### `ApiManager/index.ts` (2% different)
|
||||
- Classes: 564B, 18 lines, hash: 55a9094b
|
||||
- Classes: 597B, 19 lines, hash: 1fe59701
|
||||
- Difference: +33 bytes, +1 lines
|
||||
|
||||
### `ApiManager/UserBookmarksApi.ts` (2% different)
|
||||
- Classes: 5.4KB, 201 lines, hash: e3e4cb19
|
||||
- Classes: 5.5KB, 202 lines, hash: fd3afc78
|
||||
- Difference: +56 bytes, +1 lines
|
||||
|
||||
### `ApiManager/WeightsApi.ts` (2% different)
|
||||
- Classes: 7.8KB, 295 lines, hash: c12a98df
|
||||
- Classes: 7.9KB, 296 lines, hash: 8a7dbb68
|
||||
- Difference: +39 bytes, +1 lines
|
||||
|
||||
### `ApiManager/VideoApi.ts` (1% different)
|
||||
- Classes: 1.9KB, 70 lines, hash: a9947328
|
||||
- Classes: 1.9KB, 70 lines, hash: cb947c07
|
||||
- Difference: -11 bytes, +0 lines
|
||||
|
||||
### `ApiManager/BetaKeysApi.ts` (1% different)
|
||||
- Classes: 4.3KB, 167 lines, hash: 8d4ec6f1
|
||||
- Classes: 4.3KB, 167 lines, hash: 33f1e4b3
|
||||
- Difference: -3 bytes, +0 lines
|
||||
|
||||
### `ApiManager/UsersApi.ts` (0% different)
|
||||
- Classes: 3.9KB, 161 lines, hash: 65121217
|
||||
- Classes: 3.9KB, 161 lines, hash: 72db0eae
|
||||
- Difference: -6 bytes, +0 lines
|
||||
|
||||
|
||||
## Summary
|
||||
- **Total Unique Files**: 49
|
||||
- **Files in Classes**: 45
|
||||
- **Files in Classes**: 26
|
||||
- **Identical Files**: 6
|
||||
- **Different Files**: 16
|
||||
- **Files Only in Classes**: 23
|
||||
- **Files Only in Classes**: 4
|
||||
|
||||
## Path Information
|
||||
- Classes: `/Users/michaelchung/storyteller-rust/frontend/apps/editor2d/src/Classes/`
|
||||
- Classes: `/Users/michaelchung/storyteller-rust/frontend/apps/editor3d/app/Classes/`
|
||||
@@ -0,0 +1,38 @@
|
||||
import { ApiManager, ApiResponse } from "./ApiManager";
|
||||
|
||||
export class AnalyticsApi extends ApiManager {
|
||||
public PostAnalytics({
|
||||
maybeLastAction,
|
||||
maybeLogToken,
|
||||
}: {
|
||||
maybeLastAction: string;
|
||||
maybeLogToken: string;
|
||||
}): Promise<ApiResponse<string>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/analytics/log_session`;
|
||||
|
||||
const body = {
|
||||
maybe_last_action: maybeLastAction,
|
||||
maybe_log_token: maybeLogToken,
|
||||
};
|
||||
|
||||
return this.post<
|
||||
{
|
||||
maybe_last_action: string;
|
||||
maybe_log_token: string;
|
||||
},
|
||||
{
|
||||
success?: boolean;
|
||||
log_token?: string;
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint, body })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
data: response.log_token,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// import { GetCdnOrigin } from "~/api/GetCdnOrigin";
|
||||
// import environmentVariables from "./EnvironmentVariables";
|
||||
|
||||
// export async function get_media_url(mediaId: string) {
|
||||
// //This is for prod when we have the proper info on the url.
|
||||
// const api_base_url = environmentVariables.values.BASE_API;
|
||||
// const url = `${api_base_url}/v1/media_files/file/${mediaId}`;
|
||||
|
||||
// const response = await fetch(url);
|
||||
// const json = await JSON.parse(await response.text());
|
||||
// const bucketPath = json["media_file"]["public_bucket_path"];
|
||||
|
||||
// const media_api_base_url = GetCdnOrigin();
|
||||
|
||||
// const media_url = `${media_api_base_url}${bucketPath}`;
|
||||
// return media_url;
|
||||
// }
|
||||
@@ -0,0 +1,235 @@
|
||||
// import { bool } from "@techstark/opencv-js";
|
||||
import { API_TARGETS } from "../enums/Api";
|
||||
|
||||
type NonNullableObject<T extends object> = NonNullable<T>;
|
||||
|
||||
export interface ApiResponse<T, P = undefined> {
|
||||
success: boolean;
|
||||
errorMessage?: string;
|
||||
data?: T;
|
||||
pagination?: P;
|
||||
}
|
||||
|
||||
export class ApiManager {
|
||||
ApiTargets: Record<string, string> = {};
|
||||
|
||||
constructor() {
|
||||
this.ApiTargets = {
|
||||
BaseApi: API_TARGETS.BASE_API,
|
||||
GoggleApi: API_TARGETS.GOOGLE_API,
|
||||
FunnelApi: API_TARGETS.FUNNEL_API,
|
||||
CdnApi: API_TARGETS.CDN_API,
|
||||
GravatarApi: API_TARGETS.GRAVATAR_API,
|
||||
};
|
||||
}
|
||||
|
||||
public async fetch<B, T>(
|
||||
endpoint: string,
|
||||
{
|
||||
method,
|
||||
query,
|
||||
body,
|
||||
}: {
|
||||
method: string;
|
||||
query?: Record<string, string | boolean | number | undefined>;
|
||||
body?: B;
|
||||
},
|
||||
): Promise<T> {
|
||||
const queryInString =
|
||||
query &&
|
||||
Object.entries(query).reduce(
|
||||
(allOptions, [key, value]) => {
|
||||
if (!value) {
|
||||
return allOptions;
|
||||
}
|
||||
allOptions[key] = value.toString();
|
||||
return allOptions;
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
|
||||
const endpointWithQueries = queryInString
|
||||
? endpoint + "?" + new URLSearchParams(queryInString)
|
||||
: endpoint;
|
||||
|
||||
const bodyInString = JSON.stringify(body);
|
||||
|
||||
const response = await fetch(endpointWithQueries, {
|
||||
method,
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: bodyInString,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
public async fetchMultipartFormData<T>(
|
||||
endpoint: string,
|
||||
{
|
||||
method,
|
||||
body,
|
||||
}: {
|
||||
method: string;
|
||||
body: FormData;
|
||||
},
|
||||
): Promise<T> {
|
||||
const response = await fetch(endpoint, {
|
||||
method,
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: body,
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
|
||||
protected get<T>({
|
||||
endpoint,
|
||||
query,
|
||||
}: {
|
||||
endpoint: string;
|
||||
query?: Record<string, string | boolean | number | undefined>;
|
||||
}): Promise<T> {
|
||||
return this.fetch<null, T>(endpoint, { method: "GET", query });
|
||||
}
|
||||
|
||||
protected post<B, T>({
|
||||
endpoint,
|
||||
query,
|
||||
body,
|
||||
}: {
|
||||
endpoint: string;
|
||||
query?: Record<string, string | boolean | number | undefined>;
|
||||
body?: B;
|
||||
}): Promise<T> {
|
||||
return this.fetch<B, T>(endpoint, {
|
||||
method: "POST",
|
||||
query,
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
protected delete<B, T>({
|
||||
endpoint,
|
||||
query,
|
||||
body,
|
||||
}: {
|
||||
endpoint: string;
|
||||
query?: Record<string, string | boolean | number | undefined>;
|
||||
body?: B;
|
||||
}): Promise<T> {
|
||||
return this.fetch<B, T>(endpoint, {
|
||||
method: "DELETE",
|
||||
query,
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
protected async postFormVideo<T>({
|
||||
endpoint,
|
||||
formRecord,
|
||||
uuid,
|
||||
blob,
|
||||
blobFileName,
|
||||
}: {
|
||||
endpoint: string;
|
||||
formRecord: Record<string, string>;
|
||||
uuid: string;
|
||||
blob?: Blob | File;
|
||||
blobFileName?: string;
|
||||
}): Promise<T> {
|
||||
const formData = new FormData();
|
||||
formData.append("uuid_idempotency_token", uuid);
|
||||
Object.entries(formRecord).forEach(([key, value]) => {
|
||||
formData.append(key, value);
|
||||
});
|
||||
|
||||
if (blob && blobFileName) {
|
||||
formData.append("video", blob, blobFileName);
|
||||
} else if (blob) {
|
||||
formData.append("video", blob);
|
||||
}
|
||||
|
||||
return this.fetchMultipartFormData<T>(endpoint, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
}
|
||||
|
||||
protected async postForm<T>({
|
||||
endpoint,
|
||||
formRecord,
|
||||
uuid,
|
||||
blob,
|
||||
blobFileName,
|
||||
}: {
|
||||
endpoint: string;
|
||||
formRecord: Record<string, string>;
|
||||
uuid: string;
|
||||
blob?: Blob | File;
|
||||
blobFileName?: string;
|
||||
}): Promise<T> {
|
||||
const formData = new FormData();
|
||||
formData.append("uuid_idempotency_token", uuid);
|
||||
Object.entries(formRecord).forEach(([key, value]) => {
|
||||
formData.append(key, value);
|
||||
});
|
||||
|
||||
if (blob && blobFileName) {
|
||||
formData.append("file", blob, blobFileName);
|
||||
} else if (blob) {
|
||||
formData.append("file", blob);
|
||||
}
|
||||
|
||||
return this.fetchMultipartFormData<T>(endpoint, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
}
|
||||
|
||||
protected camelToSnakeCase(str: string) {
|
||||
return str.replace(/([a-z0])([A-Z])/g, "$1_$2").toLowerCase();
|
||||
}
|
||||
|
||||
protected parseQueryValues(
|
||||
params: Record<string, string | string[] | boolean | number | undefined>,
|
||||
): Record<string, string> {
|
||||
return Object.entries(params).reduce(
|
||||
(allParams, [key, value]) => {
|
||||
if (!value) {
|
||||
return allParams;
|
||||
}
|
||||
const snakeKey = this.camelToSnakeCase(key);
|
||||
if (Array.isArray(value)) {
|
||||
return { ...allParams, [snakeKey]: value.join(",") };
|
||||
}
|
||||
return { ...allParams, [snakeKey]: value.toString() };
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
}
|
||||
|
||||
protected parseBodyValues<T extends object, B extends object>(
|
||||
params: NonNullableObject<T>,
|
||||
): B {
|
||||
return Object.entries(params).reduce((allParams, [key, value]) => {
|
||||
if (!value) {
|
||||
return allParams;
|
||||
}
|
||||
const snakeKey = this.camelToSnakeCase(key);
|
||||
if (Array.isArray(value)) {
|
||||
return { ...allParams, [snakeKey]: value };
|
||||
}
|
||||
return { ...allParams, [snakeKey]: value };
|
||||
}, {} as B);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import { ApiManager, ApiResponse } from "../apis/ApiManager";
|
||||
import { BetaKey } from "../models/BetaKeys";
|
||||
import { Pagination } from "../models/Pagination";
|
||||
|
||||
export class BetaKeysApi extends ApiManager {
|
||||
public CreateBetaKey({
|
||||
maybeNote,
|
||||
maybeReferrerUsername,
|
||||
numberOfKeys,
|
||||
uuidIdempotencyToken,
|
||||
}: {
|
||||
maybeNote?: string;
|
||||
maybeReferrerUsername?: string;
|
||||
numberOfKeys: number;
|
||||
uuidIdempotencyToken: string;
|
||||
}): Promise<ApiResponse<string[]>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/beta_keys/create`;
|
||||
|
||||
const body = {
|
||||
maybe_note: maybeNote,
|
||||
maybe_referrer_username: maybeReferrerUsername,
|
||||
number_of_keys: numberOfKeys,
|
||||
uuid_idempotency_token: uuidIdempotencyToken,
|
||||
};
|
||||
|
||||
return this.post<
|
||||
{
|
||||
maybe_note?: string;
|
||||
maybe_referrer_username?: string;
|
||||
number_of_keys: number;
|
||||
uuid_idempotency_token: string;
|
||||
},
|
||||
{
|
||||
success?: boolean;
|
||||
beta_keys?: string[];
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint, body })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
data: response.beta_keys,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public ListBetaKeys({
|
||||
sortAscending,
|
||||
pageSize,
|
||||
pageIndex,
|
||||
maybeReferrerUsername,
|
||||
onlyListRemaining,
|
||||
}: {
|
||||
sortAscending?: boolean;
|
||||
pageSize?: number;
|
||||
pageIndex?: number;
|
||||
maybeReferrerUsername?: string;
|
||||
onlyListRemaining?: boolean;
|
||||
}): Promise<ApiResponse<BetaKey[], Pagination>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/beta_keys/list`;
|
||||
|
||||
const query = {
|
||||
sort_ascending: sortAscending,
|
||||
page_size: pageSize,
|
||||
page_index: pageIndex,
|
||||
maybe_referrer_username: maybeReferrerUsername,
|
||||
only_list_remaining: onlyListRemaining,
|
||||
};
|
||||
|
||||
return this.get<{
|
||||
success?: boolean;
|
||||
beta_keys?: BetaKey[];
|
||||
pagination?: Pagination;
|
||||
BadInput?: string;
|
||||
}>({ endpoint, query })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
data: response.beta_keys,
|
||||
pagination: response.pagination,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public RedeemBetaKey({
|
||||
betaKey,
|
||||
}: {
|
||||
betaKey: string;
|
||||
}): Promise<ApiResponse<undefined>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/beta_keys/redeem`;
|
||||
|
||||
return this.post<
|
||||
{
|
||||
beta_key: string;
|
||||
},
|
||||
{
|
||||
success?: boolean;
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint, body: { beta_key: betaKey } })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public UpdateBetaKeyIsDistributed({
|
||||
betaKey,
|
||||
isDistributed,
|
||||
}: {
|
||||
betaKey: string;
|
||||
isDistributed: boolean;
|
||||
}): Promise<ApiResponse<undefined>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/beta_keys/${betaKey}/distributed`;
|
||||
|
||||
return this.post<
|
||||
{
|
||||
is_distributed: boolean;
|
||||
},
|
||||
{
|
||||
success?: boolean;
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint, body: { is_distributed: isDistributed } })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public UpdateBetaKeyNote({
|
||||
token,
|
||||
note,
|
||||
}: {
|
||||
token: string;
|
||||
note?: string;
|
||||
}): Promise<ApiResponse<undefined>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/beta_keys/${token}/note`;
|
||||
|
||||
return this.post<
|
||||
{
|
||||
note?: string;
|
||||
},
|
||||
{
|
||||
success?: boolean;
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint, body: { note } })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { ApiManager, ApiResponse } from "../apis/ApiManager";
|
||||
import { Subscription } from "../models/Billing";
|
||||
import { LoyaltyProgram } from "../enums/Billing";
|
||||
|
||||
export class BillingApi extends ApiManager {
|
||||
public async ListActiveSubscriptions(): Promise<
|
||||
ApiResponse<{
|
||||
active_subscriptions: Subscription[];
|
||||
maybe_loyalty_program?: LoyaltyProgram;
|
||||
}>
|
||||
> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/billing/active_subscriptions`;
|
||||
return await this.get<{
|
||||
success: boolean;
|
||||
active_subscriptions?: Subscription[];
|
||||
maybe_loyalty_program?: LoyaltyProgram;
|
||||
error_message?: string;
|
||||
}>({ endpoint: endpoint })
|
||||
.then((response) => ({
|
||||
success: response.success,
|
||||
data: {
|
||||
active_subscriptions: response.active_subscriptions || [],
|
||||
maybe_loyalty_program: response.maybe_loyalty_program,
|
||||
},
|
||||
}))
|
||||
.catch((err) => {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: err.message,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { ApiManager, ApiResponse } from "../apis/ApiManager";
|
||||
|
||||
export class CommentsApi extends ApiManager {
|
||||
public CreateComment({
|
||||
commentMarkdown,
|
||||
entityToken,
|
||||
entityType,
|
||||
uuidIdempotencyToken,
|
||||
}: {
|
||||
commentMarkdown: string;
|
||||
entityToken: string;
|
||||
entityType: string;
|
||||
uuidIdempotencyToken: string;
|
||||
}): Promise<ApiResponse<string>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/comments/new`;
|
||||
|
||||
const body = {
|
||||
comment_markdown: commentMarkdown,
|
||||
entity_token: entityToken,
|
||||
entity_type: entityType,
|
||||
uuid_idempotency_token: uuidIdempotencyToken,
|
||||
};
|
||||
|
||||
return this.post<
|
||||
{
|
||||
comment_markdown: string;
|
||||
entity_token: string;
|
||||
entity_type: string;
|
||||
uuid_idempotency_token: string;
|
||||
},
|
||||
{
|
||||
success?: boolean;
|
||||
comment_token?: string;
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint, body })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
data: response.comment_token,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public DeleteComment({
|
||||
commentToken,
|
||||
}: {
|
||||
commentToken: string;
|
||||
}): Promise<ApiResponse<undefined>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/comments/delete/${commentToken}`;
|
||||
|
||||
return this.post<
|
||||
{ as_mod: boolean },
|
||||
{
|
||||
success?: boolean;
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint, body: { as_mod: true } })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public ListCommentsByEntity({
|
||||
entityType,
|
||||
entityToken,
|
||||
}: {
|
||||
entityType: string;
|
||||
entityToken: string;
|
||||
}): Promise<ApiResponse<string[]>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/comments/list/${entityType}/${entityToken}`;
|
||||
|
||||
return this.get<{
|
||||
success: boolean;
|
||||
comments: string[];
|
||||
}>({ endpoint })
|
||||
.then((response) => ({
|
||||
success: true,
|
||||
data: response.comments,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
import { ApiManager, ApiResponse } from "./ApiManager";
|
||||
|
||||
export class EngineApi extends ApiManager {
|
||||
public async ConvertTbxToGltf({
|
||||
mediaFileToken,
|
||||
uuidIdempotencyToken,
|
||||
}: {
|
||||
mediaFileToken: string;
|
||||
uuidIdempotencyToken: string;
|
||||
}): Promise<ApiResponse<string>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/conversion/enqueue_fbx_to_gltf`;
|
||||
|
||||
const body = {
|
||||
media_file_token: mediaFileToken,
|
||||
uuid_idempotency_token: uuidIdempotencyToken,
|
||||
};
|
||||
|
||||
return this.post<
|
||||
{ media_file_token: string; uuid_idempotency_token: string },
|
||||
{ success?: boolean; inference_job_token?: string; BadInput?: string }
|
||||
>({
|
||||
endpoint,
|
||||
body: body,
|
||||
})
|
||||
.then((response) => {
|
||||
return {
|
||||
success: response.success ?? false,
|
||||
data: response.inference_job_token,
|
||||
errorMessage: response.BadInput,
|
||||
};
|
||||
})
|
||||
.catch((err) => {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: err.message,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public async uploadSceneSnapshot({
|
||||
screenshot,
|
||||
sceneMediaToken,
|
||||
}: {
|
||||
screenshot: File; // base64 encoded PNG
|
||||
sceneMediaToken?: string;
|
||||
}): Promise<ApiResponse<string>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/image_studio/scene_snapshot`;
|
||||
const formData = new FormData();
|
||||
formData.append("snapshot", screenshot); // Changed from "screenshot" to "snapshot" to match API spec
|
||||
if (sceneMediaToken) {
|
||||
formData.append("scene_media_token", sceneMediaToken);
|
||||
}
|
||||
|
||||
// for now ...
|
||||
const uuidIdempotencyToken = crypto.randomUUID(); // Generate a new UUID
|
||||
formData.append("uuid_idempotency_token", uuidIdempotencyToken); // Added uuid_idempotency_token
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
const postResponse = await response.json();
|
||||
|
||||
console.log(postResponse);
|
||||
|
||||
let result: { success: boolean; data?: string; errorMessage?: string };
|
||||
|
||||
if (postResponse.success) {
|
||||
result = {
|
||||
success: true,
|
||||
data: postResponse.snapshot_media_token,
|
||||
errorMessage: undefined,
|
||||
};
|
||||
} else {
|
||||
result = {
|
||||
success: false,
|
||||
errorMessage: postResponse.BadInput,
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async enqueueImageGeneration({
|
||||
disableSystemPrompt,
|
||||
prompt,
|
||||
snapshotMediaToken,
|
||||
additionalImages,
|
||||
}: {
|
||||
disableSystemPrompt: boolean;
|
||||
prompt: string;
|
||||
snapshotMediaToken: string;
|
||||
additionalImages?: string[];
|
||||
}): Promise<ApiResponse<string>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/image_studio/prompt`;
|
||||
|
||||
const uuidIdempotencyToken = crypto.randomUUID(); // Generate a new UUID
|
||||
const body = {
|
||||
uuid_idempotency_token: uuidIdempotencyToken,
|
||||
disable_system_prompt: disableSystemPrompt,
|
||||
prompt,
|
||||
snapshot_media_token: snapshotMediaToken, // Changed from scene_media_token to snapshot_media_token
|
||||
additional_images: additionalImages,
|
||||
};
|
||||
|
||||
const postResponse = await this.post<
|
||||
{
|
||||
uuid_idempotency_token: string;
|
||||
disable_system_prompt: boolean;
|
||||
prompt: string;
|
||||
snapshot_media_token: string;
|
||||
additional_images?: string[];
|
||||
},
|
||||
{ success?: boolean; job_token?: string; BadInput?: string }
|
||||
>({
|
||||
endpoint,
|
||||
body,
|
||||
});
|
||||
|
||||
// Check if the response is successful
|
||||
const isSuccess = postResponse.success ?? false;
|
||||
|
||||
// Prepare the result object
|
||||
const result = {
|
||||
success: isSuccess,
|
||||
data: isSuccess ? postResponse.job_token : undefined,
|
||||
errorMessage: isSuccess ? undefined : postResponse.BadInput,
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async pollJobSession(jobToken: string): Promise<
|
||||
ApiResponse<{
|
||||
status: string;
|
||||
result: {
|
||||
generated_images?: string[];
|
||||
error?: string;
|
||||
};
|
||||
}>
|
||||
> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/jobs/session/${jobToken}`;
|
||||
|
||||
const response = await this.get<{
|
||||
success?: boolean;
|
||||
status?: string;
|
||||
result?: {
|
||||
generated_images?: string[];
|
||||
error?: string;
|
||||
};
|
||||
BadInput?: string;
|
||||
}>({
|
||||
endpoint,
|
||||
});
|
||||
|
||||
const success = response.success ?? false;
|
||||
const status = response.status ?? "";
|
||||
const result = response.result ?? {
|
||||
generated_images: [],
|
||||
error: undefined,
|
||||
};
|
||||
const errorMessage = response.BadInput;
|
||||
|
||||
return {
|
||||
success,
|
||||
data: { status, result },
|
||||
errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
public async pollStudioSessionJobs(jobToken: string): Promise<
|
||||
ApiResponse<{
|
||||
status: string;
|
||||
result: {
|
||||
generated_images?: string[];
|
||||
error?: string;
|
||||
};
|
||||
}>
|
||||
> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/image_studio/session_jobs/${jobToken}`;
|
||||
|
||||
const response = await this.get<{
|
||||
success?: boolean;
|
||||
status?: string;
|
||||
result?: {
|
||||
generated_images?: string[];
|
||||
error?: string;
|
||||
};
|
||||
BadInput?: string;
|
||||
}>({
|
||||
endpoint,
|
||||
});
|
||||
|
||||
const success = response.success ?? false;
|
||||
const status = response.status ?? "";
|
||||
const result = response.result ?? {
|
||||
generated_images: [],
|
||||
error: undefined,
|
||||
};
|
||||
const errorMessage = response.BadInput;
|
||||
|
||||
return {
|
||||
success,
|
||||
data: { status, result },
|
||||
errorMessage,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { ApiManager, ApiResponse } from "../apis/ApiManager";
|
||||
|
||||
export class FeaturedItemsApi extends ApiManager {
|
||||
public CreateFeaturedItem({
|
||||
entityToken,
|
||||
entityType,
|
||||
}: {
|
||||
entityToken: string;
|
||||
entityType: string;
|
||||
}): Promise<ApiResponse<undefined>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/featured_item/create`;
|
||||
|
||||
const body = {
|
||||
entity_token: entityToken,
|
||||
entity_type: entityType,
|
||||
};
|
||||
|
||||
return this.post<
|
||||
{
|
||||
entity_token: string;
|
||||
entity_type: string;
|
||||
},
|
||||
{
|
||||
success?: boolean;
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint, body })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public DeleteFeaturedItem({
|
||||
entityToken,
|
||||
entityType,
|
||||
}: {
|
||||
entityToken: string;
|
||||
entityType: string;
|
||||
}): Promise<ApiResponse<undefined>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/featured_item/delete`;
|
||||
|
||||
const body = {
|
||||
entity_token: entityToken,
|
||||
entity_type: entityType,
|
||||
};
|
||||
|
||||
return this.delete<
|
||||
{
|
||||
entity_token: string;
|
||||
entity_type: string;
|
||||
},
|
||||
{
|
||||
success?: boolean;
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint, body })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public CheckFeaturedItem({
|
||||
entityType,
|
||||
entityToken,
|
||||
}: {
|
||||
entityType: string;
|
||||
entityToken: string;
|
||||
}): Promise<ApiResponse<boolean>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/featured_item/is_featured/${entityType}/${entityToken}`;
|
||||
|
||||
return this.post<
|
||||
undefined,
|
||||
{
|
||||
success?: boolean;
|
||||
is_featured?: boolean;
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
data: response.is_featured,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// libs/api-client/src/JobsApi.ts
|
||||
import { ApiManager, ApiResponse } from "../apis/ApiManager";
|
||||
import { Job, JobPreview } from "../models/Job";
|
||||
import { JobState } from "../models/JobState";
|
||||
|
||||
export class JobsApi extends ApiManager {
|
||||
public GetPreviewStatusByJobToken({
|
||||
token,
|
||||
}: {
|
||||
token: string;
|
||||
}): Promise<ApiResponse<JobPreview>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/workflows/preview_status/${token}`;
|
||||
|
||||
return this.get<ApiResponse<JobPreview>>({ endpoint })
|
||||
.then((response) => ({
|
||||
success: response.success,
|
||||
data: response.data,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public GetJobByToken({
|
||||
token,
|
||||
}: {
|
||||
token: string;
|
||||
}): Promise<ApiResponse<JobState>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/jobs/job/${token}`;
|
||||
|
||||
return this.get<{
|
||||
success: boolean;
|
||||
state: JobState;
|
||||
}>({ endpoint })
|
||||
.then((response) => ({
|
||||
success: response.success,
|
||||
data: response.state,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public ListJobs(): Promise<ApiResponse<JobState[]>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/jobs/batch`;
|
||||
|
||||
return this.get<{
|
||||
success: boolean;
|
||||
job_states: JobState[];
|
||||
}>({ endpoint })
|
||||
.then((response) => ({
|
||||
success: response.success,
|
||||
data: response.job_states,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public ListRecentJobs(): Promise<ApiResponse<Job[]>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/jobs/session`;
|
||||
|
||||
return this.get<{
|
||||
success: boolean;
|
||||
jobs: Job[];
|
||||
error_reason?: string;
|
||||
}>({ endpoint })
|
||||
.then((response) => ({
|
||||
success: response.success,
|
||||
data: response.jobs,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public DeleteJobByToken(jobToken: string): Promise<ApiResponse<undefined>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/jobs/job/${jobToken}`;
|
||||
|
||||
return this.delete<
|
||||
undefined,
|
||||
{
|
||||
success: boolean;
|
||||
}
|
||||
>({ endpoint })
|
||||
.then((response) => ({
|
||||
success: response.success,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
|
||||
import { ApiManager, ApiResponse } from "../apis/ApiManager";
|
||||
import { FilterEngineCategories } from "../enums/QueryFilters";
|
||||
import { Visibility } from "../enums/Visibility";
|
||||
export class MediaUploadApi extends ApiManager {
|
||||
private sessionToken: string = "";
|
||||
|
||||
private async getSessionTokenForUploadStudioShot() {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/session_token`;
|
||||
return await this.get<{
|
||||
maybe_signed_session: string;
|
||||
}>({ endpoint })
|
||||
.then((response) => {
|
||||
return {
|
||||
success: true,
|
||||
data: response["maybe_signed_session"],
|
||||
};
|
||||
})
|
||||
.catch((err) => {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: err.message,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public async UploadStudioShot({
|
||||
maybe_title,
|
||||
uuid_idempotency_token,
|
||||
blob,
|
||||
fileName,
|
||||
maybe_visibility,
|
||||
}: {
|
||||
maybe_title?: string;
|
||||
uuid_idempotency_token: string;
|
||||
blob: Blob | null;
|
||||
fileName: string;
|
||||
maybe_visibility?: Visibility;
|
||||
}): Promise<ApiResponse<string>> {
|
||||
// This function is a bit special it requires that
|
||||
const endpoint = `${this.ApiTargets.uploadApi}/v1/media_files/upload/studio_shot`;
|
||||
|
||||
if (!this.sessionToken) {
|
||||
console.log("Session Token");
|
||||
const response = await this.getSessionTokenForUploadStudioShot();
|
||||
if (response.success && response.data) {
|
||||
this.sessionToken = response.data;
|
||||
console.log(`Token ${this.sessionToken}`);
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: "Could not obtain session tokens.",
|
||||
};
|
||||
}
|
||||
} else {
|
||||
console.log(`Session Token: ${this.sessionToken}`);
|
||||
}
|
||||
|
||||
const options: Record<string, string | number | undefined> = {
|
||||
maybe_title,
|
||||
maybe_frame_rate_fps: 60,
|
||||
maybe_visibility: maybe_visibility?.toString(),
|
||||
};
|
||||
|
||||
const formRecord = Object.entries(options).reduce(
|
||||
(allOptions, [key, value]) => {
|
||||
if (value === undefined) {
|
||||
return allOptions;
|
||||
}
|
||||
return { ...allOptions, [key]: value.toString() };
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("uuid_idempotency_token", uuid_idempotency_token);
|
||||
Object.entries(formRecord).forEach(([key, value]) => {
|
||||
formData.append(key, value);
|
||||
});
|
||||
if (blob && fileName) {
|
||||
formData.append("file", blob, fileName);
|
||||
} else if (blob) {
|
||||
formData.append("file", blob);
|
||||
}
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Session: this.sessionToken,
|
||||
},
|
||||
credentials: "include",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success && result.media_file_token) {
|
||||
return { success: true, data: result.media_file_token };
|
||||
} else {
|
||||
return { success: false, errorMessage: "Failed to Get Media Token" };
|
||||
}
|
||||
}
|
||||
|
||||
private async Upload({
|
||||
endpoint,
|
||||
uuid,
|
||||
blob,
|
||||
fileName,
|
||||
options,
|
||||
}: {
|
||||
endpoint: string;
|
||||
blob: Blob | File;
|
||||
fileName: string;
|
||||
uuid: string;
|
||||
options: Record<string, string | number | undefined>;
|
||||
}): Promise<ApiResponse<string>> {
|
||||
const formRecord = Object.entries(options).reduce(
|
||||
(allOptions, [key, value]) => {
|
||||
if (value === undefined) {
|
||||
return allOptions;
|
||||
}
|
||||
return { ...allOptions, [key]: value.toString() };
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
|
||||
return await this.postForm<{
|
||||
success: boolean;
|
||||
media_file_token?: string;
|
||||
BadInput?: string;
|
||||
}>({ endpoint, formRecord, blob, blobFileName: fileName, uuid })
|
||||
.then((response) => ({
|
||||
success: Boolean(response.success ?? false),
|
||||
data: response.media_file_token,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: err.message,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public async UploadAudio({
|
||||
blob,
|
||||
fileName,
|
||||
uuid,
|
||||
maybe_title,
|
||||
maybe_visibility = Visibility.Public,
|
||||
}: {
|
||||
blob: Blob;
|
||||
fileName: string;
|
||||
uuid: string;
|
||||
maybe_title?: string | undefined;
|
||||
maybe_visibility?: Visibility | undefined;
|
||||
}): Promise<ApiResponse<string>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/audio`;
|
||||
const options: Record<string, string | number | undefined> = {
|
||||
maybe_title,
|
||||
maybe_visibility: maybe_visibility?.toString(),
|
||||
};
|
||||
return this.Upload({ endpoint, blob, fileName, uuid, options });
|
||||
}
|
||||
|
||||
public async UploadImage({
|
||||
blob,
|
||||
fileName,
|
||||
uuid,
|
||||
maybe_title,
|
||||
maybe_visibility = Visibility.Public,
|
||||
is_intermediate_system_file = EIntermediateFile.true
|
||||
}: {
|
||||
blob: Blob;
|
||||
fileName: string;
|
||||
uuid: string;
|
||||
maybe_title?: string | undefined;
|
||||
maybe_visibility?: Visibility | undefined;
|
||||
is_intermediate_system_file?: EIntermediateFile;
|
||||
}): Promise<ApiResponse<string>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/image`;
|
||||
const options: Record<string, string | number | undefined> = {
|
||||
is_intermediate_system_file: EIntermediateFile[is_intermediate_system_file],
|
||||
maybe_title,
|
||||
maybe_visibility: maybe_visibility?.toString(),
|
||||
};
|
||||
return this.Upload({ endpoint, blob, fileName, uuid, options });
|
||||
}
|
||||
|
||||
public async UploadNewEngineAsset({
|
||||
file,
|
||||
fileName,
|
||||
uuid,
|
||||
engine_category,
|
||||
maybe_animation_type,
|
||||
maybe_duration_millis,
|
||||
maybe_title,
|
||||
maybe_visibility = Visibility.Public,
|
||||
}: {
|
||||
file: File;
|
||||
fileName: string;
|
||||
uuid: string;
|
||||
engine_category: FilterEngineCategories;
|
||||
maybe_animation_type?: string;
|
||||
maybe_duration_millis?: number;
|
||||
maybe_title?: string;
|
||||
maybe_visibility?: Visibility;
|
||||
}): Promise<ApiResponse<string>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/new_engine_asset`;
|
||||
const options: Record<string, string | number | undefined> = {
|
||||
engine_category,
|
||||
maybe_title,
|
||||
maybe_visibility: maybe_visibility?.toString(),
|
||||
maybe_animation_type,
|
||||
maybe_duration_millis,
|
||||
};
|
||||
return this.Upload({ endpoint, blob: file, fileName, uuid, options });
|
||||
}
|
||||
|
||||
public async UploadNewScene({
|
||||
blob,
|
||||
fileName,
|
||||
uuid,
|
||||
maybe_title,
|
||||
maybe_visibility = Visibility.Public,
|
||||
}: {
|
||||
blob: Blob;
|
||||
fileName: string;
|
||||
uuid: string;
|
||||
maybe_title?: string;
|
||||
maybe_visibility?: Visibility;
|
||||
}): Promise<ApiResponse<string>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/new_scene`;
|
||||
const options: Record<string, string | number | undefined> = {
|
||||
maybe_title,
|
||||
maybe_visibility: maybe_visibility?.toString(),
|
||||
};
|
||||
return this.Upload({ endpoint, blob, fileName, uuid, options });
|
||||
}
|
||||
|
||||
public async UploadNewVideo({
|
||||
blob,
|
||||
fileName,
|
||||
uuid,
|
||||
maybe_title,
|
||||
maybe_visibility = Visibility.Public,
|
||||
maybe_style_name,
|
||||
maybe_scene_source_media_file_token,
|
||||
}: {
|
||||
blob: Blob;
|
||||
fileName: string;
|
||||
uuid: string;
|
||||
maybe_title?: string;
|
||||
maybe_visibility?: Visibility;
|
||||
maybe_style_name?: string;
|
||||
maybe_scene_source_media_file_token?: string;
|
||||
}): Promise<ApiResponse<string>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/new_video`;
|
||||
const options: Record<string, string | number | undefined> = {
|
||||
is_intermediate_system_file: "true",
|
||||
maybe_title,
|
||||
maybe_visibility: maybe_visibility?.toString(),
|
||||
maybe_style_name,
|
||||
maybe_scene_source_media_file_token,
|
||||
};
|
||||
return this.Upload({ endpoint, blob, fileName, uuid, options });
|
||||
}
|
||||
|
||||
public async UploadPmx({
|
||||
file,
|
||||
fileName,
|
||||
uuid,
|
||||
engine_category,
|
||||
maybe_animation_type,
|
||||
maybe_duration_millis,
|
||||
maybe_title,
|
||||
maybe_visibility = Visibility.Public,
|
||||
}: {
|
||||
file: File;
|
||||
fileName: string;
|
||||
uuid: string;
|
||||
engine_category?: string;
|
||||
maybe_animation_type?: string;
|
||||
maybe_duration_millis?: number;
|
||||
maybe_title?: string;
|
||||
maybe_visibility?: Visibility;
|
||||
}): Promise<ApiResponse<string>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/pmx`;
|
||||
const options: Record<string, string | number | undefined> = {
|
||||
is_intermediate_system_file: "true",
|
||||
engine_category,
|
||||
maybe_animation_type,
|
||||
maybe_duration_millis,
|
||||
maybe_title,
|
||||
maybe_visibility,
|
||||
};
|
||||
return this.Upload({ endpoint, blob: file, fileName, uuid, options });
|
||||
}
|
||||
|
||||
public async UploadSavedScene({
|
||||
blob,
|
||||
fileName,
|
||||
uuid,
|
||||
mediaToken,
|
||||
}: {
|
||||
blob: Blob;
|
||||
fileName: string;
|
||||
uuid: string;
|
||||
mediaToken: string;
|
||||
}): Promise<ApiResponse<string>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/saved_scene/${mediaToken}`;
|
||||
const options: Record<string, string | number | undefined> = {};
|
||||
return this.Upload({ endpoint, blob, fileName, uuid, options });
|
||||
}
|
||||
|
||||
// this is used to send the scene snapshot to the vst upload endpoint this creates a snapshot of the scene where in we can remix from the og.
|
||||
public async UploadSceneSnapshotMediaFileForm({
|
||||
blob,
|
||||
uuid,
|
||||
maybe_title, // title of the scene at the time
|
||||
maybe_scene_source_media_file_token, // original token that started it all
|
||||
}: {
|
||||
blob: Blob;
|
||||
uuid: string;
|
||||
maybe_title: string;
|
||||
maybe_scene_source_media_file_token: string | undefined;
|
||||
}): Promise<ApiResponse<string>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/scene_snapshot`;
|
||||
|
||||
const options: Record<string, string | number | undefined> = {
|
||||
maybe_title: maybe_title,
|
||||
maybe_scene_source_media_file_token: maybe_scene_source_media_file_token,
|
||||
};
|
||||
|
||||
const fileName = `${maybe_title}-${uuid}`;
|
||||
return this.Upload({ endpoint, blob, fileName, uuid, options }); // token comes back and send it VST
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { ApiManager, ApiResponse } from "../apis/ApiManager";
|
||||
import { StatusAlert } from "../models/StatusAlert";
|
||||
|
||||
export class MiscApi extends ApiManager {
|
||||
public GetStatusAlertCheck(): Promise<ApiResponse<StatusAlert>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/session`;
|
||||
return this.get<{
|
||||
success: boolean;
|
||||
maybe_alert?: Omit<StatusAlert, "refresh_interval_millis">;
|
||||
refresh_interval_millis: number;
|
||||
}>({ endpoint: endpoint })
|
||||
.then((response) => ({
|
||||
success: response.success,
|
||||
data: {
|
||||
...response.maybe_alert,
|
||||
refresh_interval_millis: response.refresh_interval_millis,
|
||||
},
|
||||
}))
|
||||
.catch((err) => {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: err.mesasge,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { ApiManager, ApiResponse } from "../apis/ApiManager";
|
||||
|
||||
export class ModerationApi extends ApiManager {
|
||||
public async UpdateFeatureFlagsByToken({
|
||||
usernameOrToken,
|
||||
flags,
|
||||
}: {
|
||||
usernameOrToken: string;
|
||||
flags: string[];
|
||||
}): Promise<
|
||||
ApiResponse<{
|
||||
inference_job_token?: string;
|
||||
inference_job_token_type?: string;
|
||||
}>
|
||||
> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/moderation/user_feature_flags/${usernameOrToken}`;
|
||||
|
||||
const body = {
|
||||
action: {
|
||||
AddFlags: {
|
||||
flags,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return this.post<
|
||||
{ action: { AddFlags: { flags: string[] } } },
|
||||
{ success?: boolean; BadInput?: string }
|
||||
>({
|
||||
endpoint,
|
||||
body: body,
|
||||
})
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => ({
|
||||
success: false,
|
||||
errorMessage: err.message,
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ApiManager, ApiResponse } from "../apis/ApiManager";
|
||||
import { Prompts } from "../models/Prompts";
|
||||
|
||||
export class PromptsApi extends ApiManager {
|
||||
public GetPromptsByToken({
|
||||
token,
|
||||
}: {
|
||||
token: string;
|
||||
}): Promise<ApiResponse<Prompts>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/prompts/${token}`;
|
||||
|
||||
return this.get<{
|
||||
success: boolean;
|
||||
prompt: Prompts;
|
||||
error_reason?: string;
|
||||
}>({ endpoint })
|
||||
.then((response) => ({
|
||||
success: response.success,
|
||||
data: response.prompt,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { ApiManager } from "../apis/ApiManager";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
export interface Coordinates {
|
||||
coordinates: [number, number];
|
||||
include: boolean;
|
||||
}
|
||||
|
||||
export interface ObjectData {
|
||||
style: {
|
||||
color: SegmentationColor;
|
||||
};
|
||||
object_id: number;
|
||||
points: Coordinates[];
|
||||
}
|
||||
|
||||
export type SegmentationColor = [number, number, number];
|
||||
|
||||
export interface RequestFrame {
|
||||
timestamp: number;
|
||||
objects: ObjectData[];
|
||||
}
|
||||
export interface ResponseFrame {
|
||||
b64_image_data: number;
|
||||
idx: number;
|
||||
timestamp: number;
|
||||
objects: ObjectData[];
|
||||
preview_image_url: string;
|
||||
}
|
||||
|
||||
export interface SegmentationRequest {
|
||||
session_id: string;
|
||||
fps: number;
|
||||
frames: RequestFrame[];
|
||||
propagate: boolean;
|
||||
}
|
||||
|
||||
export interface SegmentationResponse {
|
||||
session_id: string;
|
||||
fps: number;
|
||||
masked_video_cdn_url: string;
|
||||
frames: ResponseFrame[];
|
||||
propagate: boolean;
|
||||
}
|
||||
|
||||
export class SegmentationApi extends ApiManager {
|
||||
// returns uuid
|
||||
public async createSession(
|
||||
blobVideo: File | Blob,
|
||||
): Promise<{ session_id: string }> {
|
||||
const endpoint = `https://hax.storyteller.ai/segmentation/new_session`;
|
||||
const id = uuidv4().toString();
|
||||
return this.postFormVideo<{ session_id: "fix-mimetype" }>({
|
||||
endpoint: endpoint,
|
||||
formRecord: {
|
||||
session_id: "fix-mimetype",
|
||||
},
|
||||
uuid: id,
|
||||
blobFileName: id,
|
||||
blob: blobVideo,
|
||||
});
|
||||
}
|
||||
|
||||
public addPointsToSession(
|
||||
session_id: string,
|
||||
fps: number,
|
||||
frames: RequestFrame[],
|
||||
propagate: boolean,
|
||||
): Promise<SegmentationResponse> {
|
||||
const endpoint = `https://hax.storyteller.ai/segmentation/generate_masks`;
|
||||
|
||||
const segmentationRequest: SegmentationRequest = {
|
||||
session_id: session_id,
|
||||
fps: fps,
|
||||
frames: frames,
|
||||
propagate: propagate,
|
||||
};
|
||||
|
||||
return this.post<SegmentationRequest, SegmentationResponse>({
|
||||
endpoint: endpoint,
|
||||
body: segmentationRequest,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { ApiManager, ApiResponse } from "../apis/ApiManager";
|
||||
import { GenerateTtsAudioErrorType } from "../enums/Tts";
|
||||
import { Visibility } from "../enums/Visibility";
|
||||
|
||||
export interface GenerateTtsAudioRequest {
|
||||
creator_set_visibility?: Visibility;
|
||||
uuid_idempotency_token: string;
|
||||
tts_model_token: string;
|
||||
inference_text: string;
|
||||
is_storyteller_demo?: boolean;
|
||||
}
|
||||
|
||||
export interface GenerateTtsAudioResponse {
|
||||
success?: boolean;
|
||||
inference_job_token?: string;
|
||||
inference_job_token_type?: string;
|
||||
BadInput?: string;
|
||||
}
|
||||
|
||||
export class TtsApi extends ApiManager {
|
||||
public async GenerateTtsAudio(request: GenerateTtsAudioRequest): Promise<
|
||||
ApiResponse<{
|
||||
inference_job_token?: string;
|
||||
inference_job_token_type?: string;
|
||||
}>
|
||||
> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/tts/inference`;
|
||||
|
||||
const body = {
|
||||
...request,
|
||||
...(request.creator_set_visibility
|
||||
? {}
|
||||
: { creator_set_visibility: Visibility.Public }),
|
||||
};
|
||||
|
||||
return this.post<GenerateTtsAudioRequest, GenerateTtsAudioResponse>({
|
||||
endpoint,
|
||||
body: body,
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.success) {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: response.BadInput,
|
||||
data: {},
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
inference_job_token: response.inference_job_token,
|
||||
inference_job_token_type: response.inference_job_token_type,
|
||||
},
|
||||
};
|
||||
})
|
||||
.catch(() => {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: GenerateTtsAudioErrorType.UnknownError,
|
||||
data: {},
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
import { ApiManager, ApiResponse } from "../apis/ApiManager";
|
||||
import {
|
||||
UserBookmarkBatch,
|
||||
UserBookmarkByEntity,
|
||||
UserBookmarkByUser,
|
||||
} from "../models/UserBookmark";
|
||||
import { Pagination } from "../models/Pagination";
|
||||
|
||||
export enum ScopedEntityTypes {
|
||||
USER = "user",
|
||||
TTS_MODEL = "tts-model",
|
||||
TTS_RESULT = "tts-result",
|
||||
W2L_TEMPLATE = "w2l-template",
|
||||
W2L_RESULT = "w2l-result",
|
||||
MEDIA_FILE = "media_file",
|
||||
MODEL_WEIGHT = "model_weight",
|
||||
VOICE_CONVERSION_MODEL = "voice_conversion_model",
|
||||
ZS_VOICE = "z_voice",
|
||||
}
|
||||
|
||||
export enum ScopedWeightTypes {
|
||||
HIFIGAN_TTl2 = "hifigan_tt2",
|
||||
RVC_V2 = "rvc_v2",
|
||||
SD_1_5 = "sd_1.5",
|
||||
SDXL = "sdxl",
|
||||
SO_VITS_SVC = "so_vits_svc",
|
||||
TT2 = "tt2",
|
||||
LORA = "loRA",
|
||||
VALL_E = "vall_e",
|
||||
COMFY_UI = "comfy_ui",
|
||||
}
|
||||
|
||||
export enum ScopedMediaFileType {
|
||||
IMAGE_GENERATION = "image_generation",
|
||||
TEXT_TO_SPEECH = "text_to_speech",
|
||||
VOCODER = "vocoder",
|
||||
VOICE_CONVERSION = "voice_conversion",
|
||||
WORKFLOW_CONFIG = "workflow_config",
|
||||
}
|
||||
|
||||
interface ListUserBookmarksByUserRequest {
|
||||
username: string;
|
||||
sort_ascending?: boolean;
|
||||
page_size?: number;
|
||||
page_index?: number;
|
||||
maybe_scoped_entity_type?: ScopedEntityTypes[];
|
||||
maybe_scoped_weight_type?: ScopedWeightTypes[];
|
||||
maybe_scoped_media_file_type?: ScopedMediaFileType[];
|
||||
}
|
||||
|
||||
export class UserBookmarksApi extends ApiManager {
|
||||
public CreateUserBookmark({
|
||||
entityToken,
|
||||
entityType,
|
||||
}: {
|
||||
entityToken: string;
|
||||
entityType: string;
|
||||
}): Promise<
|
||||
ApiResponse<{
|
||||
new_bookmark_count_for_entity?: number;
|
||||
user_bookmark_token?: string;
|
||||
}>
|
||||
> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/user_bookmarks/create`;
|
||||
const body = {
|
||||
entity_token: entityToken,
|
||||
entity_type: entityType,
|
||||
};
|
||||
|
||||
return this.post<
|
||||
{
|
||||
entity_token: string;
|
||||
entity_type: string;
|
||||
},
|
||||
{
|
||||
success?: boolean;
|
||||
new_bookmark_count_for_entity?: number;
|
||||
user_bookmark_token: string;
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint, body })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
data: {
|
||||
new_bookmark_count_for_entity: response.new_bookmark_count_for_entity,
|
||||
user_bookmark_token: response.user_bookmark_token,
|
||||
},
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public DeleteUserBookmark({
|
||||
entityToken,
|
||||
}: {
|
||||
entityToken: string;
|
||||
}): Promise<ApiResponse<undefined>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/user_bookmarks/delete/${entityToken}`;
|
||||
|
||||
return this.delete<
|
||||
{ as_mod: boolean },
|
||||
{
|
||||
success?: boolean;
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint, body: { as_mod: true } })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public ListUserBookmarks(): Promise<ApiResponse<UserBookmarkBatch[]>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/user_bookmarks/batch`;
|
||||
|
||||
return this.get<{
|
||||
success?: boolean;
|
||||
bookmarks?: UserBookmarkBatch[];
|
||||
BadInput?: string;
|
||||
}>({ endpoint })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
data: response.bookmarks,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public ListUserBookmarksByUser({
|
||||
username,
|
||||
sort_ascending,
|
||||
page_size,
|
||||
page_index,
|
||||
maybe_scoped_entity_type,
|
||||
maybe_scoped_weight_type,
|
||||
maybe_scoped_media_file_type,
|
||||
}: ListUserBookmarksByUserRequest): Promise<
|
||||
ApiResponse<UserBookmarkByUser[], Pagination>
|
||||
> {
|
||||
const user = username;
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/user_bookmarks/list/${user}`;
|
||||
|
||||
const query = {
|
||||
sort_ascending,
|
||||
page_size,
|
||||
page_index,
|
||||
maybe_scoped_entity_type: maybe_scoped_entity_type
|
||||
? maybe_scoped_entity_type.join(",")
|
||||
: undefined,
|
||||
maybe_scoped_weight_type: maybe_scoped_weight_type
|
||||
? maybe_scoped_weight_type.join(",")
|
||||
: undefined,
|
||||
maybe_scoped_media_file_type: maybe_scoped_media_file_type
|
||||
? maybe_scoped_media_file_type.join(",")
|
||||
: undefined,
|
||||
};
|
||||
|
||||
return this.get<{
|
||||
success?: boolean;
|
||||
pagination?: Pagination;
|
||||
results: UserBookmarkByUser[];
|
||||
}>({ endpoint, query })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
data: response.results,
|
||||
pagination: response.pagination,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public ListUserBookmarksByEntity({
|
||||
entityType,
|
||||
entityToken,
|
||||
}: {
|
||||
entityType: string;
|
||||
entityToken: string;
|
||||
}): Promise<ApiResponse<UserBookmarkByEntity[]>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/user_bookmarks/list/${entityType}/${entityToken}`;
|
||||
|
||||
return this.get<{
|
||||
success: boolean;
|
||||
user_bookmarks: UserBookmarkByEntity[];
|
||||
}>({ endpoint })
|
||||
.then((response) => ({
|
||||
success: true,
|
||||
data: response.user_bookmarks,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { ApiManager, ApiResponse } from "../apis/ApiManager";
|
||||
|
||||
export interface UserRating {
|
||||
entity_token: string;
|
||||
entity_type: string;
|
||||
rating_value: string;
|
||||
}
|
||||
|
||||
export class UserRatingApi extends ApiManager {
|
||||
public ListUserRatings(): Promise<ApiResponse<UserRating[]>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/user_rating/batch`;
|
||||
|
||||
return this.get<{
|
||||
success?: boolean;
|
||||
ratings?: UserRating[];
|
||||
BadInput?: string;
|
||||
}>({ endpoint })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
data: response.ratings,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public PostUserRating({
|
||||
entityToken,
|
||||
entityType,
|
||||
ratingValue,
|
||||
}: {
|
||||
entityToken: string;
|
||||
entityType: string;
|
||||
ratingValue: string;
|
||||
}): Promise<ApiResponse<number>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/user_rating/rate`;
|
||||
|
||||
const body = {
|
||||
entity_token: entityToken,
|
||||
entity_type: entityType,
|
||||
rating_value: ratingValue,
|
||||
};
|
||||
|
||||
return this.post<
|
||||
{
|
||||
entity_token: string;
|
||||
entity_type: string;
|
||||
rating_value: string;
|
||||
},
|
||||
{
|
||||
success?: boolean;
|
||||
new_positive_rating_count_for_entity?: number;
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint, body })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
data: response.new_positive_rating_count_for_entity,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
|
||||
public ListUserRatingByEntity({
|
||||
entityType,
|
||||
entityToken,
|
||||
}: {
|
||||
entityType: string;
|
||||
entityToken: string;
|
||||
}): Promise<ApiResponse<string>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/user_rating/view/${entityType}/${entityToken}`;
|
||||
|
||||
return this.get<{
|
||||
success?: boolean;
|
||||
maybe_rating_value?: string;
|
||||
BadInput?: string;
|
||||
}>({ endpoint })
|
||||
.then((response) => {
|
||||
return {
|
||||
success: response.success ?? false,
|
||||
data: response.maybe_rating_value,
|
||||
errorMessage: response.BadInput,
|
||||
};
|
||||
})
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
import { ApiManager, ApiResponse } from "../apis/ApiManager";
|
||||
import { UserInfo } from "../models/Users";
|
||||
|
||||
export class UsersApi extends ApiManager {
|
||||
public GetSession(): Promise<
|
||||
ApiResponse<{
|
||||
loggedIn: boolean;
|
||||
user?: UserInfo;
|
||||
}>
|
||||
> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/session`;
|
||||
return this.get<{
|
||||
success: boolean;
|
||||
logged_in: boolean;
|
||||
user?: UserInfo;
|
||||
error_message?: string;
|
||||
}>({ endpoint: endpoint })
|
||||
.then((response) => ({
|
||||
success: response.success,
|
||||
data: {
|
||||
loggedIn: response.logged_in,
|
||||
user: response.user,
|
||||
},
|
||||
}))
|
||||
.catch((err) => {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: err.mesasge,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public GetUserProfile(username: string): Promise<
|
||||
ApiResponse<{
|
||||
user?: UserInfo;
|
||||
}>
|
||||
> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/user/${username}/profile`;
|
||||
return this.get<{
|
||||
success: boolean;
|
||||
user?: UserInfo;
|
||||
error_message?: string;
|
||||
}>({ endpoint: endpoint })
|
||||
.then((response) => ({
|
||||
success: response.success,
|
||||
data: {
|
||||
user: response.user,
|
||||
},
|
||||
}))
|
||||
.catch((err) => {
|
||||
return {
|
||||
success: false,
|
||||
error_message: err.message,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public async Login({
|
||||
usernameOrEmail,
|
||||
password,
|
||||
}: {
|
||||
usernameOrEmail: string;
|
||||
password: string;
|
||||
}): Promise<ApiResponse<{ signedSession?: string }>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/login`;
|
||||
const body = {
|
||||
username_or_email: usernameOrEmail,
|
||||
password: password,
|
||||
};
|
||||
return await this.post<
|
||||
{ username_or_email: string; password: string },
|
||||
{
|
||||
success: boolean;
|
||||
signed_session?: string;
|
||||
error_message?: string;
|
||||
error_type?: string;
|
||||
}
|
||||
>({
|
||||
endpoint: endpoint,
|
||||
body: body,
|
||||
})
|
||||
.then((response) => ({
|
||||
success: response.success,
|
||||
data: { signedSession: response.signed_session },
|
||||
errorMessage: response.error_message,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return {
|
||||
success: false,
|
||||
error_message: err.message,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public Logout(): Promise<ApiResponse<null>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/logout`;
|
||||
return this.post<null, { success: boolean; error_message?: string }>({
|
||||
endpoint: endpoint,
|
||||
})
|
||||
.then((response) => ({
|
||||
success: response.success,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: err.message,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public async Signup({
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
passwordConfirmation,
|
||||
}: {
|
||||
username: string;
|
||||
email: string;
|
||||
password: string;
|
||||
passwordConfirmation: string;
|
||||
}): Promise<ApiResponse<{ signedSession?: string }>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/create_account`;
|
||||
const body = {
|
||||
email_address: email,
|
||||
password,
|
||||
password_confirmation: passwordConfirmation,
|
||||
username,
|
||||
};
|
||||
return await this.post<
|
||||
{
|
||||
username: string;
|
||||
email_address: string;
|
||||
password: string;
|
||||
password_confirmation: string;
|
||||
},
|
||||
{
|
||||
success: boolean;
|
||||
signed_session?: string;
|
||||
error_fields?: Record<string, string>;
|
||||
error_message?: string;
|
||||
error_type?: string;
|
||||
}
|
||||
>({
|
||||
endpoint: endpoint,
|
||||
body: body,
|
||||
})
|
||||
.then((response) => {
|
||||
return {
|
||||
success: response.success,
|
||||
data: { signedSession: response.signed_session },
|
||||
errorMessage: Object.values(response.error_fields ?? {}).join(","),
|
||||
};
|
||||
})
|
||||
.catch((err) => {
|
||||
return {
|
||||
success: false,
|
||||
error_message: err.message,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { ApiManager, ApiResponse } from "../apis/ApiManager";
|
||||
import { Visibility } from "../enums/Visibility";
|
||||
|
||||
interface GenerateVideoStyleTransferRequest {
|
||||
creator_set_visibility: string;
|
||||
disable_lcm: boolean;
|
||||
enable_lipsync: boolean;
|
||||
frame_skip: number;
|
||||
global_ipa_media_token: string;
|
||||
input_depth_file: string;
|
||||
input_file: string;
|
||||
input_normal_file: string;
|
||||
input_outline_file: string;
|
||||
negative_prompt: string;
|
||||
prompt: string;
|
||||
remove_watermark: boolean;
|
||||
style: string;
|
||||
travel_prompt: string;
|
||||
trim_end_millis: number;
|
||||
trim_start_millis: number;
|
||||
use_cinematic: boolean;
|
||||
use_face_detailer: boolean;
|
||||
use_strength: number;
|
||||
use_upscaler: boolean;
|
||||
uuid_idempotency_token: string;
|
||||
}
|
||||
|
||||
export class VideoApi extends ApiManager {
|
||||
public async EnqueueStudio({
|
||||
enqueueVideo,
|
||||
}: {
|
||||
enqueueVideo: GenerateVideoStyleTransferRequest;
|
||||
}): Promise<
|
||||
ApiResponse<{
|
||||
inference_job_token?: string;
|
||||
inference_job_token_type?: string;
|
||||
}>
|
||||
> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/workflows/enqueue_studio`;
|
||||
|
||||
const body = {
|
||||
...enqueueVideo,
|
||||
...(enqueueVideo.creator_set_visibility
|
||||
? {}
|
||||
: { creator_set_visibility: Visibility.Public }),
|
||||
};
|
||||
|
||||
return await this.post<
|
||||
GenerateVideoStyleTransferRequest,
|
||||
{
|
||||
success?: boolean;
|
||||
inference_job_token?: string;
|
||||
inference_job_token_type?: string;
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint, body: body })
|
||||
.then((response) => ({
|
||||
success: Boolean(response.success ?? false),
|
||||
data: {
|
||||
inference_job_token: response.inference_job_token,
|
||||
inference_job_token_type: response.inference_job_token_type,
|
||||
},
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
console.log(err.message);
|
||||
return { success: false, error_reason: err.message };
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { ApiManager, ApiResponse } from "./ApiManager";
|
||||
|
||||
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";
|
||||
import { Pagination } from "../models/Pagination";
|
||||
import { ZsDataset } from "../models/Dataset";
|
||||
|
||||
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 };
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
import { ApiManager, ApiResponse } from "./ApiManager";
|
||||
import { Weight } from "../models/Weight";
|
||||
import { Pagination, PaginationInfinite } from "../models/Pagination";
|
||||
import {
|
||||
FilterEngineCategories,
|
||||
FilterMediaClasses,
|
||||
FilterMediaType,
|
||||
} from "../enums/QueryFilters";
|
||||
import { Visibility } from "../enums/Visibility";
|
||||
|
||||
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 };
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,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,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",
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
# Recursive Folder Comparison Report: Classes vs Classes
|
||||
|
||||
Generated on: 2025-04-16 13:33:19
|
||||
|
||||
## File Comparison Table
|
||||
|
||||
| File Path | Status | Classes | Classes | Difference |
|
||||
|------------------------------------|----------|--------------------|--------------------|------------------------------|
|
||||
|
||||
| ApiManager/MediaFilesApi.ts | be67aab2:44342e0f | 10.6KB (364 lines) | 10.6KB (369 lines) | +56B (+5 lines) 5% different |
|
||||
|
||||
| ApiManager/VoiceDesignerApi.ts | e4d874c3:3635ba42 | 2.0KB (77 lines) | 2.1KB (78 lines) | +94B (+1 lines) 5% different |
|
||||
| ApiManager/PromptsApi.ts | d73d329a:c4824d2e | 648B (25 lines) | 657B (25 lines) | +9B (0 lines) 4% different |
|
||||
|
||||
## Files Present in One Folder But Missing in the Other
|
||||
|
||||
|
||||
### Files Only in Classes (23 files)
|
||||
- `ApiManager/SegmentationApi.ts` (1.9KB, 84 lines, hash: bb14fbf6)
|
||||
- `ApiManager/enums/Api.ts` (254B, 7 lines, hash: ea867d98)
|
||||
- `ApiManager/enums/Billing.ts` (242B, 11 lines, hash: e47c3eaa)
|
||||
- `ApiManager/enums/Job.ts` (433B, 16 lines, hash: d148fd64)
|
||||
- `ApiManager/enums/MediaFileClass.ts` (524B, 26 lines, hash: c90ce542)
|
||||
- `ApiManager/enums/MediaFileType.ts` (1.7KB, 58 lines, hash: a2374fec)
|
||||
- `ApiManager/enums/QueryFilters.ts` (579B, 29 lines, hash: 6d10a292)
|
||||
- `ApiManager/enums/Tts.ts` (205B, 7 lines, hash: f6393d2e)
|
||||
- `ApiManager/enums/UserFeatures.ts` (167B, 6 lines, hash: 05d49e51)
|
||||
- `ApiManager/enums/Visibility.ts` (92B, 5 lines, hash: 297d031b)
|
||||
- `ApiManager/enums/WeightCategory.ts` (180B, 8 lines, hash: f9be3d32)
|
||||
- `ApiManager/enums/WeightType.ts` (208B, 11 lines, hash: 80436726)
|
||||
- `ApiManager/models/BetaKeys.ts` (502B, 26 lines, hash: 7c718ac6)
|
||||
- `ApiManager/models/Billing.tsx` (416B, 17 lines, hash: 8db6eb3b)
|
||||
- `ApiManager/models/Dataset.ts` (588B, 23 lines, hash: 6af860ff)
|
||||
- `ApiManager/models/Job.ts` (1.8KB, 72 lines, hash: 9d7b385a)
|
||||
- `ApiManager/models/MediaFile.ts` (1.8KB, 59 lines, hash: 41778349)
|
||||
- `ApiManager/models/Pagination.ts` (200B, 9 lines, hash: 837b24d3)
|
||||
- `ApiManager/models/Prompts.ts` (424B, 16 lines, hash: 8ef1f366)
|
||||
- `ApiManager/models/StatusAlert.ts` (175B, 9 lines, hash: fcee4684)
|
||||
- `ApiManager/models/UserBookmark.ts` (1.6KB, 75 lines, hash: 8f70d1b1)
|
||||
- `ApiManager/models/Users.ts` (1.3KB, 50 lines, hash: 5ed82bbd)
|
||||
- `ApiManager/models/Weight.ts` (767B, 40 lines, hash: 3f9cccdb)
|
||||
|
||||
### Files Only in Classes (4 files)
|
||||
- `ApiHelpers.ts` (637B, 17 lines, hash: 9b638c6c)
|
||||
- `ApiManager/StudioGen2Api.ts` (1.6KB, 61 lines, hash: b2707195)
|
||||
- `EnvironmentVariables.ts` (263B, 10 lines, hash: 29c93666)
|
||||
- `GetFrontendEnvironment.ts` (1.9KB, 65 lines, hash: 7ee8b738)
|
||||
|
||||
## Files with Differences (Sorted by Most Different First)
|
||||
|
||||
### `ApiManager/MediaUploadApi.ts` (72% different)
|
||||
- Classes: 10.3KB, 349 lines, hash: 707d2bf2
|
||||
- Classes: 10.0KB, 339 lines, hash: 06a93d8c
|
||||
- Difference: -255 bytes, -10 lines
|
||||
|
||||
### `ApiManager/EngineApi.ts` (69% different)
|
||||
- Classes: 1.0KB, 39 lines, hash: c4850e80
|
||||
- Classes: 5.5KB, 213 lines, hash: 85e20a81
|
||||
- Difference: +4469 bytes, +174 lines
|
||||
|
||||
### `ApiManager/JobsApi.ts` (28% different)
|
||||
- Classes: 2.0KB, 77 lines, hash: f7eb8e22
|
||||
- Classes: 1.9KB, 74 lines, hash: 2aa03349
|
||||
- Difference: -56 bytes, -3 lines
|
||||
|
||||
### `ApiManager/ApiManager.ts` (14% different)
|
||||
- Classes: 5.5KB, 235 lines, hash: 44db0b07
|
||||
- Classes: 5.3KB, 211 lines, hash: 67a3a046
|
||||
- Difference: -200 bytes, -24 lines
|
||||
|
||||
### `ApiManager/BillingApi.ts` (6% different)
|
||||
- Classes: 1.0KB, 33 lines, hash: 20051893
|
||||
- Classes: 1.0KB, 33 lines, hash: f5b7af84
|
||||
- Difference: -8 bytes, +0 lines
|
||||
|
||||
### `ApiManager/VoiceDesignerApi.ts` (5% different)
|
||||
- Classes: 2.0KB, 77 lines, hash: e4d874c3
|
||||
- Classes: 2.1KB, 78 lines, hash: 3635ba42
|
||||
- Difference: +94 bytes, +1 lines
|
||||
|
||||
### `ApiManager/MediaFilesApi.ts` (5% different)
|
||||
- Classes: 10.6KB, 364 lines, hash: be67aab2
|
||||
- Classes: 10.6KB, 369 lines, hash: 44342e0f
|
||||
- Difference: +56 bytes, +5 lines
|
||||
|
||||
### `ApiManager/PromptsApi.ts` (4% different)
|
||||
- Classes: 648B, 25 lines, hash: d73d329a
|
||||
- Classes: 657B, 25 lines, hash: c4824d2e
|
||||
- Difference: +9 bytes, +0 lines
|
||||
|
||||
### `ApiManager/MiscApi.ts` (3% different)
|
||||
- Classes: 799B, 26 lines, hash: 298f5d93
|
||||
- Classes: 799B, 26 lines, hash: 187ad676
|
||||
- Difference: +0 bytes, +0 lines
|
||||
|
||||
### `ApiManager/TtsApi.ts` (3% different)
|
||||
- Classes: 1.7KB, 64 lines, hash: 5feb5cad
|
||||
- Classes: 1.7KB, 64 lines, hash: 6228b278
|
||||
- Difference: +2 bytes, +0 lines
|
||||
|
||||
### `ApiManager/index.ts` (2% different)
|
||||
- Classes: 564B, 18 lines, hash: 55a9094b
|
||||
- Classes: 597B, 19 lines, hash: 1fe59701
|
||||
- Difference: +33 bytes, +1 lines
|
||||
|
||||
### `ApiManager/UserBookmarksApi.ts` (2% different)
|
||||
- Classes: 5.4KB, 201 lines, hash: e3e4cb19
|
||||
- Classes: 5.5KB, 202 lines, hash: fd3afc78
|
||||
- Difference: +56 bytes, +1 lines
|
||||
|
||||
### `ApiManager/WeightsApi.ts` (2% different)
|
||||
- Classes: 7.8KB, 295 lines, hash: c12a98df
|
||||
- Classes: 7.9KB, 296 lines, hash: 8a7dbb68
|
||||
- Difference: +39 bytes, +1 lines
|
||||
|
||||
### `ApiManager/VideoApi.ts` (1% different)
|
||||
- Classes: 1.9KB, 70 lines, hash: a9947328
|
||||
- Classes: 1.9KB, 70 lines, hash: cb947c07
|
||||
- Difference: -11 bytes, +0 lines
|
||||
|
||||
### `ApiManager/BetaKeysApi.ts` (1% different)
|
||||
- Classes: 4.3KB, 167 lines, hash: 8d4ec6f1
|
||||
- Classes: 4.3KB, 167 lines, hash: 33f1e4b3
|
||||
- Difference: -3 bytes, +0 lines
|
||||
|
||||
### `ApiManager/UsersApi.ts` (0% different)
|
||||
- Classes: 3.9KB, 161 lines, hash: 65121217
|
||||
- Classes: 3.9KB, 161 lines, hash: 72db0eae
|
||||
- Difference: -6 bytes, +0 lines
|
||||
|
||||
|
||||
## Summary
|
||||
- **Total Unique Files**: 49
|
||||
- **Files in Classes**: 45
|
||||
- **Files in Classes**: 26
|
||||
- **Identical Files**: 6
|
||||
- **Different Files**: 16
|
||||
- **Files Only in Classes**: 23
|
||||
- **Files Only in Classes**: 4
|
||||
|
||||
## Path Information
|
||||
- Classes: `/Users/michaelchung/storyteller-rust/frontend/apps/editor2d/src/Classes/`
|
||||
- Classes: `/Users/michaelchung/storyteller-rust/frontend/apps/editor3d/app/Classes/`
|
||||
@@ -0,0 +1,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";
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { JobStatus } from "../enums/Job";
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { USER_FEATURE_FLAGS } from "../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;
|
||||
}
|
||||
Reference in New Issue
Block a user