Compare commits

...
7 Commits
Author SHA1 Message Date
Tanish Manku 942005282a Some bug fixes and improvements for FK 2025-04-04 00:38:31 -07:00
Tanish Manku 14474e68c2 Add basic Forward Kinematics support (#475)
There's still some glitches around selection and deletion.
2025-04-04 00:34:09 -07:00
Michael Chung 2f44da8ac7 Changed api calls to use file upload and uuid idempotency token and stuff works. 2025-04-03 23:10:23 -04:00
Brandon Thomas 3d67f86a19 set sora creds externally 2025-04-03 22:26:38 -04:00
Brandon Thomas 2913d91d0d fix media file downloading 2025-04-03 18:47:57 -04:00
ArEnScandMichael Chung ceb77505e1 Feature/gen (#474)
Co-authored-by: Michael Chung <michaelchung@michaels-mbp-2.home>
2025-04-03 18:01:29 -04:00
ArEnScandMichael Chung 0cad3fe7da Wire screenshot upload and generate to the engine. (#470)
Co-authored-by: Michael Chung <michaelchung@michaels-mbp-2.home>
2025-04-03 17:38:34 -04:00
21 changed files with 919 additions and 143 deletions
@@ -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;
@@ -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()),
}))
}
@@ -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;
@@ -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;
@@ -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>