mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-26 11:49:30 -04:00
Compare commits
7
Commits
feature/fk
...
bugfix/fk
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
942005282a | ||
|
|
14474e68c2 | ||
|
|
2f44da8ac7 | ||
|
|
3d67f86a19 | ||
|
|
2913d91d0d | ||
|
|
ceb77505e1 | ||
|
|
0cad3fe7da |
@@ -234,17 +234,31 @@ impl BucketClient {
|
||||
.map(|s| s.to_string())
|
||||
.ok_or(anyhow!("could not convert object path to string"))?;
|
||||
|
||||
info!("downloading from bucket: {:?}", &object_path_str);
|
||||
info!("creating file for bucket download: {:?}", filesystem_path.as_ref());
|
||||
|
||||
let mut output_file = tokio::fs::File::create(filesystem_path).await?;
|
||||
let mut output_file = File::create(filesystem_path).await?;
|
||||
|
||||
let status_code = self.bucket.get_object_to_writer(&object_path_str, &mut output_file).await?;
|
||||
let result = self.bucket.get_object_to_writer(&object_path_str, &mut output_file).await;
|
||||
|
||||
info!("downloading from bucket (named '{}'), path: {}", &self.bucket.name, &object_path_str);
|
||||
|
||||
let status_code = match result {
|
||||
Ok(status_code) => status_code,
|
||||
Err(err) => {
|
||||
return bail!("Error downloading from bucket (named '{}'): {:?}", &self.bucket.name, err)
|
||||
}
|
||||
};
|
||||
|
||||
match status_code {
|
||||
404 => bail!("File not found in bucket: {}", &object_path_str),
|
||||
_ => {},
|
||||
404 => {
|
||||
error!("File not found in bucket (named '{}'), path: {}", &self.bucket.name, &object_path_str);
|
||||
bail!("File not found in bucket: {}", &object_path_str)
|
||||
},
|
||||
_ => {
|
||||
info!("download code: {}", status_code);
|
||||
},
|
||||
}
|
||||
info!("download code: {}", status_code);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pub mod set_sora_secret_handler;
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::{HttpRequest, HttpResponse, web, Either};
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::{Form, Json, Path};
|
||||
use anyhow::anyhow;
|
||||
use log::{error, log, warn};
|
||||
use r2d2_redis::redis::Commands;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use enums::by_table::media_files::media_file_animation_type::MediaFileAnimationType;
|
||||
use enums::by_table::media_files::media_file_engine_category::MediaFileEngineCategory;
|
||||
use http_server_common::response::serialize_as_json_error::serialize_as_json_error;
|
||||
use mysql_queries::queries::media_files::edit::update_media_file_animation_type::update_media_file_animation_type;
|
||||
use mysql_queries::queries::media_files::get::get_media_file::get_media_file;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::state::server_state::ServerState;
|
||||
use crate::util::redis_sora_secrets::get_sora_credentials;
|
||||
use crate::util::redis_sora_secrets::set_sora_credential_field;
|
||||
use crate::util::redis_sora_secrets::RedisSoraCredentialSubkey;
|
||||
|
||||
#[derive(Deserialize, Clone, Copy)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SetSoraSecretType {
|
||||
Bearer,
|
||||
Cookie,
|
||||
Sentinel
|
||||
}
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct SetSoraSecretRequest {
|
||||
pub key: SetSoraSecretType,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct SetSoraSecretSuccessResponse {
|
||||
pub success: bool,
|
||||
pub bearer: String,
|
||||
pub cookie: String,
|
||||
pub sentinel: String,
|
||||
}
|
||||
|
||||
// =============== Error Response ===============
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub enum SetSoraSecretError {
|
||||
BadInput(String),
|
||||
NotAuthorized,
|
||||
ServerError,
|
||||
}
|
||||
|
||||
impl ResponseError for SetSoraSecretError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
SetSoraSecretError::BadInput(_) => StatusCode::BAD_REQUEST,
|
||||
SetSoraSecretError::NotAuthorized => StatusCode::UNAUTHORIZED,
|
||||
SetSoraSecretError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
serialize_as_json_error(self)
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for SetSoraSecretError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
// =============== Handler ===============
|
||||
|
||||
/// Set a Sora secret
|
||||
#[utoipa::path(
|
||||
post,
|
||||
tag = "Control Plane",
|
||||
path = "/v1/control_plane/sora_secret",
|
||||
responses(
|
||||
(status = 200, description = "Success", body = SetSoraSecretSuccessResponse),
|
||||
(status = 400, description = "Bad input", body = SetSoraSecretError),
|
||||
(status = 401, description = "Not authorized", body = SetSoraSecretError),
|
||||
(status = 500, description = "Server error", body = SetSoraSecretError),
|
||||
),
|
||||
params(
|
||||
("request" = SetSoraSecretRequest, description = "Payload for Request"),
|
||||
("path" = MediaFileTokenPathInfo, description = "Path for Request")
|
||||
)
|
||||
)]
|
||||
pub async fn set_sora_secret_handler(
|
||||
http_request: HttpRequest,
|
||||
request: Form<SetSoraSecretRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<Json<SetSoraSecretSuccessResponse>, SetSoraSecretError> {
|
||||
|
||||
// TODO(bt,2025-04-03): Some form of authentication should be required for this endpoint.
|
||||
let value = request.value.trim().to_string();
|
||||
|
||||
let mut redis = server_state.redis_pool
|
||||
.get()
|
||||
.map_err(|e| {
|
||||
error!("redis error: {:?}", e);
|
||||
SetSoraSecretError::ServerError
|
||||
})?;
|
||||
|
||||
set_sora_credential_field(
|
||||
&mut redis,
|
||||
match request.key {
|
||||
SetSoraSecretType::Bearer => RedisSoraCredentialSubkey::Bearer,
|
||||
SetSoraSecretType::Cookie => RedisSoraCredentialSubkey::Cookie,
|
||||
SetSoraSecretType::Sentinel => RedisSoraCredentialSubkey::Sentinel,
|
||||
},
|
||||
&value
|
||||
).map_err(|e| {
|
||||
error!("redis error: {:?}", e);
|
||||
SetSoraSecretError::ServerError
|
||||
})?;
|
||||
|
||||
let credentials = get_sora_credentials(&mut redis)
|
||||
.map_err(|e| {
|
||||
error!("redis error: {:?}", e);
|
||||
SetSoraSecretError::ServerError
|
||||
})?;
|
||||
|
||||
// TODO(bt,2025-04-03): Probably not smart to return, but useful for debugging.
|
||||
Ok(Json(SetSoraSecretSuccessResponse {
|
||||
success: true,
|
||||
bearer: credentials.bearer_token,
|
||||
cookie: credentials.cookie,
|
||||
sentinel: credentials.sentinel.unwrap_or_else(|| "".to_string()),
|
||||
}))
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
use actix_web::HttpRequest;
|
||||
use anyhow::anyhow;
|
||||
use log::{error, info};
|
||||
use r2d2_redis::r2d2::PooledConnection;
|
||||
use r2d2_redis::RedisConnectionManager;
|
||||
use errors::AnyhowResult;
|
||||
use http_server_common::request::get_request_header_optional::get_request_header_optional;
|
||||
use openai_sora_client::credentials::SoraCredentials;
|
||||
use crate::util::redis_sora_secrets::get_sora_credentials;
|
||||
|
||||
const SORA_BEARER_HEADER_NAME : &str = "sora-bearer";
|
||||
const SORA_COOKIE_HEADER_NAME : &str = "sora-cookie";
|
||||
const SORA_SENTINEL_HEADER_NAME : &str = "sora-sentinel";
|
||||
|
||||
pub fn get_sora_credentials_from_request(
|
||||
http_request: &HttpRequest,
|
||||
redis: &mut PooledConnection<RedisConnectionManager>,
|
||||
) -> AnyhowResult<SoraCredentials> {
|
||||
|
||||
let sora_bearer = get_request_header_optional(&http_request, SORA_BEARER_HEADER_NAME);
|
||||
let sora_cookie = get_request_header_optional(&http_request, SORA_COOKIE_HEADER_NAME);
|
||||
let sora_sentinel = get_request_header_optional(&http_request, SORA_SENTINEL_HEADER_NAME);
|
||||
|
||||
match (&sora_bearer, &sora_cookie, &sora_sentinel) {
|
||||
(Some(bearer), Some(cookie), Some(sentinel)) => {
|
||||
info!("Sora credentials were present in HTTP request headers!");
|
||||
|
||||
return Ok(SoraCredentials {
|
||||
bearer_token: bearer.to_string(),
|
||||
cookie: cookie.to_string(),
|
||||
sentinel: Some(sentinel.to_string()),
|
||||
});
|
||||
}
|
||||
_ => {} // Fall through
|
||||
}
|
||||
|
||||
let redis_result = get_sora_credentials(redis);
|
||||
|
||||
match redis_result {
|
||||
Ok(mut credentials) => {
|
||||
if let Some(bearer) = sora_bearer {
|
||||
info!("override bearer value");
|
||||
credentials.bearer_token = bearer;
|
||||
}
|
||||
if let Some(cookie) = sora_cookie {
|
||||
info!("override cookie value");
|
||||
credentials.cookie = cookie;
|
||||
}
|
||||
if let Some(sentinel) = sora_sentinel {
|
||||
info!("override sentinel value");
|
||||
credentials.sentinel = Some(sentinel);
|
||||
}
|
||||
return Ok(credentials)
|
||||
},
|
||||
Err(err) => {
|
||||
error!("Failed to get Sora credentials from Redis; using env as backup. Error: {:?}", err);
|
||||
// NB: Fall through
|
||||
}
|
||||
};
|
||||
|
||||
match SoraCredentials::from_env() {
|
||||
Ok(creds) => Ok(creds),
|
||||
Err(err) => {
|
||||
error!("Failed to load Sora credentials from environment: {:?}", err);
|
||||
Err(anyhow!("Failed to load Sora credentials from environment"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod upload;
|
||||
pub mod prompt;
|
||||
pub mod prompt;
|
||||
pub mod get_sora_credentials_from_request;
|
||||
+41
-95
@@ -11,13 +11,14 @@ use actix_web::web::Json;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use cloud_storage::bucket_client::BucketClient;
|
||||
use log::{debug, error};
|
||||
use log::{debug, error, info};
|
||||
use log::warn;
|
||||
use mysql_queries::queries::media_files::get::get_media_file::get_media_file;
|
||||
use openai_sora_client::credentials::SoraCredentials;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use sqlx::MySqlPool;
|
||||
use tempdir::TempDir;
|
||||
use utoipa::ToSchema;
|
||||
use web::Data;
|
||||
|
||||
@@ -47,10 +48,13 @@ use tokens::tokens::model_weights::ModelWeightToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
use crate::configs::plans::get_correct_plan_for_session::get_correct_plan_for_session;
|
||||
use crate::http_server::endpoints::image_studio::get_sora_credentials_from_request::get_sora_credentials_from_request;
|
||||
use crate::http_server::endpoints::tts::get_tts_inference_job_status::GetTtsInferenceStatusError;
|
||||
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::state::server_state::ServerState;
|
||||
use crate::util::allowed_studio_access::allowed_studio_access;
|
||||
use crate::util::redis_sora_secrets::get_sora_credentials;
|
||||
|
||||
/// This is the number of images (batch size) to generate for each request.
|
||||
/// We should allow all users to have multiple images generated at once as this
|
||||
@@ -68,10 +72,6 @@ const DEBUG_HEADER_NAME: &str = "enable-debug-mode";
|
||||
/// This is useful for catching the live logs or intercepting the job.
|
||||
const ROUTING_TAG_HEADER_NAME: &str = "routing-tag";
|
||||
|
||||
const SORA_BEARER_HEADER_NAME : &str = "sora-bearer";
|
||||
const SORA_COOKIE_HEADER_NAME : &str = "sora-cookie";
|
||||
const SORA_SENTINEL_HEADER_NAME : &str = "sora-sentinel";
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct EnqueueStudioImageGenRequest {
|
||||
pub uuid_idempotency_token: String,
|
||||
@@ -195,7 +195,6 @@ pub async fn enqueue_studio_image_generation_handler(
|
||||
|routing_tag| routing_tag.trim().to_string()
|
||||
);
|
||||
|
||||
|
||||
// ==================== BANNED USERS ==================== //
|
||||
|
||||
if let Some(ref user) = maybe_user_session {
|
||||
@@ -220,7 +219,6 @@ pub async fn enqueue_studio_image_generation_handler(
|
||||
// Get up IP address
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
|
||||
// ==================== HANDLE IDEMPOTENCY ==================== //
|
||||
|
||||
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
|
||||
@@ -239,17 +237,14 @@ pub async fn enqueue_studio_image_generation_handler(
|
||||
// TODO: Maybe this moves to a job.
|
||||
|
||||
let work_temp_dir = server_state.temp_dir_creator.new_tempdir("image_studio").unwrap();
|
||||
|
||||
let public_bucket_client = server_state.public_bucket_client.clone();
|
||||
let private_bucket_client = server_state.private_bucket_client.clone();
|
||||
|
||||
let scene_media_token = request.snapshot_media_token.clone();
|
||||
let scene_media_name = scene_media_token.to_string();
|
||||
let scene_media_path = work_temp_dir.path().join(scene_media_name.clone());
|
||||
|
||||
let scene_media_path = do_download_media_file(
|
||||
&scene_media_name,
|
||||
let scene_media_path = query_and_download_media_file(
|
||||
&scene_media_token,
|
||||
&scene_media_path,
|
||||
&work_temp_dir,
|
||||
&public_bucket_client,
|
||||
&server_state.mysql_pool
|
||||
).await.map_err(|err| {
|
||||
@@ -265,13 +260,9 @@ pub async fn enqueue_studio_image_generation_handler(
|
||||
let additional_images = request.maybe_additional_images.clone().unwrap_or_else(|| vec![]);
|
||||
|
||||
for media_file_token in &additional_images {
|
||||
let media_file_name = media_file_token.to_string();
|
||||
let media_file_path = work_temp_dir.path().join(media_file_name.clone());
|
||||
|
||||
let media_file_path = do_download_media_file(
|
||||
&media_file_name,
|
||||
let media_file_path = query_and_download_media_file(
|
||||
&media_file_token,
|
||||
&media_file_path,
|
||||
&work_temp_dir,
|
||||
&public_bucket_client,
|
||||
&server_state.mysql_pool
|
||||
).await.map_err(|err| {
|
||||
@@ -284,30 +275,18 @@ pub async fn enqueue_studio_image_generation_handler(
|
||||
|
||||
// ==================== HANDLE SORA CREDENTIALS ==================== //
|
||||
|
||||
let sora_bearer = get_request_header_optional(&http_request, SORA_BEARER_HEADER_NAME);
|
||||
let sora_cookie = get_request_header_optional(&http_request, SORA_COOKIE_HEADER_NAME);
|
||||
let sora_sentinel = get_request_header_optional(&http_request, SORA_SENTINEL_HEADER_NAME);
|
||||
let mut redis = server_state.redis_pool
|
||||
.get()
|
||||
.map_err(|e| {
|
||||
error!("redis error: {:?}", e);
|
||||
EnqueueImageGenRequestError::ServerError
|
||||
})?;
|
||||
|
||||
let sora_credentials;
|
||||
|
||||
match (sora_bearer, sora_cookie, sora_sentinel) {
|
||||
(Some(bearer), Some(cookie), Some(sentinel)) => {
|
||||
sora_credentials = SoraCredentials {
|
||||
bearer_token: bearer,
|
||||
cookie,
|
||||
sentinel: Some(sentinel),
|
||||
};
|
||||
}
|
||||
_ => {
|
||||
sora_credentials = match SoraCredentials::from_env() {
|
||||
Ok(creds) => creds,
|
||||
Err(err) => {
|
||||
error!("Failed to load Sora credentials from environment: {:?}", err);
|
||||
return Err(EnqueueImageGenRequestError::ServerError);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
let sora_credentials = get_sora_credentials_from_request(&http_request, &mut redis)
|
||||
.map_err(|e| {
|
||||
error!("sora credential error: {:?}", e);
|
||||
EnqueueImageGenRequestError::ServerError
|
||||
})?;
|
||||
|
||||
// ==================== HANDLE SORA UPLOAD ==================== //
|
||||
|
||||
@@ -436,84 +415,51 @@ pub enum DownloadMediaFileError {
|
||||
Other(anyhow::Error),
|
||||
}
|
||||
|
||||
impl From<GetMediaFileError> for DownloadMediaFileError {
|
||||
fn from(err: GetMediaFileError) -> Self {
|
||||
match err {
|
||||
GetMediaFileError::Other(e) => DownloadMediaFileError::Other(e),
|
||||
GetMediaFileError::NotFound => DownloadMediaFileError::MediaFileNotFound,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for DownloadMediaFileError {
|
||||
fn from(err: anyhow::Error) -> Self {
|
||||
DownloadMediaFileError::Other(err)
|
||||
}
|
||||
}
|
||||
|
||||
async fn do_download_media_file(
|
||||
media_file_name: &str,
|
||||
async fn query_and_download_media_file(
|
||||
media_file_token: &MediaFileToken,
|
||||
download_directory: &Path,
|
||||
download_dir: &TempDir,
|
||||
bucket_client: &BucketClient,
|
||||
mysql_pool: &MySqlPool,
|
||||
) -> Result<(PathBuf), DownloadMediaFileError> {
|
||||
let final_download_path = download_directory.join(format!("{}", media_file_name));
|
||||
|
||||
if final_download_path.exists() {
|
||||
log::debug!("Deleting existing file at {:?}", final_download_path.display().to_string());
|
||||
std::fs::remove_file(&final_download_path)
|
||||
.map_err(|err| DownloadMediaFileError::IoError(err))?;
|
||||
}
|
||||
|
||||
let bucket_object_path = get_media_file_details(&media_file_token, mysql_pool).await?;
|
||||
|
||||
log::info!("Downloading media from bucket path: {:?}", &bucket_object_path);
|
||||
|
||||
bucket_client.download_file_to_disk(
|
||||
&bucket_object_path,
|
||||
&final_download_path,
|
||||
).await?;
|
||||
|
||||
Ok(final_download_path)
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
enum GetMediaFileError {
|
||||
NotFound,
|
||||
Other(anyhow::Error),
|
||||
}
|
||||
|
||||
|
||||
async fn get_media_file_details(media_file_token: &MediaFileToken, mysql_pool: &MySqlPool) -> Result<PathBuf, GetMediaFileError> {
|
||||
) -> Result<PathBuf, DownloadMediaFileError> {
|
||||
let media_file_result = get_media_file(
|
||||
&media_file_token,
|
||||
false,
|
||||
mysql_pool
|
||||
).await;
|
||||
|
||||
let media_file_result = match media_file_result {
|
||||
let media_file= match media_file_result {
|
||||
Ok(Some(result)) => result,
|
||||
Ok(None) => {
|
||||
warn!("could not find media file: {:?}", media_file_token);
|
||||
return Err(GetMediaFileError::NotFound)
|
||||
warn!("could not find media file in database: {:?}", media_file_token);
|
||||
return Err(DownloadMediaFileError::MediaFileNotFound)
|
||||
}
|
||||
Err(e) => {
|
||||
error!("could not query media file: {:?}", e);
|
||||
return Err(GetMediaFileError::Other(e))
|
||||
return Err(DownloadMediaFileError::Other(e))
|
||||
}
|
||||
};
|
||||
|
||||
let media_file_bucket_path = MediaFileBucketPath::from_object_hash(
|
||||
&media_file_result.public_bucket_directory_hash,
|
||||
media_file_result.maybe_public_bucket_prefix.as_deref(),
|
||||
media_file_result.maybe_public_bucket_extension.as_deref());
|
||||
&media_file.public_bucket_directory_hash,
|
||||
media_file.maybe_public_bucket_prefix.as_deref(),
|
||||
media_file.maybe_public_bucket_extension.as_deref());
|
||||
|
||||
Ok(media_file_bucket_path.to_full_object_pathbuf())
|
||||
}
|
||||
let extension = media_file.maybe_public_bucket_extension.clone().unwrap_or_else(|| ".jpg".to_string());
|
||||
let download_filename = format!("download_file_{}.{}", media_file.token.as_str(), extension);
|
||||
let download_file_path = download_dir.path().join(download_filename);
|
||||
|
||||
info!("Downloading from bucket...");
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
bucket_client.download_file_to_disk(
|
||||
&media_file_bucket_path.to_full_object_pathbuf(),
|
||||
&download_file_path,
|
||||
).await?;
|
||||
|
||||
Ok(download_file_path)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ pub mod app_state;
|
||||
pub mod beta_keys;
|
||||
pub mod categories;
|
||||
pub mod comments;
|
||||
pub mod control_plane;
|
||||
pub mod conversion;
|
||||
pub mod download_job;
|
||||
pub mod engine;
|
||||
@@ -12,6 +13,7 @@ pub mod events;
|
||||
pub mod featured_items;
|
||||
pub mod flags;
|
||||
pub mod image_gen;
|
||||
pub mod image_studio;
|
||||
pub mod inference_job;
|
||||
pub mod investor_demo;
|
||||
pub mod leaderboard;
|
||||
@@ -30,9 +32,9 @@ pub mod subscriptions;
|
||||
pub mod tags;
|
||||
pub mod trending;
|
||||
pub mod tts;
|
||||
pub mod users;
|
||||
pub mod user_bookmarks;
|
||||
pub mod user_ratings;
|
||||
pub mod users;
|
||||
pub mod vocoders;
|
||||
pub mod voice_clone_requests;
|
||||
pub mod voice_conversion;
|
||||
@@ -40,4 +42,3 @@ pub mod voice_designer;
|
||||
pub mod w2l;
|
||||
pub mod weights;
|
||||
pub mod workflows;
|
||||
pub mod image_studio;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
use actix_http::body::MessageBody;
|
||||
use actix_service::ServiceFactory;
|
||||
use actix_web::dev::{ServiceRequest, ServiceResponse};
|
||||
use actix_web::{web, App, Error, HttpResponse};
|
||||
use crate::http_server::endpoints::control_plane::set_sora_secret_handler::set_sora_secret_handler;
|
||||
|
||||
pub fn add_control_plane_routes<T, B> (app: App<T>) -> App<T>
|
||||
where
|
||||
B: MessageBody,
|
||||
T: ServiceFactory<
|
||||
ServiceRequest,
|
||||
Config = (),
|
||||
Response = ServiceResponse<B>,
|
||||
Error = Error,
|
||||
InitError = (),
|
||||
>,
|
||||
{
|
||||
app.service(web::scope("/v1/control_plane")
|
||||
.service(web::resource("/sora_secret")
|
||||
.route(web::post().to(set_sora_secret_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -123,6 +123,7 @@ use crate::http_server::endpoints::w2l::get_w2l_template_use_count::get_w2l_temp
|
||||
use crate::http_server::endpoints::w2l::get_w2l_upload_template_job_status::get_w2l_upload_template_job_status_handler;
|
||||
use crate::http_server::endpoints::w2l::list_w2l_templates::list_w2l_templates_handler;
|
||||
use crate::http_server::endpoints::w2l::set_w2l_template_mod_approval::set_w2l_template_mod_approval_handler;
|
||||
use crate::http_server::routes::add_control_plane_routes::add_control_plane_routes;
|
||||
use crate::http_server::routes::add_image_studio_routes::add_image_studio_routes;
|
||||
use crate::http_server::routes::add_studio_gen2_routes::add_studio_gen2_routes;
|
||||
use crate::http_server::routes::beta_key_routes::add_beta_key_routes;
|
||||
@@ -175,6 +176,7 @@ pub fn add_routes<T, B> (app: App<T>, server_environment: ServerEnvironment) ->
|
||||
app = add_image_studio_routes(app);
|
||||
app = add_job_routes(app);
|
||||
app = add_engine_routes(app); /* /v1/engine/... */
|
||||
app = add_control_plane_routes(app); /* /v1/control_plane/... */
|
||||
|
||||
// app = add_image_gen_routes(app);
|
||||
|
||||
|
||||
@@ -9,4 +9,5 @@ pub mod user_routes;
|
||||
pub mod weights_routes;
|
||||
pub mod workflow_routes;
|
||||
mod tag_routes;
|
||||
mod add_image_studio_routes;
|
||||
mod add_image_studio_routes;
|
||||
mod add_control_plane_routes;
|
||||
+14
@@ -11,6 +11,20 @@ pub struct ScopedTempDirCreator {
|
||||
}
|
||||
|
||||
impl ScopedTempDirCreator {
|
||||
pub fn auto_setup() -> Self {
|
||||
// Production configuration
|
||||
let maybe_tmp = easyenv::get_env_pathbuf_optional("TEMP_DIR");
|
||||
|
||||
// MacOS
|
||||
// TODO: Only run this on macOS
|
||||
let maybe_mac_tmp = easyenv::get_env_pathbuf_optional("TMPDIR");
|
||||
|
||||
let directory = maybe_tmp
|
||||
.or(maybe_mac_tmp)
|
||||
.unwrap_or_else(|| PathBuf::from("/tmp"));
|
||||
|
||||
Self::for_directory(directory)
|
||||
}
|
||||
|
||||
pub fn for_directory<P: AsRef<Path>>(base_dir: P) -> Self {
|
||||
Self {
|
||||
|
||||
@@ -464,7 +464,7 @@ async fn main() -> AnyhowResult<()> {
|
||||
user_tokens: user_token_troll_bans,
|
||||
ip_addresses: ip_address_troll_bans,
|
||||
},
|
||||
temp_dir_creator: ScopedTempDirCreator::for_directory(easyenv::get_env_pathbuf_or_default("TEMP_DIR", "/tmp")),
|
||||
temp_dir_creator: ScopedTempDirCreator::auto_setup(),
|
||||
google_sign_in_cert: GoogleSignInCert::new(),
|
||||
};
|
||||
|
||||
|
||||
@@ -11,5 +11,6 @@ pub mod enroll_in_studio;
|
||||
pub mod generate_random_username;
|
||||
pub mod placeholder_images;
|
||||
pub mod read_toml_file_to_struct;
|
||||
pub mod redis_sora_secrets;
|
||||
pub mod title_to_url_slug;
|
||||
pub mod troll_user_bans;
|
||||
@@ -0,0 +1,67 @@
|
||||
use std::collections::HashMap;
|
||||
use anyhow::anyhow;
|
||||
use r2d2_redis::r2d2::PooledConnection;
|
||||
use r2d2_redis::redis::Commands;
|
||||
use r2d2_redis::RedisConnectionManager;
|
||||
use errors::AnyhowResult;
|
||||
use openai_sora_client::credentials::SoraCredentials;
|
||||
|
||||
/// Redis key
|
||||
const SORA_SECRET_REDIS_KEY : &str = "sora_secret";
|
||||
|
||||
// Fields within the HKEY
|
||||
const BEARER_SUBKEY : &str = "bearer";
|
||||
const COOKIE_SUBKEY : &str = "cookie";
|
||||
const SENTINEL_SUBKEY : &str = "sentinel";
|
||||
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
pub enum RedisSoraCredentialSubkey {
|
||||
Bearer,
|
||||
Cookie,
|
||||
Sentinel,
|
||||
}
|
||||
|
||||
impl RedisSoraCredentialSubkey {
|
||||
pub fn to_str(&self) -> &'static str {
|
||||
match self {
|
||||
RedisSoraCredentialSubkey::Bearer => BEARER_SUBKEY,
|
||||
RedisSoraCredentialSubkey::Cookie => COOKIE_SUBKEY,
|
||||
RedisSoraCredentialSubkey::Sentinel => SENTINEL_SUBKEY,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_sora_credentials(
|
||||
redis: &mut PooledConnection<RedisConnectionManager>
|
||||
) -> AnyhowResult<SoraCredentials> {
|
||||
|
||||
let values : HashMap<String, String> = redis.hgetall(SORA_SECRET_REDIS_KEY)
|
||||
.map_err(|e| anyhow!("Failed to get Sora credentials from Redis: {}", e))?;
|
||||
|
||||
let bearer = values.get(BEARER_SUBKEY);
|
||||
let cookie = values.get(COOKIE_SUBKEY);
|
||||
let sentinel = values.get(SENTINEL_SUBKEY);
|
||||
|
||||
match (bearer, cookie, sentinel) {
|
||||
(Some(b), Some(c), Some(s)) => {
|
||||
Ok(SoraCredentials {
|
||||
bearer_token: b.to_string(),
|
||||
cookie: c.to_string(),
|
||||
sentinel: Some(s.to_string()),
|
||||
})
|
||||
}
|
||||
_ => Err(anyhow!("redis sora credential values not present")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_sora_credential_field(
|
||||
redis: &mut PooledConnection<RedisConnectionManager>,
|
||||
subkey: RedisSoraCredentialSubkey,
|
||||
value: &str,
|
||||
) -> AnyhowResult<()> {
|
||||
|
||||
redis.hset(SORA_SECRET_REDIS_KEY, subkey.to_str(), value)
|
||||
.map_err(|e| anyhow!("Failed to set Sora credentials in Redis: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -36,4 +36,174 @@ export class EngineApi extends ApiManager {
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
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({
|
||||
prompt,
|
||||
snapshotMediaToken,
|
||||
additionalImages,
|
||||
}: {
|
||||
prompt: string;
|
||||
snapshotMediaToken: string;
|
||||
additionalImages?: string[];
|
||||
}): Promise<ApiResponse<string>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/image_studio/prompt`;
|
||||
// for now ...
|
||||
const uuidIdempotencyToken = crypto.randomUUID(); // Generate a new UUID
|
||||
const body = {
|
||||
uuid_idempotency_token: uuidIdempotencyToken,
|
||||
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;
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ export const uploadImage = async ({
|
||||
}) => {
|
||||
const mediaUploadApi = new MediaUploadApi();
|
||||
|
||||
console.log("Uploading image:", assetFile);
|
||||
|
||||
progressCallback({ status: UploaderStates.uploadingImage });
|
||||
|
||||
const imageResponse = await mediaUploadApi.UploadImage({
|
||||
@@ -24,7 +26,10 @@ export const uploadImage = async ({
|
||||
maybe_title: "char_frame_" + title,
|
||||
});
|
||||
|
||||
console.log("Image response:", imageResponse);
|
||||
|
||||
if (imageResponse == undefined) {
|
||||
console.log("Error: Could not upload image!");
|
||||
progressCallback({
|
||||
status: UploaderStates.imageCreateError,
|
||||
errorMessage: "Could not upload image!",
|
||||
@@ -33,6 +38,7 @@ export const uploadImage = async ({
|
||||
}
|
||||
|
||||
if (!imageResponse.success || !imageResponse.data) {
|
||||
console.log("Error:", imageResponse.errorMessage);
|
||||
progressCallback({
|
||||
status: UploaderStates.imageCreateError,
|
||||
errorMessage: imageResponse.errorMessage,
|
||||
@@ -40,5 +46,9 @@ export const uploadImage = async ({
|
||||
return;
|
||||
}
|
||||
|
||||
progressCallback({ status: UploaderStates.success, data: imageResponse.data });
|
||||
console.log("Upload successful:", imageResponse.data);
|
||||
progressCallback({
|
||||
status: UploaderStates.success,
|
||||
data: imageResponse.data,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import { TransformControls } from "three/examples/jsm/Addons";
|
||||
import * as THREE from "three";
|
||||
|
||||
export const FKHelperSphereName = "__FKHelperSphere__";
|
||||
|
||||
export class FKHelper {
|
||||
|
||||
private targetBoneSpheres: THREE.Object3D<THREE.Object3DEventMap>[];
|
||||
private transformControls: TransformControls;
|
||||
private raycaster: THREE.Raycaster;
|
||||
private scene: THREE.Scene;
|
||||
|
||||
private skeletonHelper: THREE.SkeletonHelper | null = null;
|
||||
private isControlInScene = false;
|
||||
|
||||
// Need the actual object to perform FK on
|
||||
// This means the object must have bones
|
||||
// Not all bones require FK right now
|
||||
// Need a set of names to filter bones
|
||||
// Use mixamo names
|
||||
// Need a transform control (rotation mode) for the bones
|
||||
// Need to disable the object transforms for bone transforms
|
||||
// Need raycaster
|
||||
constructor({
|
||||
camera,
|
||||
domElement,
|
||||
scene,
|
||||
onDragChange
|
||||
}: {
|
||||
camera: THREE.Camera;
|
||||
domElement: HTMLElement;
|
||||
scene: THREE.Scene;
|
||||
onDragChange: (dragging: boolean) => void;
|
||||
}) {
|
||||
this.raycaster = new THREE.Raycaster();
|
||||
this.scene = scene;
|
||||
this.targetBoneSpheres = [];
|
||||
this.transformControls = new TransformControls(camera, domElement);
|
||||
|
||||
this.transformControls.setSpace("local");
|
||||
this.transformControls.setMode("rotate");
|
||||
this.transformControls.setSize(0.5);
|
||||
this.transformControls.addEventListener("dragging-changed", (event: any) => {
|
||||
onDragChange(event.value);
|
||||
});
|
||||
}
|
||||
|
||||
// Set FK target
|
||||
setTarget(target: THREE.Object3D) {
|
||||
// TODO: Highlight all the necessary bones
|
||||
this.clear();
|
||||
target.traverse((child) => {
|
||||
if (child.type !== "Bone") {
|
||||
return;
|
||||
}
|
||||
|
||||
// For each target bone, display a sphere to show the FK target and raycast intersect
|
||||
const geometry = new THREE.SphereGeometry(1, 16, 16);
|
||||
const material = new THREE.MeshBasicMaterial({ color: 0xff0000, depthTest: false, transparent: true });
|
||||
const sphere = new THREE.Mesh(geometry, material);
|
||||
sphere.name = FKHelperSphereName;
|
||||
const uniformScale = 1.5;
|
||||
sphere.scale.set(uniformScale, uniformScale, uniformScale);
|
||||
child.add(sphere);
|
||||
this.targetBoneSpheres.push(sphere);
|
||||
});
|
||||
|
||||
this.skeletonHelper = new THREE.SkeletonHelper(target);
|
||||
this.scene.add(this.skeletonHelper);
|
||||
}
|
||||
|
||||
onMouseClick(mouse: THREE.Vector2) {
|
||||
this.raycaster.setFromCamera(mouse, this.transformControls.camera);
|
||||
const intersections = this.raycaster.intersectObjects(this.targetBoneSpheres, false);
|
||||
|
||||
if (intersections.length < 1) {
|
||||
console.log("No intersections");
|
||||
this.resetHighlight();
|
||||
this.removeControls();
|
||||
return;
|
||||
}
|
||||
|
||||
// Attach the controls to the first bone
|
||||
const firstBone = intersections[0];
|
||||
this.highlightSphere(firstBone.object);
|
||||
this.showControls(firstBone.object.parent!);
|
||||
console.log("Showing bone", firstBone.object);
|
||||
}
|
||||
|
||||
private showControls(object?: THREE.Object3D) {
|
||||
if (object) {
|
||||
this.transformControls.attach(object);
|
||||
}
|
||||
|
||||
if (this.isControlInScene) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.scene.add(this.transformControls);
|
||||
this.isControlInScene = true;
|
||||
}
|
||||
|
||||
private highlightSphere(sphere: THREE.Object3D) {
|
||||
this.targetBoneSpheres.forEach((s) => {
|
||||
// @ts-expect-error Material on object3d, but it's fine since this is a mesh
|
||||
s.material.opacity = 0.2
|
||||
});
|
||||
// @ts-expect-error Material on object3d, but it's fine since this is a mesh
|
||||
sphere.material.opacity = 1;
|
||||
}
|
||||
|
||||
private resetHighlight() {
|
||||
this.targetBoneSpheres.forEach((s) => {
|
||||
// @ts-expect-error Material on object3d, but it's fine since this is a mesh
|
||||
s.material.opacity = 1;
|
||||
});
|
||||
}
|
||||
|
||||
private removeControls() {
|
||||
this.transformControls.detach();
|
||||
|
||||
if (this.isControlInScene) {
|
||||
this.scene.remove(this.transformControls);
|
||||
this.isControlInScene = false;
|
||||
}
|
||||
}
|
||||
|
||||
private removeSpheres() {
|
||||
this.targetBoneSpheres.forEach((sphere) => {
|
||||
this.scene.remove(sphere);
|
||||
sphere.parent?.remove(sphere);
|
||||
// @ts-expect-error Geometry on object3d, but it's fine since this is a mesh
|
||||
sphere.geometry.dispose();
|
||||
});
|
||||
this.targetBoneSpheres = [];
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.removeControls();
|
||||
this.removeSpheres();
|
||||
|
||||
if (this.skeletonHelper) {
|
||||
this.scene.remove(this.skeletonHelper!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -655,6 +655,35 @@ class Editor {
|
||||
document.body.appendChild(this.stats.dom);
|
||||
}
|
||||
|
||||
public snapShotOfCurrentFrame(shouldDownload: boolean = true) {
|
||||
const snapshot = this.renderer?.domElement.toDataURL("image/png");
|
||||
if (!snapshot) {
|
||||
console.error("Error: Snapshot could not be created.");
|
||||
return null;
|
||||
}
|
||||
|
||||
const base64Snapshot = snapshot.split(",")[1];
|
||||
|
||||
if (shouldDownload) {
|
||||
const link = document.createElement("a");
|
||||
link.download = "scene-snapshot.png";
|
||||
link.href = snapshot;
|
||||
link.click();
|
||||
}
|
||||
|
||||
const byteString = atob(base64Snapshot);
|
||||
const mimeString = "image/png";
|
||||
const ab = new ArrayBuffer(byteString.length);
|
||||
const ia = new Uint8Array(ab);
|
||||
for (let i = 0; i < byteString.length; i++) {
|
||||
ia[i] = byteString.charCodeAt(i);
|
||||
}
|
||||
const uuid = crypto.randomUUID(); // Generate a new UUID
|
||||
const file = new File([ab], `${uuid}.png`, { type: mimeString });
|
||||
|
||||
return { base64Snapshot, file };
|
||||
}
|
||||
|
||||
public async newScene(sceneTitleInput: string) {
|
||||
this.activeScene.clear();
|
||||
this.audio_engine = new AudioEngine();
|
||||
@@ -931,6 +960,8 @@ class Editor {
|
||||
}
|
||||
|
||||
deleteObject(uuid: string) {
|
||||
this.mouse_controls?.clearFKVisuals();
|
||||
this.mouse_controls?.removeTransformControls(true);
|
||||
this.utils.deleteObject(uuid);
|
||||
if (this.outliner_feature_flag) {
|
||||
const result = this.sceneManager?.render_outliner(
|
||||
@@ -962,26 +993,26 @@ class Editor {
|
||||
this.render_camera_aspect_ratio === CameraAspectRatio.HORIZONTAL_16_9
|
||||
? 1024
|
||||
: this.render_camera_aspect_ratio ===
|
||||
CameraAspectRatio.VERTICAL_9_16
|
||||
meraAspectRatio.VERTICAL_9_16
|
||||
? 576
|
||||
: this.render_camera_aspect_ratio ===
|
||||
CameraAspectRatio.HORIZONTAL_3_2
|
||||
meraAspectRatio.HORIZONTAL_3_2
|
||||
? 900
|
||||
: this.render_camera_aspect_ratio ===
|
||||
CameraAspectRatio.VERTICAL_2_3
|
||||
meraAspectRatio.VERTICAL_2_3
|
||||
? 600
|
||||
: 1000;
|
||||
const height =
|
||||
this.render_camera_aspect_ratio === CameraAspectRatio.HORIZONTAL_16_9
|
||||
? 576
|
||||
: this.render_camera_aspect_ratio ===
|
||||
CameraAspectRatio.VERTICAL_9_16
|
||||
meraAspectRatio.VERTICAL_9_16
|
||||
? 1024
|
||||
: this.render_camera_aspect_ratio ===
|
||||
CameraAspectRatio.HORIZONTAL_3_2
|
||||
meraAspectRatio.HORIZONTAL_3_2
|
||||
? 600
|
||||
: this.render_camera_aspect_ratio ===
|
||||
CameraAspectRatio.VERTICAL_2_3
|
||||
meraAspectRatio.VERTICAL_2_3
|
||||
? 900
|
||||
: 1000;
|
||||
|
||||
|
||||
@@ -14,6 +14,13 @@ import { TransformControls } from "./TransformControls";
|
||||
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";
|
||||
|
||||
export enum KinMode {
|
||||
FK,
|
||||
IK,
|
||||
NONE,
|
||||
}
|
||||
|
||||
export class MouseControls {
|
||||
camera: THREE.PerspectiveCamera | null;
|
||||
@@ -47,8 +54,12 @@ export class MouseControls {
|
||||
private isMovable: Function;
|
||||
enable_stats: Function;
|
||||
|
||||
private kinMode: KinMode = KinMode.NONE;
|
||||
private fkHelper: FKHelper;
|
||||
private isBoneDragged: boolean = false;
|
||||
|
||||
constructor(
|
||||
camera: THREE.PerspectiveCamera | null,
|
||||
camera: THREE.PerspectiveCamera,
|
||||
camera_person_mode: boolean,
|
||||
cameraViewControls: FreeCam,
|
||||
lockControls: PointerLockControls | undefined,
|
||||
@@ -62,7 +73,7 @@ export class MouseControls {
|
||||
mouse: THREE.Vector2 | undefined,
|
||||
timeline_mouse: THREE.Vector2 | undefined,
|
||||
raycaster: THREE.Raycaster | undefined,
|
||||
control: TransformControls | undefined,
|
||||
control: TransformControls,
|
||||
outlinePass: OutlinePass | undefined,
|
||||
scene: THREE.Scene,
|
||||
publishSelect: Function,
|
||||
@@ -101,6 +112,20 @@ export class MouseControls {
|
||||
this.sceneManager = undefined;
|
||||
this.isMovable = isMovable;
|
||||
this.enable_stats = enable_stats;
|
||||
this.fkHelper = new FKHelper({
|
||||
camera: this.camera,
|
||||
domElement: this.control.domElement,
|
||||
scene: this.scene,
|
||||
onDragChange: this.onFKControlsDragging.bind(this),
|
||||
});
|
||||
}
|
||||
|
||||
onFKControlsDragging(dragging: boolean) {
|
||||
this.isBoneDragged = dragging;
|
||||
}
|
||||
|
||||
clearFKVisuals() {
|
||||
this.fkHelper.clear();
|
||||
}
|
||||
|
||||
focus() {
|
||||
@@ -125,9 +150,26 @@ export class MouseControls {
|
||||
this.selected = [];
|
||||
this.publishSelect();
|
||||
}
|
||||
this.hideTransformControls();
|
||||
if (remove_outline) this.outlinePass.selectedObjects = [];
|
||||
}
|
||||
|
||||
hideTransformControls() {
|
||||
if (this.control == undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.control.detach();
|
||||
this.scene.remove(this.control);
|
||||
if (remove_outline) this.outlinePass.selectedObjects = [];
|
||||
}
|
||||
|
||||
reattachTransformControls() {
|
||||
if (this.control == undefined || this.selected == undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.control.attach(this.selected[0]);
|
||||
this.scene.add(this.control);
|
||||
}
|
||||
|
||||
selectObject(currentObject: THREE.Object3D) {
|
||||
@@ -151,7 +193,7 @@ export class MouseControls {
|
||||
}
|
||||
|
||||
onMouseDown(event: any) {
|
||||
if ((event.button === 0 || event.button === 1) && this.isMovable()) {
|
||||
if ((event.button === 0 || event.button === 1) && this.isMovable() && !this.isBoneDragged) {
|
||||
this.isMouseClicked = true;
|
||||
}
|
||||
}
|
||||
@@ -176,6 +218,35 @@ export class MouseControls {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
toggleFKMode() {
|
||||
if (this.kinMode == KinMode.FK) {
|
||||
this.fkHelper.clear();
|
||||
this.kinMode = KinMode.NONE;
|
||||
this.reattachTransformControls();
|
||||
console.log("FK mode off");
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure we have an intersection
|
||||
if (!(this.selected && this.selected.length > 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure FK is supported only on character type objects
|
||||
const firstSelection = this.selected[0];
|
||||
if (!firstSelection.userData.isCharacter) {
|
||||
return;
|
||||
}
|
||||
|
||||
// FK is good to go
|
||||
// Disable main transform controls
|
||||
this.hideTransformControls();
|
||||
this.kinMode = KinMode.FK;
|
||||
this.fkHelper.setTarget(this.selected[0]);
|
||||
console.log("FK mode on");
|
||||
return;
|
||||
}
|
||||
|
||||
async onkeydown(event: KeyboardEvent) {
|
||||
if (hotkeysStatus.value.disabled) {
|
||||
return;
|
||||
@@ -198,7 +269,7 @@ export class MouseControls {
|
||||
// transform
|
||||
this.control?.setMode("translate");
|
||||
return;
|
||||
} else if (event.key === "r") {
|
||||
} else if (event.key === "r" && !event.ctrlKey) {
|
||||
// rotate
|
||||
this.control?.setMode("rotate");
|
||||
return;
|
||||
@@ -206,6 +277,8 @@ export class MouseControls {
|
||||
// scale
|
||||
this.control?.setMode("scale");
|
||||
return;
|
||||
} else if (event.key === "k") {
|
||||
this.toggleFKMode();
|
||||
}
|
||||
|
||||
if ((event.ctrlKey || event.metaKey) && !this.isProcessing) {
|
||||
@@ -298,6 +371,11 @@ export class MouseControls {
|
||||
}
|
||||
this.camera_last_pos.copy(camera_pos);
|
||||
|
||||
if (this.kinMode == KinMode.FK) {
|
||||
this.fkHelper.onMouseClick(this.mouse);
|
||||
return;
|
||||
}
|
||||
|
||||
this.raycaster.setFromCamera(this.mouse, this.camera);
|
||||
const interactable: any[] = [];
|
||||
this.scene.children.forEach((child: THREE.Object3D) => {
|
||||
|
||||
@@ -287,10 +287,6 @@ export class TimeLine {
|
||||
// testDeformBody(lastRig);
|
||||
}
|
||||
|
||||
// Add a skeleton helper to the character
|
||||
const skeletonHelper = new THREE.SkeletonHelper(obj);
|
||||
this.scene.scene.add(skeletonHelper);
|
||||
|
||||
const object_uuid = obj.uuid;
|
||||
|
||||
this.characters[object_uuid] = ClipGroup.CHARACTER; // TODO: Create a class to make the idea of a character.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
|
||||
import { EngineContext } from "~/pages/PageEnigma/contexts/EngineContext";
|
||||
import { useContext } from "react";
|
||||
import { uploadImage } from "~/components/reusable/UploadModalMedia/uploadImage";
|
||||
import {
|
||||
faPlus,
|
||||
faCamera,
|
||||
@@ -18,11 +20,12 @@ import {
|
||||
faSquare,
|
||||
faRectangle,
|
||||
} from "@fortawesome/pro-regular-svg-icons";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { PopoverItem, PopoverMenu } from "~/components/reusable/Popover";
|
||||
import { Button } from "~/components";
|
||||
import { CameraAspectRatio } from "~/pages/PageEnigma/enums";
|
||||
import { cameraAspectRatio } from "~/pages/PageEnigma/signals";
|
||||
import { cameraAspectRatio, disableHotkeyInput, DomLevels, enableHotkeyInput } from "~/pages/PageEnigma/signals";
|
||||
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
|
||||
import Queue from "~/pages/PageEnigma/Queue/Queue";
|
||||
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
|
||||
@@ -31,10 +34,15 @@ interface ReferenceImage {
|
||||
id: string;
|
||||
url: string;
|
||||
file: File;
|
||||
mediaToken: string;
|
||||
}
|
||||
import { EngineApi } from "~/Classes/ApiManager/EngineApi";
|
||||
import { scene } from "~/signals/scene";
|
||||
import { UploaderStates } from "~/enums/UploaderStates";
|
||||
|
||||
export const PromptBox = () => {
|
||||
useSignals();
|
||||
const editorEngine = useContext(EngineContext);
|
||||
const [prompt, setPrompt] = useState("");
|
||||
const [isEnqueueing, setisEnqueueing] = useState(false);
|
||||
const [referenceImages, setReferenceImages] = useState<ReferenceImage[]>([]);
|
||||
@@ -163,15 +171,27 @@ export const PromptBox = () => {
|
||||
Array.from(files).forEach((file) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
setReferenceImages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: Math.random().toString(36).substring(7),
|
||||
url: reader.result as string,
|
||||
file,
|
||||
uploadImage({
|
||||
title: `reference-image-${Math.random().toString(36).substring(2, 15)}`,
|
||||
assetFile: file,
|
||||
progressCallback: (newState) => {
|
||||
console.debug("Upload progress:", newState.data);
|
||||
if (newState.status === UploaderStates.success && newState.data) {
|
||||
const referenceImage: ReferenceImage = {
|
||||
id: Math.random().toString(36).substring(7),
|
||||
url: reader.result as string,
|
||||
file,
|
||||
mediaToken: newState.data || "",
|
||||
};
|
||||
setReferenceImages((prev) => [...prev, referenceImage]);
|
||||
console.log("Reference image added:", referenceImage);
|
||||
} else {
|
||||
console.error("No data from uploadImage");
|
||||
}
|
||||
},
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
@@ -218,18 +238,37 @@ export const PromptBox = () => {
|
||||
const handleEnqueue = async () => {
|
||||
if (!prompt.trim()) return;
|
||||
|
||||
setisEnqueueing(true);
|
||||
try {
|
||||
// Here we would pass both the prompt and reference images to the generation
|
||||
console.log(
|
||||
"Enqueuing with prompt:",
|
||||
prompt,
|
||||
"and reference images:",
|
||||
referenceImages,
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
} finally {
|
||||
setisEnqueueing(false);
|
||||
if (editorEngine) {
|
||||
editorEngine.positive_prompt = prompt;
|
||||
|
||||
const engineApi = new EngineApi();
|
||||
const snapshot = editorEngine.snapShotOfCurrentFrame(false);
|
||||
if (snapshot) {
|
||||
const snapshotResult = await engineApi.uploadSceneSnapshot({
|
||||
screenshot: snapshot.file,
|
||||
sceneMediaToken: "",
|
||||
});
|
||||
|
||||
await engineApi.enqueueImageGeneration({
|
||||
prompt: prompt,
|
||||
snapshotMediaToken: snapshotResult.data || "",
|
||||
additionalImages: referenceImages.map((image) => image.mediaToken),
|
||||
});
|
||||
}
|
||||
|
||||
setisEnqueueing(true);
|
||||
try {
|
||||
// Here we would pass both the prompt and reference images to the generation
|
||||
console.log(
|
||||
"Enqueuing with prompt:",
|
||||
prompt,
|
||||
"and reference images:",
|
||||
referenceImages,
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
} finally {
|
||||
setisEnqueueing(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -258,6 +297,25 @@ export const PromptBox = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveFrame = async () => {
|
||||
if (editorEngine) {
|
||||
const snapshot = editorEngine.snapShotOfCurrentFrame(false);
|
||||
if (snapshot) {
|
||||
const engineApi = new EngineApi();
|
||||
await engineApi.uploadSceneSnapshot({
|
||||
screenshot: snapshot.file || "",
|
||||
sceneMediaToken: "",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadFrame = () => {
|
||||
if (editorEngine) {
|
||||
editorEngine.snapShotOfCurrentFrame(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="absolute bottom-4 left-1/2 flex -translate-x-1/2 flex-col gap-3">
|
||||
{referenceImages.length > 0 && (
|
||||
@@ -325,6 +383,14 @@ export const PromptBox = () => {
|
||||
onChange={handleChange}
|
||||
onPaste={handlePaste}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={() => {
|
||||
console.log("disabling hotkeys")
|
||||
disableHotkeyInput(DomLevels.INPUT);
|
||||
}}
|
||||
onBlur={() => {
|
||||
console.log("enabling hotkeys")
|
||||
enableHotkeyInput(DomLevels.INPUT);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center justify-between gap-2">
|
||||
@@ -360,6 +426,7 @@ export const PromptBox = () => {
|
||||
className="flex items-center border-none bg-[#5F5F68]/60 px-3 text-sm text-white backdrop-blur-lg hover:bg-[#5F5F68]/90"
|
||||
variant="secondary"
|
||||
icon={faDownload}
|
||||
onClick={handleDownloadFrame}
|
||||
>
|
||||
Download frame
|
||||
</Button>
|
||||
@@ -367,6 +434,7 @@ export const PromptBox = () => {
|
||||
className="flex items-center border-none bg-[#5F5F68]/60 px-3 text-sm text-white backdrop-blur-lg hover:bg-[#5F5F68]/90"
|
||||
variant="secondary"
|
||||
icon={faSave}
|
||||
onClick={handleSaveFrame}
|
||||
>
|
||||
Save frame
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user