mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-30 05:22:51 -04:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6294fe01f | ||
|
|
ee1043f92c | ||
|
|
e88b6ce39b | ||
|
|
0e535e52ad | ||
|
|
aa594b773e | ||
|
|
ac209f8f4f | ||
|
|
66abfd18ba | ||
|
|
79cf730270 | ||
|
|
698351f2a3 |
Generated
+6
-1
@@ -5653,8 +5653,10 @@ dependencies = [
|
||||
name = "images"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"errors",
|
||||
"image",
|
||||
"mimetypes",
|
||||
"testing",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
@@ -6868,7 +6870,9 @@ dependencies = [
|
||||
name = "mimetypes"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"infer 0.16.0",
|
||||
"anyhow",
|
||||
"infer 0.19.0",
|
||||
"testing",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
@@ -11191,6 +11195,7 @@ dependencies = [
|
||||
"hostname 0.4.0",
|
||||
"http_server_common",
|
||||
"idempotency",
|
||||
"images",
|
||||
"infer 0.16.0",
|
||||
"language-tags",
|
||||
"lexical-sort",
|
||||
|
||||
@@ -177,7 +177,9 @@ enums = { path = "crates/_types/enums" }
|
||||
enums_public = { path = "crates/_types/enums_public" }
|
||||
errors = { path = "crates/lib/errors" }
|
||||
filesys = { path = "crates/lib/files/filesys" }
|
||||
images = { path = "crates/lib/files/images" }
|
||||
memory_caching = { path = "crates/lib/caching/memory_caching" }
|
||||
mimetypes = { path = "crates/lib/files/mimetypes" }
|
||||
openai_sora_client = { path = "crates/lib/api_clients/openai_sora_client" }
|
||||
primitives = { path = "crates/lib/data/primitives" }
|
||||
shared_service_components = { path = "crates/lib/shared_service_components" }
|
||||
|
||||
@@ -28,11 +28,12 @@ pub enum MediaFileOriginCategory {
|
||||
DeviceApi,
|
||||
|
||||
/// From Storyteller Studio Engine
|
||||
#[deprecated(note="This db field should only denote file provenance, not the product.")]
|
||||
#[serde(rename = "studio")]
|
||||
StorytellerStudio,
|
||||
|
||||
/// From Storyteller Studio Engine
|
||||
#[deprecated(note = "Use StorytellerStudio instead")]
|
||||
#[deprecated(note = "DO NOT USE. Originally deprecated in favor of `StorytellerStudio`.")]
|
||||
StoryEngine,
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,10 @@ pub enum MediaFileOriginProductCategory {
|
||||
#[serde(rename = "vst")]
|
||||
VideoStyleTransfer,
|
||||
|
||||
/// Image Studio
|
||||
#[serde(rename = "image_studio")]
|
||||
ImageStudio,
|
||||
|
||||
/// Storyteller Studio
|
||||
#[serde(rename = "studio")]
|
||||
StorytellerStudio,
|
||||
@@ -87,6 +91,7 @@ impl MediaFileOriginProductCategory {
|
||||
Self::FaceFusion => "face_fusion",
|
||||
Self::FaceMirror => "face_mirror",
|
||||
Self::VideoStyleTransfer => "vst",
|
||||
Self::ImageStudio => "image_studio",
|
||||
Self::StorytellerStudio => "studio",
|
||||
Self::TextToSpeech => "tts",
|
||||
Self::VoiceConversion => "voice_conversion",
|
||||
@@ -105,6 +110,7 @@ impl MediaFileOriginProductCategory {
|
||||
"face_fusion" => Ok(Self::FaceFusion),
|
||||
"face_mirror" => Ok(Self::FaceMirror),
|
||||
"vst" => Ok(Self::VideoStyleTransfer),
|
||||
"image_studio" => Ok(Self::ImageStudio),
|
||||
"studio" => Ok(Self::StorytellerStudio),
|
||||
"tts" => Ok(Self::TextToSpeech),
|
||||
"voice_conversion" => Ok(Self::VoiceConversion),
|
||||
@@ -126,6 +132,7 @@ impl MediaFileOriginProductCategory {
|
||||
Self::FaceFusion,
|
||||
Self::FaceMirror,
|
||||
Self::VideoStyleTransfer,
|
||||
Self::ImageStudio,
|
||||
Self::StorytellerStudio,
|
||||
Self::TextToSpeech,
|
||||
Self::VoiceConversion,
|
||||
@@ -153,6 +160,7 @@ mod tests {
|
||||
assert_serialization(MediaFileOriginProductCategory::FaceFusion, "face_fusion");
|
||||
assert_serialization(MediaFileOriginProductCategory::FaceMirror, "face_mirror");
|
||||
assert_serialization(MediaFileOriginProductCategory::VideoStyleTransfer, "vst");
|
||||
assert_serialization(MediaFileOriginProductCategory::ImageStudio, "image_studio");
|
||||
assert_serialization(MediaFileOriginProductCategory::StorytellerStudio, "studio");
|
||||
assert_serialization(MediaFileOriginProductCategory::TextToSpeech, "tts");
|
||||
assert_serialization(MediaFileOriginProductCategory::VoiceConversion, "voice_conversion");
|
||||
@@ -170,6 +178,7 @@ mod tests {
|
||||
assert_eq!(MediaFileOriginProductCategory::FaceFusion.to_str(), "face_fusion");
|
||||
assert_eq!(MediaFileOriginProductCategory::FaceMirror.to_str(), "face_mirror");
|
||||
assert_eq!(MediaFileOriginProductCategory::VideoStyleTransfer.to_str(), "vst");
|
||||
assert_eq!(MediaFileOriginProductCategory::ImageStudio.to_str(), "image_studio");
|
||||
assert_eq!(MediaFileOriginProductCategory::StorytellerStudio.to_str(), "studio");
|
||||
assert_eq!(MediaFileOriginProductCategory::TextToSpeech.to_str(), "tts");
|
||||
assert_eq!(MediaFileOriginProductCategory::VoiceConversion.to_str(), "voice_conversion");
|
||||
@@ -187,6 +196,7 @@ mod tests {
|
||||
assert_eq!(MediaFileOriginProductCategory::from_str("face_fusion").unwrap(), MediaFileOriginProductCategory::FaceFusion);
|
||||
assert_eq!(MediaFileOriginProductCategory::from_str("face_mirror").unwrap(), MediaFileOriginProductCategory::FaceMirror);
|
||||
assert_eq!(MediaFileOriginProductCategory::from_str("vst").unwrap(), MediaFileOriginProductCategory::VideoStyleTransfer);
|
||||
assert_eq!(MediaFileOriginProductCategory::from_str("image_studio").unwrap(), MediaFileOriginProductCategory::ImageStudio);
|
||||
assert_eq!(MediaFileOriginProductCategory::from_str("studio").unwrap(), MediaFileOriginProductCategory::StorytellerStudio);
|
||||
assert_eq!(MediaFileOriginProductCategory::from_str("tts").unwrap(), MediaFileOriginProductCategory::TextToSpeech);
|
||||
assert_eq!(MediaFileOriginProductCategory::from_str("voice_conversion").unwrap(), MediaFileOriginProductCategory::VoiceConversion);
|
||||
@@ -200,12 +210,13 @@ mod tests {
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
let mut variants = MediaFileOriginProductCategory::all_variants();
|
||||
assert_eq!(variants.len(), 13);
|
||||
assert_eq!(variants.len(), 14);
|
||||
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::Unknown));
|
||||
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::FaceAnimator));
|
||||
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::FaceFusion));
|
||||
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::FaceMirror));
|
||||
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::VideoStyleTransfer));
|
||||
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::ImageStudio));
|
||||
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::StorytellerStudio));
|
||||
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::TextToSpeech));
|
||||
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::VoiceConversion));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "images"
|
||||
edition = "2018"
|
||||
edition = "2024"
|
||||
version = "0.0.1"
|
||||
authors = [
|
||||
"Brandon Thomas <bt@brand.io>",
|
||||
@@ -18,15 +18,16 @@ path = "src/lib.rs"
|
||||
normal = ["workspace-build-acceleration"]
|
||||
|
||||
[dependencies]
|
||||
# Workspace hack (faster compile times)
|
||||
workspace-build-acceleration = { workspace = true }
|
||||
|
||||
# Internal
|
||||
errors = { path = "../../errors" }
|
||||
|
||||
# Workspace hack (faster compile times)
|
||||
workspace-build-acceleration = { version = "0.1", path = "../../../workspace-build-acceleration" }
|
||||
errors = { workspace = true}
|
||||
mimetypes = { workspace = true }
|
||||
|
||||
# External
|
||||
image = "0.25.1"
|
||||
image = "0.25.6"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
testing = { path = "../../testing" }
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
use crate::image_info::image_info_error::ImageInfoError;
|
||||
use image::{GenericImageView, ImageReader};
|
||||
use mimetypes::mimetype_info::file_extension::FileExtension;
|
||||
use mimetypes::mimetype_info::mimetype_info::MimetypeInfo;
|
||||
use std::io::Cursor;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ImageInfo {
|
||||
/// The mimetype we determined
|
||||
mimetype_info: MimetypeInfo,
|
||||
|
||||
/// The width of the image in pixels
|
||||
width: u32,
|
||||
|
||||
/// The height of the image in pixels
|
||||
height: u32,
|
||||
}
|
||||
|
||||
impl ImageInfo {
|
||||
/// Note: This has to do a full file read, and we throw the parsed image on the floor.
|
||||
pub fn decode_image_from_bytes(bytes: &[u8]) -> Result<Self, ImageInfoError> {
|
||||
let image = ImageReader::new(Cursor::new(bytes)).with_guessed_format()?.decode()?;
|
||||
let (width, height) = image.dimensions();
|
||||
Ok(ImageInfo {
|
||||
mimetype_info: MimetypeInfo::get_for_bytes(bytes)
|
||||
.ok_or_else(|| ImageInfoError::CouldNotDetermineMimetype)?,
|
||||
width,
|
||||
height,
|
||||
})
|
||||
}
|
||||
|
||||
/// Note: This has to do a full file read, and we throw the parsed image on the floor.
|
||||
pub fn decode_image_from_path<P: AsRef<Path>>(path: P) -> Result<Self, ImageInfoError> {
|
||||
let image = ImageReader::open(&path)?.decode()?;
|
||||
let (width, height) = image.dimensions();
|
||||
Ok(ImageInfo {
|
||||
mimetype_info: MimetypeInfo::get_for_path(path)?
|
||||
.ok_or_else(|| ImageInfoError::CouldNotDetermineMimetype)?,
|
||||
width,
|
||||
height,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn width(&self) -> u32 {
|
||||
self.width
|
||||
}
|
||||
|
||||
pub fn height(&self) -> u32 {
|
||||
self.height
|
||||
}
|
||||
|
||||
pub fn dimensions(&self) -> (u32, u32) {
|
||||
(self.width, self.height)
|
||||
}
|
||||
|
||||
pub fn mime_type(&self) -> &str {
|
||||
self.mimetype_info.mime_type()
|
||||
}
|
||||
|
||||
pub fn file_extension(&self) -> Option<FileExtension> {
|
||||
self.mimetype_info.file_extension()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::image_info::image_info::ImageInfo;
|
||||
use mimetypes::mimetype_info::file_extension::FileExtension;
|
||||
use testing::test_file_path::test_file_path;
|
||||
|
||||
#[test]
|
||||
fn jpg_file() -> anyhow::Result<()> {
|
||||
let path = test_file_path("test_data/image/mochi.jpg")?;
|
||||
let info = ImageInfo::decode_image_from_path(path)?;
|
||||
assert_eq!(info.width(), 753);
|
||||
assert_eq!(info.height(), 1000);
|
||||
assert_eq!(info.mime_type(), "image/jpeg");
|
||||
let ext = info.file_extension().expect("should have extension");
|
||||
assert_eq!(ext, FileExtension::Jpg);
|
||||
assert_eq!(ext.extension_with_period(), ".jpg");
|
||||
assert_eq!(ext.extension_without_period(), "jpg");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jpg_bytes() -> anyhow::Result<()> {
|
||||
let path = test_file_path("test_data/image/juno.jpg")?;
|
||||
let bytes = std::fs::read(path)?;
|
||||
let info = ImageInfo::decode_image_from_bytes(&bytes)?;
|
||||
assert_eq!(info.width(), 1000);
|
||||
assert_eq!(info.height(), 750);
|
||||
assert_eq!(info.mime_type(), "image/jpeg");
|
||||
let ext = info.file_extension().expect("should have extension");
|
||||
assert_eq!(ext, FileExtension::Jpg);
|
||||
assert_eq!(ext.extension_with_period(), ".jpg");
|
||||
assert_eq!(ext.extension_without_period(), "jpg");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::io;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ImageInfoError {
|
||||
CouldNotDetermineMimetype,
|
||||
ImageError(image::ImageError),
|
||||
IoError(io::Error),
|
||||
}
|
||||
|
||||
impl Error for ImageInfoError {}
|
||||
|
||||
impl Display for ImageInfoError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ImageInfoError::CouldNotDetermineMimetype => {
|
||||
write!(f, "ImageInfoError::CouldNotDetermineMimetype")
|
||||
}
|
||||
ImageInfoError::ImageError(err) => {
|
||||
write!(f, "ImageInfoError::ImageError: {}", err)
|
||||
}
|
||||
ImageInfoError::IoError(err) => {
|
||||
write!(f, "ImageInfoError::IoError: {}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<image::ImageError> for ImageInfoError {
|
||||
fn from(err: image::ImageError) -> Self {
|
||||
ImageInfoError::ImageError(err)
|
||||
}
|
||||
}
|
||||
impl From<io::Error> for ImageInfoError {
|
||||
fn from(err: io::Error) -> Self {
|
||||
ImageInfoError::IoError(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod image_info;
|
||||
pub mod image_info_error;
|
||||
@@ -23,6 +23,7 @@
|
||||
// Re-export image to everyone importing this library
|
||||
pub use image;
|
||||
|
||||
pub mod image_info;
|
||||
pub mod resize_image_file_preserving_aspect;
|
||||
pub mod resize_preserving_aspect;
|
||||
|
||||
|
||||
@@ -16,15 +16,14 @@ path = "src/lib.rs"
|
||||
normal = ["workspace-build-acceleration"]
|
||||
|
||||
[dependencies]
|
||||
# Workspace hack (faster compile times)
|
||||
workspace-build-acceleration = { workspace = true }
|
||||
|
||||
# Internal
|
||||
errors = { path = "../../errors" }
|
||||
|
||||
# Workspace hack (faster compile times)
|
||||
workspace-build-acceleration = { version = "0.1", path = "../../../workspace-build-acceleration" }
|
||||
errors = { workspace = true }
|
||||
|
||||
# External
|
||||
log = "0.4.14"
|
||||
log = { workspace = true }
|
||||
|
||||
# Powerful audio decoder and demuxer
|
||||
#symphonia = { version = "0.5.2", features = ["aac", "alac", "flac", "isomp4", "mkv", "mp3", "ogg", "vorbis", "wav"] }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mimetypes"
|
||||
edition = "2018"
|
||||
edition = "2024"
|
||||
version = "0.0.1"
|
||||
authors = [
|
||||
"Brandon Thomas <bt@brand.io>",
|
||||
@@ -18,11 +18,11 @@ normal = ["workspace-build-acceleration"]
|
||||
|
||||
[dependencies]
|
||||
# Workspace hack (faster compile times)
|
||||
workspace-build-acceleration = { version = "0.1", path = "../../../workspace-build-acceleration" }
|
||||
workspace-build-acceleration = { workspace = true }
|
||||
|
||||
# External
|
||||
infer = "0.16.0" # Mimetype detection
|
||||
|
||||
infer = "0.19.0" # Mimetype detection
|
||||
|
||||
[dev-dependencies]
|
||||
# None yet
|
||||
testing = { path = "../../testing" }
|
||||
anyhow = { workspace = true }
|
||||
|
||||
@@ -21,4 +21,5 @@
|
||||
|
||||
pub mod mimetype_for_bytes;
|
||||
pub mod mimetype_for_file;
|
||||
pub mod mimetype_info;
|
||||
pub mod mimetype_to_extension;
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
|
||||
/// This is a constrained set of file extensions that:
|
||||
/// 1. we care about
|
||||
/// 2. we can detect with magic bytes
|
||||
/// 3. are not text formats / are binary formats
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
|
||||
pub enum FileExtension {
|
||||
Aac,
|
||||
Flac,
|
||||
Gif,
|
||||
Jpg,
|
||||
Mov,
|
||||
Mp3,
|
||||
Mp4,
|
||||
Ogg,
|
||||
Opus,
|
||||
Png,
|
||||
Wav,
|
||||
Webm,
|
||||
Webp,
|
||||
}
|
||||
|
||||
impl FileExtension {
|
||||
pub fn from_mimetype(mimetype: &str) -> Option<Self> {
|
||||
match mimetype {
|
||||
"audio/aac" => Some(FileExtension::Aac),
|
||||
"audio/flac" => Some(FileExtension::Flac),
|
||||
"image/gif" => Some(FileExtension::Gif),
|
||||
"image/jpeg" => Some(FileExtension::Jpg),
|
||||
"video/quicktime" => Some(FileExtension::Mov),
|
||||
"audio/mpeg" => Some(FileExtension::Mp3),
|
||||
"video/mp4" => Some(FileExtension::Mp4),
|
||||
"audio/ogg" => Some(FileExtension::Ogg),
|
||||
"audio/opus" => Some(FileExtension::Opus),
|
||||
"image/png" => Some(FileExtension::Png),
|
||||
"audio/wav" => Some(FileExtension::Wav),
|
||||
"video/webm" => Some(FileExtension::Webm),
|
||||
"image/webp" => Some(FileExtension::Webp),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extension_with_period(&self) -> &'static str {
|
||||
self.with_and_without_period().0
|
||||
}
|
||||
|
||||
pub fn extension_without_period(&self) -> &'static str {
|
||||
self.with_and_without_period().1
|
||||
}
|
||||
|
||||
fn with_and_without_period(&self) -> (&'static str, &'static str) {
|
||||
match self {
|
||||
FileExtension::Aac => (".aac", "aac"),
|
||||
FileExtension::Flac => (".flac", "flac"),
|
||||
FileExtension::Gif => (".gif", "gif"),
|
||||
FileExtension::Jpg => (".jpg", "jpg"),
|
||||
FileExtension::Mov => (".mov", "mov"),
|
||||
FileExtension::Mp3 => (".mp3", "mp3"),
|
||||
FileExtension::Mp4 => (".mp4", "mp4"),
|
||||
FileExtension::Ogg => (".ogg", "ogg"),
|
||||
FileExtension::Opus => (".opus", "opus"),
|
||||
FileExtension::Png => (".png", "png"),
|
||||
FileExtension::Wav => (".wav", "wav"),
|
||||
FileExtension::Webm => (".webm", "webm"),
|
||||
FileExtension::Webp => (".webp", "webp"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
use crate::mimetype_info::file_extension::FileExtension;
|
||||
use infer::Type;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MimetypeInfo {
|
||||
/// The mimetype we determined
|
||||
mime_type: String,
|
||||
|
||||
/// The extension for the mimetype, if we could determine it.
|
||||
file_extension: Option<FileExtension>,
|
||||
}
|
||||
|
||||
impl MimetypeInfo {
|
||||
pub fn get_for_bytes(bytes: &[u8]) -> Option<Self> {
|
||||
infer::get(bytes)
|
||||
.map(|t| Self::for_type(t))
|
||||
}
|
||||
|
||||
pub fn get_for_path<P: AsRef<Path>>(path: P) -> io::Result<Option<Self>> {
|
||||
infer::get_from_path(path)
|
||||
.map(|maybe_type| maybe_type.map(|t| Self::for_type(t)))
|
||||
}
|
||||
|
||||
fn for_type(r#type: Type) -> Self {
|
||||
let mime_type = r#type.mime_type().to_string();
|
||||
MimetypeInfo {
|
||||
file_extension: FileExtension::from_mimetype(&mime_type),
|
||||
mime_type,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mime_type(&self) -> &str {
|
||||
&self.mime_type
|
||||
}
|
||||
|
||||
pub fn file_extension(&self) -> Option<FileExtension> {
|
||||
self.file_extension
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::mimetype_info::file_extension::FileExtension;
|
||||
use crate::mimetype_info::mimetype_info::MimetypeInfo;
|
||||
|
||||
mod for_bytes {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn png() {
|
||||
// NB: From "Hanashi Mask.png"
|
||||
let bytes: Vec<u8> = vec![137, 80, 78, 71, 13, 10, 26, 10];
|
||||
let t = MimetypeInfo::get_for_bytes(&bytes).expect("should detect bytes");
|
||||
assert_eq!(t.mime_type(), "image/png");
|
||||
let ext = t.file_extension.expect("should have extension");
|
||||
assert_eq!(ext, FileExtension::Png);
|
||||
assert_eq!(ext.extension_with_period(), ".png");
|
||||
assert_eq!(ext.extension_without_period(), "png");
|
||||
}
|
||||
}
|
||||
|
||||
mod for_files {
|
||||
use testing::test_file_path::test_file_path;
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn mp3() -> anyhow::Result<()> {
|
||||
let path = test_file_path("test_data/audio/mp3/super_mario_rpg_beware_the_forests_mushrooms.mp3")?;
|
||||
let t = MimetypeInfo::get_for_path(path)?.expect("should detect type");
|
||||
assert_eq!(t.mime_type(), "audio/mpeg");
|
||||
let ext = t.file_extension.expect("should have extension");
|
||||
assert_eq!(ext, FileExtension::Mp3);
|
||||
assert_eq!(ext.extension_with_period(), ".mp3");
|
||||
assert_eq!(ext.extension_without_period(), "mp3");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod file_extension;
|
||||
pub mod mimetype_info;
|
||||
@@ -18,14 +18,14 @@ normal = ["workspace-build-acceleration"]
|
||||
|
||||
[dependencies]
|
||||
# Workspace hack (faster compile times)
|
||||
workspace-build-acceleration = { version = "0.1", path = "../../../workspace-build-acceleration" }
|
||||
workspace-build-acceleration = { workspace = true }
|
||||
|
||||
# Internal
|
||||
errors = { path = "../../errors" }
|
||||
errors = { workspace = true }
|
||||
|
||||
# External
|
||||
ffprobe = "0.4.0"
|
||||
log = "0.4.20"
|
||||
log = { workspace = true }
|
||||
# TODO(bt,2024-08-08): We might drop `mp4` crate because it crashes on reading
|
||||
# some videos with the message "avcc not found"
|
||||
mp4 = "0.14.0"
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use enums::by_table::generic_inference_jobs::inference_result_type::InferenceResultType;
|
||||
use errors::AnyhowResult;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use crate::queries::generic_inference::job::list_available_generic_inference_jobs::AvailableInferenceJob;
|
||||
|
||||
pub async fn mark_generic_inference_job_successfully_done_by_token(
|
||||
pool: &MySqlPool,
|
||||
token: &InferenceJobToken,
|
||||
maybe_entity_type: Option<InferenceResultType>,
|
||||
maybe_entity_token: Option<&str>,
|
||||
total_job_duration: Option<Duration>,
|
||||
inference_duration: Option<Duration>,
|
||||
) -> AnyhowResult<()>
|
||||
{
|
||||
// NB: MySql's unsigned int (32 bits) can store integers up to 4,294,967,295.
|
||||
// Given milliseconds, this is ~49.71 days, which should be plenty for us.
|
||||
let truncated_total_job_execution_millis = total_job_duration
|
||||
.map(|duration| duration.as_millis() as u32)
|
||||
.unwrap_or(0);
|
||||
let truncated_inference_execution_millis = inference_duration
|
||||
.map(|duration| duration.as_millis() as u32)
|
||||
.unwrap_or(0);
|
||||
|
||||
let query_result = sqlx::query!(
|
||||
r#"
|
||||
UPDATE generic_inference_jobs
|
||||
SET
|
||||
status = "complete_success",
|
||||
on_success_result_entity_type = ?,
|
||||
on_success_result_entity_token = ?,
|
||||
failure_reason = NULL,
|
||||
internal_debugging_failure_reason = NULL,
|
||||
success_execution_millis = ?,
|
||||
success_inference_execution_millis = ?,
|
||||
retry_at = NULL,
|
||||
successfully_completed_at = NOW()
|
||||
WHERE token = ?
|
||||
"#,
|
||||
maybe_entity_type,
|
||||
maybe_entity_token,
|
||||
truncated_total_job_execution_millis,
|
||||
truncated_inference_execution_millis,
|
||||
token.as_str()
|
||||
)
|
||||
.execute(pool)
|
||||
.await;
|
||||
|
||||
match query_result {
|
||||
Err(err) => Err(anyhow!("error with query: {:?}", err)),
|
||||
Ok(_r) => Ok(()),
|
||||
}
|
||||
}
|
||||
@@ -7,4 +7,5 @@ pub mod job_status;
|
||||
pub mod kill_generic_inference_jobs;
|
||||
pub mod kill_jobs_in_development;
|
||||
pub mod list_session_jobs;
|
||||
pub mod mark_generic_inference_job_cancelled_by_user;
|
||||
pub mod mark_generic_inference_job_cancelled_by_user;
|
||||
pub mod mark_generic_inference_job_successfully_done_by_token;
|
||||
+269
@@ -0,0 +1,269 @@
|
||||
use crate::queries::media_files::create::generic_insert::insert_media_file_generic::{insert_media_file_generic, InsertArgs};
|
||||
use anyhow::anyhow;
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use enums::by_table::generic_synthetic_ids::id_category::IdCategory;
|
||||
use enums::by_table::media_files::media_file_class::MediaFileClass;
|
||||
use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory;
|
||||
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
|
||||
use enums::by_table::media_files::media_file_type::MediaFileType;
|
||||
use enums::common::visibility::Visibility;
|
||||
use errors::AnyhowResult;
|
||||
use sqlx::MySqlPool;
|
||||
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
use crate::queries::media_files::create::insert_builder::media_file_insert_builder_error::MediaFileInsertBuilderError;
|
||||
|
||||
/// Builder for inserting a media files into the database.
|
||||
/// Resembles an ORM, but can still use statically type checked queries (which is probably
|
||||
/// really important since we don't have tests on database functionality yet).
|
||||
#[derive(Clone)]
|
||||
pub struct MediaFileInsertBuilder {
|
||||
// TODO: Allow specifying the token externally
|
||||
// token: MediaFileToken,
|
||||
|
||||
// Creator info
|
||||
maybe_creator_user_token: Option<UserToken>,
|
||||
maybe_creator_anonymous_visitor_token: Option<AnonymousVisitorTrackingToken>,
|
||||
creator_ip_address: Option<String>, // NB: Non-nullable field
|
||||
creator_set_visibility: Visibility, // NB: Non-nullable field
|
||||
|
||||
// Important database indices
|
||||
media_file_class: MediaFileClass, // NB: Non-nullable field
|
||||
media_file_type: Option<MediaFileType>, // NB: Non-nullable field
|
||||
is_user_upload: bool,
|
||||
is_intermediate_system_file: bool,
|
||||
|
||||
// Product and other origination information
|
||||
origin_category: Option<MediaFileOriginCategory>, // NB: Non-nullable field
|
||||
origin_product_category: MediaFileOriginProductCategory, // NB: Non-nullable field
|
||||
// maybe_origin_model_type: Option<MediaFileOriginModelType>,
|
||||
// maybe_origin_model_token: Option<&'a ModelWeightToken>,
|
||||
// maybe_origin_filename: Option<String>,
|
||||
|
||||
// Media info
|
||||
maybe_mime_type: Option<String>,
|
||||
file_size_bytes: u64, // NB: Non-nullable field
|
||||
// maybe_duration_millis: Option<u64>,
|
||||
// maybe_audio_encoding: Option<&'a str>,
|
||||
// maybe_video_encoding: Option<&'a str>,
|
||||
maybe_frame_width: Option<u32>,
|
||||
maybe_frame_height: Option<u32>,
|
||||
checksum_sha2: Option<String>, // NB: Non-nullable field
|
||||
|
||||
// // User text information
|
||||
// maybe_title: Option<&'a str>,
|
||||
// maybe_text_transcript: Option<&'a str>,
|
||||
|
||||
// // If generated from a scene, this is the scene media file token.
|
||||
// maybe_scene_source_media_file_token: Option<&'a MediaFileToken>,
|
||||
|
||||
// // If additional prompt details are stored, this is the prompt token.
|
||||
// maybe_prompt_token: Option<&'a PromptToken>,
|
||||
|
||||
// // If batch generated, this is the batch token.
|
||||
// maybe_batch_token: Option<&'a BatchGenerationToken>,
|
||||
|
||||
// Storage details
|
||||
public_bucket_directory_hash: Option<MediaFileBucketPath>, // NB: Non-nullable field(s)
|
||||
|
||||
// Remaining fields...
|
||||
}
|
||||
|
||||
impl MediaFileInsertBuilder {
|
||||
//pub fn generate_token() -> Self {
|
||||
// Self::with_token(&MediaFileToken::generate())
|
||||
//}
|
||||
|
||||
pub fn new() -> Self {
|
||||
MediaFileInsertBuilder {
|
||||
//token: token.clone(),
|
||||
maybe_creator_user_token: None,
|
||||
maybe_creator_anonymous_visitor_token: None,
|
||||
creator_ip_address: None,
|
||||
creator_set_visibility: Visibility::Public,
|
||||
media_file_class: MediaFileClass::Unknown,
|
||||
media_file_type: None,
|
||||
is_user_upload: false,
|
||||
is_intermediate_system_file: false,
|
||||
origin_category: None,
|
||||
origin_product_category: MediaFileOriginProductCategory::Unknown,
|
||||
maybe_mime_type: None,
|
||||
file_size_bytes: 0,
|
||||
maybe_frame_width: None,
|
||||
maybe_frame_height: None,
|
||||
checksum_sha2: None,
|
||||
public_bucket_directory_hash: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn creator_user(mut self, user_token: &UserToken) -> Self {
|
||||
self.maybe_creator_user_token = Some(user_token.clone());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn maybe_creator_user(mut self, maybe_user: Option<&UserToken>) -> Self {
|
||||
self.maybe_creator_user_token = maybe_user.map(|token| token.clone());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn creator_anonymous_visitor(mut self, avt: &AnonymousVisitorTrackingToken) -> Self {
|
||||
self.maybe_creator_anonymous_visitor_token = Some(avt.clone());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn maybe_creator_anonymous_visitor(mut self, maybe_avt: Option<&AnonymousVisitorTrackingToken>) -> Self {
|
||||
self.maybe_creator_anonymous_visitor_token = maybe_avt.map(|token| token.clone());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn creator_ip_address(mut self, ip_address: &str) -> Self {
|
||||
self.creator_ip_address = Some(ip_address.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn creator_set_visibility(mut self, visibility: Visibility) -> Self {
|
||||
self.creator_set_visibility = visibility;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn media_file_class(mut self, media_file_class: MediaFileClass) -> Self {
|
||||
self.media_file_class = media_file_class;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn media_file_type(mut self, media_file_type: MediaFileType) -> Self {
|
||||
self.media_file_type = Some(media_file_type);
|
||||
self
|
||||
}
|
||||
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
pub fn is_user_upload(mut self, is_user_upload: bool) -> Self {
|
||||
self.is_user_upload = is_user_upload;
|
||||
self
|
||||
}
|
||||
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
pub fn is_intermediate_system_file(mut self, is_intermediate_system_file: bool) -> Self {
|
||||
self.is_intermediate_system_file = is_intermediate_system_file;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn media_file_origin_category(mut self, origin_category: MediaFileOriginCategory) -> Self {
|
||||
self.origin_category = Some(origin_category);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn media_file_origin_product_category(mut self, origin_category: MediaFileOriginProductCategory) -> Self {
|
||||
self.origin_product_category = origin_category;
|
||||
self
|
||||
}
|
||||
|
||||
// TODO: Method for maybe_origin_model_type
|
||||
// TODO: Method for maybe_origin_model_token
|
||||
// TODO: Method for maybe_origin_filename
|
||||
|
||||
pub fn mime_type(mut self, mime_type: &str) -> Self {
|
||||
self.maybe_mime_type = Some(mime_type.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn file_size_bytes(mut self, file_size_bytes: u64) -> Self {
|
||||
self.file_size_bytes = file_size_bytes;
|
||||
self
|
||||
}
|
||||
|
||||
// TODO: maybe_duration_millis
|
||||
// TODO: maybe_audio_encoding
|
||||
// TODO: maybe_video_encoding
|
||||
|
||||
pub fn frame_width(mut self, frame_width: u32) -> Self {
|
||||
self.maybe_frame_width = Some(frame_width);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn frame_height(mut self, frame_height: u32) -> Self {
|
||||
self.maybe_frame_height = Some(frame_height);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn checksum_sha2(mut self, checksum_sha2: &str) -> Self {
|
||||
self.checksum_sha2 = Some(checksum_sha2.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
// TODO: maybe_title
|
||||
// TODO: maybe_text_transcript
|
||||
// TODO: maybe_scene_source_media_file_token
|
||||
// TODO: maybe_prompt_token
|
||||
// TODO: maybe_batch_token
|
||||
|
||||
pub fn public_bucket_directory_hash(mut self, public_bucket_directory_hash: &MediaFileBucketPath) -> Self {
|
||||
self.public_bucket_directory_hash = Some(public_bucket_directory_hash.clone());
|
||||
self
|
||||
}
|
||||
|
||||
// TODO(bt,2025-04-26): Other connector options.
|
||||
pub async fn insert_pool(self, mysql_pool: &MySqlPool) -> Result<MediaFileToken, MediaFileInsertBuilderError> {
|
||||
let media_file_type = self.media_file_type
|
||||
.ok_or_else(|| MediaFileInsertBuilderError::MissingRequiredField(
|
||||
"Media file type is required".to_string()))?;
|
||||
|
||||
let origin_category = self.origin_category
|
||||
.ok_or_else(|| MediaFileInsertBuilderError::MissingRequiredField(
|
||||
"Origin category is required".to_string()))?;
|
||||
|
||||
let checksum_sha2 = self.checksum_sha2
|
||||
.ok_or_else(|| MediaFileInsertBuilderError::MissingRequiredField(
|
||||
"Checksum SHA2 is required".to_string()))?;
|
||||
|
||||
let bucket_path = self.public_bucket_directory_hash
|
||||
.ok_or_else(|| MediaFileInsertBuilderError::MissingRequiredField(
|
||||
"Public bucket directory hash is required".to_string()))?;
|
||||
|
||||
let result = insert_media_file_generic(InsertArgs {
|
||||
pool: mysql_pool,
|
||||
maybe_creator_user_token: self.maybe_creator_user_token.as_ref(),
|
||||
maybe_creator_anonymous_visitor_token: self.maybe_creator_anonymous_visitor_token.as_ref(),
|
||||
creator_ip_address: self.creator_ip_address.as_deref().unwrap_or("127.0.0.1"),
|
||||
creator_set_visibility: self.creator_set_visibility,
|
||||
media_class: self.media_file_class,
|
||||
media_type: media_file_type,
|
||||
is_user_upload: self.is_user_upload,
|
||||
is_intermediate_system_file: self.is_intermediate_system_file,
|
||||
origin_category,
|
||||
origin_product_category: self.origin_product_category,
|
||||
maybe_origin_model_type: None, // TODO
|
||||
maybe_origin_model_token: None, // TODO
|
||||
maybe_origin_filename: None, // TODO
|
||||
maybe_mime_type: self.maybe_mime_type.as_deref(),
|
||||
file_size_bytes: self.file_size_bytes,
|
||||
maybe_duration_millis: None, // TODO
|
||||
maybe_audio_encoding: None, // TODO
|
||||
maybe_video_encoding: None, // TODO
|
||||
maybe_frame_width: self.maybe_frame_width,
|
||||
maybe_frame_height: self.maybe_frame_height,
|
||||
checksum_sha2: &checksum_sha2,
|
||||
maybe_title: None, // TODO
|
||||
maybe_text_transcript: None, // TODO
|
||||
maybe_scene_source_media_file_token: None, // TODO
|
||||
maybe_prompt_token: None, // TODO
|
||||
maybe_batch_token: None, // TODO
|
||||
public_bucket_directory_hash: bucket_path.get_object_hash(),
|
||||
maybe_public_bucket_prefix: bucket_path.get_optional_prefix(),
|
||||
maybe_public_bucket_extension: bucket_path.get_optional_extension(),
|
||||
maybe_creator_file_synthetic_id_category: IdCategory::MediaFile, // TODO: Remove this
|
||||
maybe_creator_category_synthetic_id_category: IdCategory::FileUpload, // TODO: Remove this
|
||||
maybe_extra_media_info: None, // TODO
|
||||
is_generated_on_prem: false, // TODO
|
||||
generated_by_worker: None, // TODO
|
||||
generated_by_cluster: None, // TODO
|
||||
maybe_mod_user_token: None, // TODO
|
||||
}).await;
|
||||
|
||||
match result {
|
||||
Ok(result) => Ok(result.0),
|
||||
Err(err) => Err(MediaFileInsertBuilderError::ProbablyDatabaseError(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Debug, Display, Formatter};
|
||||
use errors::AnyhowError;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MediaFileInsertBuilderError {
|
||||
/// A user-provided field is missing (400 error).
|
||||
MissingRequiredField(String),
|
||||
|
||||
// TODO: We need to refactor to ensure these are actually DB errors (500s).
|
||||
ProbablyDatabaseError(AnyhowError),
|
||||
}
|
||||
|
||||
impl Error for MediaFileInsertBuilderError {}
|
||||
|
||||
impl Display for MediaFileInsertBuilderError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
MediaFileInsertBuilderError::MissingRequiredField(reason) => {
|
||||
write!(f, "Missing required field: {}", reason)
|
||||
}
|
||||
MediaFileInsertBuilderError::ProbablyDatabaseError(reason) => {
|
||||
write!(f, "Probably database error: {:?}", reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod media_file_insert_builder;
|
||||
mod media_file_insert_builder_error;
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod generic_insert;
|
||||
pub mod insert_builder;
|
||||
pub mod insert_media_file_from_face_fusion;
|
||||
pub mod insert_media_file_from_gptsovits;
|
||||
pub mod insert_media_file_from_live_portrait;
|
||||
|
||||
@@ -51,11 +51,12 @@ google_sign_in = { path = "../../../lib/api_clients/google_sign_in" }
|
||||
hashing = { path = "../../../lib/files/hashing" }
|
||||
http_server_common = { path = "../../../lib/deprecated/http_server_common" }
|
||||
idempotency = { path = "../../../lib/idempotency" }
|
||||
images = { workspace = true }
|
||||
markdown = { path = "../../../lib/markdown" }
|
||||
media = { path = "../../../lib/files/media" }
|
||||
memory_caching = { path = "../../../lib/caching/memory_caching" }
|
||||
migration = { path = "../../../lib/migration" }
|
||||
mimetypes = { path = "../../../lib/files/mimetypes" }
|
||||
mimetypes = { workspace = true }
|
||||
mysql_queries = { path = "../../../lib/mysql_queries" }
|
||||
openai_sora_client = { path = "../../../lib/api_clients/openai_sora_client" }
|
||||
password = { path = "../../../lib/password" }
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod upload;
|
||||
pub mod prompt;
|
||||
pub mod get_sora_credentials_from_request;
|
||||
pub mod get_sora_credentials_from_request;
|
||||
pub mod update_gpt_image_job_status_handler;
|
||||
+281
@@ -0,0 +1,281 @@
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::http_server::endpoints::engine::create_scene_handler::CreateSceneError;
|
||||
use crate::http_server::endpoints::inference_job::get::get_inference_job_status_handler::GetInferenceJobStatusError;
|
||||
use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError;
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
|
||||
use crate::state::server_state::ServerState;
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::{Data, Json, Path};
|
||||
use actix_web::{web, HttpMessage, HttpRequest, HttpResponse};
|
||||
use base64::alphabet::STANDARD;
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
use base64::Engine;
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use cloud_storage::bucket_client::BucketClient;
|
||||
use enums::by_table::media_files::media_file_class::MediaFileClass;
|
||||
use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory;
|
||||
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
|
||||
use enums::by_table::media_files::media_file_type::MediaFileType;
|
||||
use enums::common::job_status::JobStatus;
|
||||
use enums::common::job_status_plus::JobStatusPlus;
|
||||
use errors::AnyhowResult;
|
||||
use hashing::sha256::sha256_hash_bytes::sha256_hash_bytes;
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use images::image_info::image_info::ImageInfo;
|
||||
use log::{error, info, warn};
|
||||
use mysql_queries::queries::generic_inference::job::mark_generic_inference_job_completely_failed::mark_generic_inference_job_completely_failed;
|
||||
use mysql_queries::queries::generic_inference::web::dismiss_finished_jobs_for_user::dismiss_finished_jobs_for_user;
|
||||
use mysql_queries::queries::generic_inference::web::get_inference_job_status::{get_inference_job_status, get_inference_job_status_from_connection};
|
||||
use mysql_queries::queries::generic_inference::web::job_status::GenericInferenceJobStatus;
|
||||
use mysql_queries::queries::generic_inference::web::mark_generic_inference_job_cancelled_by_user::mark_generic_inference_job_cancelled_by_user;
|
||||
use mysql_queries::queries::media_files::create::generic_insert::insert_media_file_generic::{insert_media_file_generic, InsertArgs};
|
||||
use mysql_queries::queries::media_files::create::insert_builder::media_file_insert_builder::MediaFileInsertBuilder;
|
||||
use mysql_queries::queries::media_files::create::specialized_insert::insert_media_file_from_file_upload::{insert_media_file_from_file_upload, InsertMediaFileFromUploadArgs, UploadType};
|
||||
use mysql_queries::queries::tts::tts_inference_jobs::mark_tts_inference_job_permanently_dead::mark_tts_inference_job_permanently_dead;
|
||||
use sqlx::{MySqlConnection, MySqlPool};
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use utoipa::ToSchema;
|
||||
use mysql_queries::queries::generic_inference::web::mark_generic_inference_job_successfully_done_by_token::mark_generic_inference_job_successfully_done_by_token;
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct UpdateGptImageJobStatusRequest {
|
||||
/// The token of the job we're updating.
|
||||
pub job_token: InferenceJobToken,
|
||||
|
||||
/// How to mark the job.
|
||||
/// A subset of the job states that are relevant to image completion status.
|
||||
pub job_status: UpdatedJobStatus,
|
||||
|
||||
/// Base64-encoded image data.
|
||||
/// Only present on success.
|
||||
/// In the success case, there may be one or more images.
|
||||
pub images: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum UpdatedJobStatus {
|
||||
Started,
|
||||
CompleteSuccess,
|
||||
CompleteFailure,
|
||||
AttemptFailed,
|
||||
Dead,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct UpdateGptImageJobStatusSuccessResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, ToSchema)]
|
||||
pub enum UpdateGptImageJobStatusError {
|
||||
ServerError,
|
||||
NotFound,
|
||||
NotAuthorized,
|
||||
}
|
||||
|
||||
impl ResponseError for UpdateGptImageJobStatusError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
UpdateGptImageJobStatusError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
UpdateGptImageJobStatusError::NotFound => StatusCode::NOT_FOUND,
|
||||
UpdateGptImageJobStatusError::NotAuthorized => StatusCode::UNAUTHORIZED,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let error_reason = match self {
|
||||
Self::ServerError => "server error".to_string(),
|
||||
Self::NotFound => "not found".to_string(),
|
||||
Self::NotAuthorized => "unauthorized".to_string(),
|
||||
};
|
||||
|
||||
to_simple_json_error(&error_reason, self.status_code())
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for UpdateGptImageJobStatusError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
/// [INTERNAL ENDPOINT] Update a GPT Image generation job status.
|
||||
/// This is called by our internal infra, not users. Keep it guarded.
|
||||
/// We can do secrets-based auth later.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
tag = "Jobs",
|
||||
path = "/v1/image_studio/update_job_status",
|
||||
responses(
|
||||
(status = 200, body = UpdateGptImageJobStatusSuccessResponse),
|
||||
(status = 500, body = UpdateGptImageJobStatusError),
|
||||
),
|
||||
params(
|
||||
("request" = UpdateGptImageJobStatusRequest, description = "Request"),
|
||||
)
|
||||
)]
|
||||
pub async fn update_gpt_image_job_status_handler(
|
||||
http_request: HttpRequest,
|
||||
request: Json<UpdateGptImageJobStatusRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>) -> Result<Json<UpdateGptImageJobStatusSuccessResponse>, UpdateGptImageJobStatusError>
|
||||
{
|
||||
//// TODO(bt,2024-06-16): Reuse connection
|
||||
//let mut mysql_connection = server_state.mysql_pool.acquire()
|
||||
// .await
|
||||
// .map_err(|e| {
|
||||
// warn!("Could not acquire DB pool: {:?}", e);
|
||||
// UpdateGptImageJobStatusError::ServerError
|
||||
// })?;
|
||||
|
||||
let inference_job = get_inference_job_status(
|
||||
&request.job_token,
|
||||
&server_state.mysql_pool
|
||||
).await;
|
||||
|
||||
let inference_job = match inference_job {
|
||||
Ok(Some(record)) => record,
|
||||
Ok(None) => return Err(UpdateGptImageJobStatusError::NotFound),
|
||||
Err(err) => {
|
||||
error!("tts job query error: {:?}", err);
|
||||
return Err(UpdateGptImageJobStatusError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
match inference_job.status {
|
||||
JobStatusPlus::CompleteSuccess => {
|
||||
// Job has already been completed. Don't replay the request.
|
||||
return Ok(Json(UpdateGptImageJobStatusSuccessResponse { success: true }))
|
||||
},
|
||||
// TODO: Handle other states as terminal states?
|
||||
//JobStatusPlus::CompleteFailure => return Err(UpdateGptImageJobStatusError::ServerError),
|
||||
//JobStatusPlus::AttemptFailed => {}
|
||||
//JobStatusPlus::Dead => {}
|
||||
//JobStatusPlus::CancelledByUser => return Err(UpdateGptImageJobStatusError::ServerError),
|
||||
//JobStatusPlus::CancelledBySystem => {}
|
||||
_ => {} // Intentional fall through.
|
||||
}
|
||||
|
||||
if let Some(images) = &request.images {
|
||||
for image in images.iter() {
|
||||
let result = upload_and_save_image(
|
||||
image,
|
||||
&inference_job,
|
||||
&server_state.mysql_pool,
|
||||
&server_state.public_bucket_client,
|
||||
).await;
|
||||
let media_token = match result {
|
||||
Ok(media_token) => media_token,
|
||||
Err(err) => {
|
||||
error!("Error uploading image: {:?}", err);
|
||||
return Err(UpdateGptImageJobStatusError::ServerError);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
update_job_record(&request, &server_state, inference_job)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error updating job: {:?}", err);
|
||||
UpdateGptImageJobStatusError::ServerError
|
||||
})?;
|
||||
|
||||
Ok(Json(UpdateGptImageJobStatusSuccessResponse {
|
||||
success: true,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn upload_and_save_image(
|
||||
base64_image: &str,
|
||||
inference_job: &GenericInferenceJobStatus,
|
||||
mysql_pool: &MySqlPool,
|
||||
public_bucket_client: &BucketClient,
|
||||
) -> AnyhowResult<MediaFileToken> {
|
||||
|
||||
let image_bytes = BASE64_STANDARD.decode(base64_image)?;
|
||||
|
||||
// Read file metadata
|
||||
let file_size_bytes = image_bytes.len();
|
||||
let file_hash = sha256_hash_bytes(&image_bytes)?;
|
||||
let image_info = ImageInfo::decode_image_from_bytes(&image_bytes)?;
|
||||
let mimetype = image_info.mime_type();
|
||||
|
||||
// Upload file
|
||||
const PREFIX : Option<&str> = Some("image_");
|
||||
|
||||
let extension_with_period = image_info.file_extension()
|
||||
.map(|ext| ext.extension_with_period())
|
||||
.unwrap_or_else(|| ".png");
|
||||
|
||||
let media_file_type = match mimetype {
|
||||
"image/png" => MediaFileType::Png,
|
||||
"image/jpeg" => MediaFileType::Jpg,
|
||||
_ => MediaFileType::Png,
|
||||
};
|
||||
|
||||
let bucket_upload_path = MediaFileBucketPath::generate_new(
|
||||
PREFIX, Some(extension_with_period));
|
||||
|
||||
info!("Uploading media to bucket path: {}", bucket_upload_path.get_full_object_path_str());
|
||||
|
||||
public_bucket_client.upload_file_with_content_type(
|
||||
bucket_upload_path.get_full_object_path_str(),
|
||||
image_bytes.as_ref(),
|
||||
mimetype
|
||||
).await?;
|
||||
|
||||
let media_token = MediaFileInsertBuilder::new()
|
||||
.maybe_creator_user(inference_job.user_details.maybe_creator_user_token.as_ref())
|
||||
.maybe_creator_anonymous_visitor(inference_job.user_details.maybe_creator_anonymous_visitor_token.as_ref())
|
||||
.creator_ip_address(&inference_job.user_details.creator_ip_address)
|
||||
.public_bucket_directory_hash(&bucket_upload_path)
|
||||
.media_file_class(MediaFileClass::Image)
|
||||
.media_file_type(media_file_type)
|
||||
.media_file_origin_category(MediaFileOriginCategory::Inference)
|
||||
.media_file_origin_product_category(MediaFileOriginProductCategory::ImageStudio)
|
||||
.mime_type(mimetype)
|
||||
.file_size_bytes(file_size_bytes as u64)
|
||||
.frame_width(image_info.width())
|
||||
.frame_height(image_info.height())
|
||||
.checksum_sha2(&file_hash)
|
||||
.insert_pool(mysql_pool)
|
||||
.await?;
|
||||
|
||||
Ok(media_token)
|
||||
}
|
||||
|
||||
async fn update_job_record(
|
||||
request: &Json<UpdateGptImageJobStatusRequest>,
|
||||
server_state: &Data<Arc<ServerState>>,
|
||||
inference_job: GenericInferenceJobStatus
|
||||
) -> AnyhowResult<()> {
|
||||
|
||||
match inference_job.status {
|
||||
JobStatusPlus::Pending => {}
|
||||
JobStatusPlus::Started => {}
|
||||
JobStatusPlus::CompleteSuccess => {
|
||||
mark_generic_inference_job_successfully_done_by_token(
|
||||
&server_state.mysql_pool,
|
||||
&request.job_token,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
).await?;
|
||||
}
|
||||
// TODO: Handle terminal states
|
||||
JobStatusPlus::CompleteFailure => {}
|
||||
JobStatusPlus::AttemptFailed => {}
|
||||
JobStatusPlus::Dead => {}
|
||||
JobStatusPlus::CancelledByUser => {}
|
||||
JobStatusPlus::CancelledBySystem => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::http_server::endpoints::image_studio::prompt::enqueue_studio_image_generation_handler::enqueue_studio_image_generation_handler;
|
||||
use crate::http_server::endpoints::image_studio::update_gpt_image_job_status_handler::update_gpt_image_job_status_handler;
|
||||
use crate::http_server::endpoints::image_studio::upload::upload_snapshot_media_file_handler::upload_snapshot_media_file_handler;
|
||||
use actix_http::body::MessageBody;
|
||||
use actix_service::ServiceFactory;
|
||||
@@ -25,5 +26,9 @@ where
|
||||
.route(web::post().to(enqueue_studio_image_generation_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
.service(web::resource("/update_job_status")
|
||||
.route(web::post().to(update_gpt_image_job_status_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ export class UsersApi extends ApiManager {
|
||||
password: string;
|
||||
}): Promise<ApiResponse<{ signedSession?: string }>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/login`;
|
||||
|
||||
const body = {
|
||||
username_or_email: usernameOrEmail,
|
||||
password: password,
|
||||
|
||||
@@ -5,10 +5,11 @@ import { faKey, faUser } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { Button, Input, LoadingSpinner } from "~/components/ui";
|
||||
import { authentication } from "~/signals";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { UsersApi } from "@storyteller/api";
|
||||
|
||||
export const Login = () => {
|
||||
const {
|
||||
signals: { status: authStatus },
|
||||
signals: { status: authStatus, userInfo },
|
||||
fetchers: { login },
|
||||
enums: { AUTH_STATUS },
|
||||
} = authentication;
|
||||
@@ -36,17 +37,33 @@ export const Login = () => {
|
||||
return "Getting Session...";
|
||||
}
|
||||
|
||||
const handleOnSumbit = (ev: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleOnSumbit = async (ev: React.FormEvent<HTMLFormElement>) => {
|
||||
ev.preventDefault();
|
||||
if (formRef.current) {
|
||||
const form = new FormData(formRef.current);
|
||||
|
||||
const usernameOrEmail = form.get("usernameOrEmail")?.toString();
|
||||
const password = form.get("password")?.toString();
|
||||
if (usernameOrEmail && password && login) {
|
||||
login({
|
||||
|
||||
if (usernameOrEmail && password) {
|
||||
let api = new UsersApi();
|
||||
|
||||
let response = await api.Login({
|
||||
usernameOrEmail,
|
||||
password,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
let session = response.data?.signedSession;
|
||||
if (session) {
|
||||
localStorage.setItem("session", session);
|
||||
authStatus.value = AUTH_STATUS.LOGGED_IN;
|
||||
let response = await api.GetUserProfile(usernameOrEmail);
|
||||
if (response.success) {
|
||||
userInfo.value = response.data?.user;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}; // end handleOnSubmit
|
||||
@@ -82,6 +99,7 @@ export const Login = () => {
|
||||
shouldShowLoader && "opacity-0",
|
||||
)}
|
||||
>
|
||||
|
||||
<Input
|
||||
label="Username or Email"
|
||||
icon={faUser}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { EditModeRootComponent } from "~/EditModeRootComponent/EditModeRootCompo
|
||||
import { useRef } from "react";
|
||||
import { GenerationEngine } from "~/KonvaApp/GenerationEngine";
|
||||
import { GalleryRootComponent } from "~/GalleryRootComponent";
|
||||
|
||||
import { DemoModal } from "@storyteller/ui-demo-modal";
|
||||
|
||||
export const isTauri = (): boolean => {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { faGear, faImages } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Activity } from "~/components/ui/Activity/Activity";
|
||||
|
||||
import { GalleryModal } from "@storyteller/ui-gallery-modal";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { SettingsModal } from "@storyteller/ui-settings-modal";
|
||||
import { useState } from "react";
|
||||
import { AuthButtons } from "~/components/shared_authentication/AuthButtons";
|
||||
|
||||
export const ToolbarTopRight = () => {
|
||||
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
|
||||
@@ -29,8 +31,8 @@ export const ToolbarTopRight = () => {
|
||||
My Gallery
|
||||
</Button>
|
||||
<Activity />
|
||||
<AuthButtons/>
|
||||
</div>
|
||||
|
||||
<SettingsModal
|
||||
isOpen={isSettingsModalOpen}
|
||||
onClose={() => setIsSettingsModalOpen(false)}
|
||||
|
||||
+5
-20
@@ -23,22 +23,7 @@ export function ProfileDropdown() {
|
||||
} = authentication;
|
||||
|
||||
if (!userInfo.value) return null;
|
||||
|
||||
// Extract user info for better readability
|
||||
const {
|
||||
core_info: {
|
||||
username,
|
||||
gravatar_hash: emailHash,
|
||||
display_name,
|
||||
default_avatar: {
|
||||
image_index: avatarIndex,
|
||||
color_index: backgroundColorIndex,
|
||||
},
|
||||
},
|
||||
} = userInfo.value;
|
||||
|
||||
const profileUrl = `https://storyteller.ai/profile/${display_name}`;
|
||||
|
||||
const profileUrl = `https://storyteller.ai/profile/${userInfo.value.display_name}`;
|
||||
const menuOptions = [
|
||||
{
|
||||
label: "Logout",
|
||||
@@ -57,10 +42,10 @@ export function ProfileDropdown() {
|
||||
>
|
||||
<Gravatar
|
||||
size={34}
|
||||
username={username}
|
||||
email_hash={emailHash}
|
||||
avatarIndex={avatarIndex}
|
||||
backgroundIndex={backgroundColorIndex}
|
||||
username={userInfo.value.display_name}
|
||||
email_hash={userInfo.value.email_gravatar_hash}
|
||||
avatarIndex={Number(userInfo.value.core_info.default_avatar.image_index) || 0}
|
||||
backgroundIndex={Number(userInfo.value.core_info.default_avatar.color_index) || 0}
|
||||
/>
|
||||
<FontAwesomeIcon icon={faChevronDown} />
|
||||
</MenuButton>
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { Link, LinkProps } from "@remix-run/react";
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
interface LinkButtonProps extends LinkProps {
|
||||
interface ButtonLinkProps {
|
||||
icon?: IconDefinition;
|
||||
variant?: "primary" | "secondary";
|
||||
className?: string;
|
||||
to: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ButtonLink = ({
|
||||
@@ -13,9 +17,11 @@ export const ButtonLink = ({
|
||||
children,
|
||||
className: propsClassName,
|
||||
variant = "primary",
|
||||
to,
|
||||
...rest
|
||||
}: LinkButtonProps) => {
|
||||
//TODO: Duplicated from Button.tsx
|
||||
}: ButtonLinkProps) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
function getVariantClassNames(variant: string) {
|
||||
switch (variant) {
|
||||
case "secondary": {
|
||||
@@ -31,14 +37,19 @@ export const ButtonLink = ({
|
||||
"text-sm font-medium whitespace-nowrap rounded-lg px-3 py-2 shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 transition-all duration-150";
|
||||
const variantClassNames = getVariantClassNames(variant);
|
||||
const className = twMerge(baseClassName, variantClassNames, propsClassName);
|
||||
// END TODO
|
||||
|
||||
const handleClick = () => {
|
||||
navigate(to);
|
||||
};
|
||||
|
||||
return (
|
||||
<Link {...rest}>
|
||||
<button className={className}>
|
||||
{icon && <FontAwesomeIcon className="mr-2" icon={icon} size="sm" />}
|
||||
{children}
|
||||
</button>
|
||||
</Link>
|
||||
<button
|
||||
className={className}
|
||||
onClick={handleClick}
|
||||
{...rest}
|
||||
>
|
||||
{icon && <FontAwesomeIcon className="mr-2" icon={icon} size="sm" />}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { AUTH_STATUS } from "~/signals/authentication/enums";
|
||||
import { authentication } from "~/signals";
|
||||
import { ButtonLink } from "~/components/reusable/ButtonLink/ButtonLink";
|
||||
import { ProfileDropdown } from "../features/ToolbarUserProfile/ProfileDropdown";
|
||||
|
||||
// TODO MAKE THIS A SHARED COMPONENT ART CRAFT
|
||||
export const AuthButtons = () => {
|
||||
useSignals();
|
||||
const { status } = authentication.signals;
|
||||
console.log("status", status.value);
|
||||
|
||||
if (status.value === AUTH_STATUS.LOGGED_IN) {
|
||||
return <ProfileDropdown />;
|
||||
} else {
|
||||
return (
|
||||
<div className="flex items-center gap-3.5">
|
||||
<span className="text-white/20">|</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<ButtonLink
|
||||
to="/login"
|
||||
variant="primary"
|
||||
className="bg-[#5F5F68]/60 backdrop-blur-lg hover:bg-[#5F5F68]/90" // TODO(bt,2025-04-19): Once we have in-page routing, get rid of this.
|
||||
>
|
||||
Login
|
||||
</ButtonLink>
|
||||
<ButtonLink
|
||||
to="/signup"
|
||||
variant="primary"
|
||||
className="bg-[#5F5F68]/60 backdrop-blur-lg hover:bg-[#5F5F68]/90" // TODO(bt,2025-04-19): Once we have in-page routing, get rid of this.
|
||||
>
|
||||
Sign Up
|
||||
</ButtonLink>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,193 @@
|
||||
// import { faEnvelope, faKey, faUser } from "@fortawesome/pro-solid-svg-icons";
|
||||
// // import { GlobalSettingsManager } from "./pages/PageEnigma/GlobalSettingsManager";
|
||||
|
||||
// import { Button } from "@storyteller/ui-button";
|
||||
// import { Input } from "@storyteller/ui-input";
|
||||
// import { Toaster } from "@storyteller/ui-toaster";
|
||||
|
||||
// // import { authentication, signUp } from "~/signals";
|
||||
// import { FormEvent, useRef, useState, StrictMode } from "react";
|
||||
// // import { AUTH_STATUS, ToastTypes } from "~/enums";
|
||||
// import { useSignalEffect, useSignals } from "@preact/signals-react/runtime";
|
||||
// import { useNavigate, useSearchParams, BrowserRouter } from "react-router-dom";
|
||||
// import { createRoot } from "react-dom/client";
|
||||
|
||||
// // import "./styles/normalize.css";
|
||||
// // import "./styles/tailwind.css";
|
||||
// // import "./styles/base.css";
|
||||
|
||||
// import "@fortawesome/fontawesome-svg-core/styles.css";
|
||||
|
||||
// import { config } from "@fortawesome/fontawesome-svg-core";
|
||||
|
||||
// config.autoAddCss = false; /* eslint-disable import/first */
|
||||
// // TODO MAKE THIS A SHARED COMPONENT ART CRAFT
|
||||
// const regEx =
|
||||
// /^(([^<>()[\].,;:\s@"]+(.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i;
|
||||
|
||||
// // TODO(bt,2025-04-19): Make these configurable
|
||||
// const ENV = {
|
||||
// BASE_API: "https://api.storyteller.ai",
|
||||
// GOOGLE_API: "https://studio.storyteller.ai",
|
||||
// FUNNEL_API: "https://studio.storyteller.ai",
|
||||
// CDN_API: "https://cdn-2.fakeyou.com",
|
||||
// GRAVATAR_API: "https://studio.storyteller.ai",
|
||||
// DEPLOY_PRIME_URL: "https://studio.storyteller.ai",
|
||||
// };
|
||||
|
||||
// export default function SignUpScreen() {
|
||||
// useSignals();
|
||||
// const navigate = useNavigate();
|
||||
// const formRef = useRef<HTMLFormElement | null>(null);
|
||||
// const [usernameError, setUsernameError] = useState("");
|
||||
// const [emailError, setEmailError] = useState("");
|
||||
// const [passwordError, setPasswordError] = useState("");
|
||||
// const [passwordConfirmationError, setPasswordConfirmationError] =
|
||||
// useState("");
|
||||
// const [searchParams] = useSearchParams();
|
||||
// // const { status: authStatus } = authentication;
|
||||
|
||||
// const handleOnSubmit = (ev: FormEvent<HTMLFormElement>) => {
|
||||
// ev.preventDefault();
|
||||
// if (formRef.current) {
|
||||
// const form = new FormData(formRef.current);
|
||||
// const email = form.get("email")?.toString();
|
||||
// const password = form.get("password")?.toString();
|
||||
// const passwordConfirmation = form
|
||||
// .get("password-confirmation")
|
||||
// ?.toString();
|
||||
// const username = form.get("username")?.toString();
|
||||
// setUsernameError("");
|
||||
// if (!username || username.length < 6) {
|
||||
// setUsernameError("Username must be at least 6 characters long");
|
||||
// }
|
||||
// setEmailError("");
|
||||
// if (!email || !email.match(regEx)) {
|
||||
// setEmailError("Email must be a valid email address");
|
||||
// }
|
||||
// setPasswordError("");
|
||||
// if (!password || password.length < 8) {
|
||||
// setPasswordError("Password must be at least 8 characters long");
|
||||
// }
|
||||
// setPasswordConfirmationError("");
|
||||
// if (!passwordError && passwordConfirmation !== password) {
|
||||
// setPasswordConfirmationError("Passwords do not match");
|
||||
// }
|
||||
|
||||
// if (
|
||||
// !usernameError &&
|
||||
// !passwordError &&
|
||||
// !emailError &&
|
||||
// !passwordConfirmationError &&
|
||||
// signUp
|
||||
// ) {
|
||||
// signUp({
|
||||
// username: username!,
|
||||
// email: email!,
|
||||
// password: password!,
|
||||
// passwordConfirmation: passwordConfirmation!,
|
||||
// }).then((error) => {
|
||||
// console.log("Error occured", error);
|
||||
// if (error) {
|
||||
// Toaster.toast(ToastTypes.ERROR, error);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }; // end handleOnSubmit
|
||||
|
||||
// useSignalEffect(() => {
|
||||
// const redirectPath = searchParams.get("redirect");
|
||||
// if (authStatus.value === AUTH_STATUS.LOGGED_IN) {
|
||||
// navigate(redirectPath ? redirectPath : "/");
|
||||
// return;
|
||||
// }
|
||||
// if (authStatus.value === AUTH_STATUS.NO_ACCESS) {
|
||||
// window.open("https://storyteller.ai/", "_self");
|
||||
// return;
|
||||
// }
|
||||
// });
|
||||
|
||||
// return (
|
||||
// <div className="fixed flex h-full w-full flex-col items-center justify-center overflow-auto bg-[#1b1b1f] bg-center bg-repeat">
|
||||
// <div className="mb-7 flex w-10/12 max-w-2xl items-center justify-center gap-4">
|
||||
// <img
|
||||
// src="/resources/images/artcraft-logo-3.png"
|
||||
// alt="ArtCraft Logo"
|
||||
// className="h-9 select-none"
|
||||
// />
|
||||
// </div>
|
||||
// <div className="w-full max-w-xl rounded-xl shadow-lg">
|
||||
// <div className="glass glass-no-hover relative mx-auto w-full rounded-none rounded-t-xl p-6 shadow-none">
|
||||
// <h1 className="mb-8 text-center text-2xl font-bold">Sign up</h1>
|
||||
// <form
|
||||
// ref={formRef}
|
||||
// onSubmit={handleOnSubmit}
|
||||
// className="flex flex-col gap-4"
|
||||
// >
|
||||
// <Input
|
||||
// label="Username"
|
||||
// icon={faUser}
|
||||
// placeholder="Username"
|
||||
// name="username"
|
||||
// errorMessage={usernameError}
|
||||
// required
|
||||
// />
|
||||
// <Input
|
||||
// type="email"
|
||||
// label="Email"
|
||||
// icon={faEnvelope}
|
||||
// placeholder="Email"
|
||||
// name="email"
|
||||
// errorMessage={emailError}
|
||||
// required
|
||||
// />
|
||||
// <Input
|
||||
// type="password"
|
||||
// label="Password"
|
||||
// icon={faKey}
|
||||
// name="password"
|
||||
// placeholder="Password"
|
||||
// autoComplete="current-password"
|
||||
// errorMessage={passwordError}
|
||||
// required
|
||||
// />
|
||||
// <Input
|
||||
// type="password"
|
||||
// label="Password Confirmation"
|
||||
// icon={faKey}
|
||||
// placeholder="Password Confirmation"
|
||||
// name="password-confirmation"
|
||||
// errorMessage={passwordConfirmationError}
|
||||
// required
|
||||
// />
|
||||
// <Button className="mt-4 py-3">Sign up</Button>
|
||||
// </form>
|
||||
// </div>
|
||||
// <div className="align-items flex w-full max-w-xl rounded-b-xl bg-ui-controls px-6 py-4">
|
||||
// <div className="flex w-full justify-end gap-1 text-sm">
|
||||
// <p className="opacity-75">Already have an account?</p>
|
||||
// <a
|
||||
// href="/login"
|
||||
// className="text-brand-primary-400 hover:text-brand-primary-300 font-medium"
|
||||
// >
|
||||
// Log in
|
||||
// </a>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
// // TODO: Replace environment variables from `root.tsx`
|
||||
// createRoot(document.getElementById("root")!).render(
|
||||
// <>
|
||||
// <StrictMode>
|
||||
// <BrowserRouter>
|
||||
// {/* <GlobalSettingsManager env={ENV} /> */}
|
||||
// <SignUpScreen />
|
||||
// </BrowserRouter>
|
||||
// </StrictMode>
|
||||
// </>,
|
||||
// );
|
||||
-1
@@ -1,5 +1,4 @@
|
||||
import { computed, signal } from "@preact/signals-react";
|
||||
|
||||
import { UserInfo } from "@ApiManager/models/Users";
|
||||
import { ActiveSubscriptions } from "@ApiManager/models/Billing";
|
||||
import { LoyaltyProgram } from "@ApiManager/enums/Billing";
|
||||
@@ -1,5 +1,5 @@
|
||||
import { signals } from "./signals";
|
||||
import { UsersApi } from "@ApiManager/UsersApi";
|
||||
import { signals } from "./auth_signals";
|
||||
import { UsersApi } from "@storyteller/api";
|
||||
import { BillingApi } from "@ApiManager/BillingApi";
|
||||
|
||||
import {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { signals } from "./signals";
|
||||
import { signals } from "./auth_signals";
|
||||
import * as fetchers from "./fetchers";
|
||||
import * as enums from "./enums";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { AUTH_STATUS } from "./enums";
|
||||
import { UserInfo } from "~/Classes/ApiManager/models/Users";
|
||||
import { ActiveSubscriptions } from "@ApiManager/models/Billing";
|
||||
|
||||
import { signals } from "./signals";
|
||||
import { signals } from "./auth_signals";
|
||||
|
||||
export const updateAuthStatus = (newStatus: AUTH_STATUS) => {
|
||||
signals.status.value = newStatus;
|
||||
|
||||
@@ -42,6 +42,9 @@
|
||||
},
|
||||
{
|
||||
"path": "../../libs/components/demo-modal/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../../libs/api/tsconfig.lib.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
{
|
||||
"path": "../../libs/components/demo-modal"
|
||||
},
|
||||
{
|
||||
"path": "../../libs/api"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
|
||||
@@ -66,7 +66,7 @@ export const PopoverMenu = ({
|
||||
"text-sm font-medium rounded-lg px-3 py-2 shadow-sm",
|
||||
"flex gap-2 items-center justify-center outline-none",
|
||||
"transition-all duration-150",
|
||||
"bg-[#5F5F68]/60 px-3 text-white backdrop-blur-lg hover:bg-[#5F5F68]/90",
|
||||
"bg-[#5F5F68]/60 px-3 text-white hover:bg-[#5F5F68]/90",
|
||||
buttonClassName,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { EventDispatcher, Quaternion, Vector3, Vector2 } from "three";
|
||||
import {
|
||||
EventDispatcher,
|
||||
Quaternion,
|
||||
Vector3,
|
||||
Vector2,
|
||||
MathUtils,
|
||||
} from "three";
|
||||
import * as THREE from "three";
|
||||
import { cameras, selectedCameraId } from "~/pages/PageEnigma/signals/camera";
|
||||
import { isPromptBoxFocused } from "~/pages/PageEnigma/signals/promptBox";
|
||||
@@ -15,6 +21,11 @@ class FreeCam extends EventDispatcher {
|
||||
xOffset = 0;
|
||||
yOffset = 0;
|
||||
|
||||
// Smoothing properties
|
||||
smoothingFactor = 0.2;
|
||||
currentVelocity = new Vector3();
|
||||
lastPosition = new Vector3();
|
||||
|
||||
lastMousePosition = new Vector2();
|
||||
mouseVelocity = new Vector2();
|
||||
tmpQuaternion = new Quaternion();
|
||||
@@ -46,6 +57,7 @@ class FreeCam extends EventDispatcher {
|
||||
|
||||
this.object = object;
|
||||
this.domElement = domElement;
|
||||
this.lastPosition.copy(object.position);
|
||||
|
||||
this.keydown = this.keydown.bind(this);
|
||||
this.keyup = this.keyup.bind(this);
|
||||
@@ -168,6 +180,7 @@ class FreeCam extends EventDispatcher {
|
||||
this.dragging = true;
|
||||
this.xOffset = event.clientX;
|
||||
this.yOffset = event.clientY;
|
||||
this.resetVelocity();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,6 +191,7 @@ class FreeCam extends EventDispatcher {
|
||||
if (event.button === 2) {
|
||||
this.xOffset = event.clientX;
|
||||
this.yOffset = event.clientY;
|
||||
this.resetVelocity();
|
||||
}
|
||||
this.dragging = false;
|
||||
}
|
||||
@@ -191,9 +205,28 @@ class FreeCam extends EventDispatcher {
|
||||
const mouseY = event.clientY - this.yOffset;
|
||||
this.xOffset = event.clientX;
|
||||
this.yOffset = event.clientY;
|
||||
|
||||
if (Math.abs(mouseX + mouseY) > 0) {
|
||||
this.object.translateX(-mouseX * (this.movementSpeed * 0.01));
|
||||
this.object.translateY(mouseY * (this.movementSpeed * 0.01));
|
||||
// Store last position before movement
|
||||
this.lastPosition.copy(this.object.position);
|
||||
|
||||
// Apply movement with smoothing
|
||||
const translationX = -mouseX * (this.movementSpeed * 0.01);
|
||||
const translationY = mouseY * (this.movementSpeed * 0.01);
|
||||
|
||||
this.currentVelocity.x = MathUtils.lerp(
|
||||
this.currentVelocity.x,
|
||||
translationX,
|
||||
this.smoothingFactor,
|
||||
);
|
||||
this.currentVelocity.y = MathUtils.lerp(
|
||||
this.currentVelocity.y,
|
||||
translationY,
|
||||
this.smoothingFactor,
|
||||
);
|
||||
|
||||
this.object.translateX(this.currentVelocity.x);
|
||||
this.object.translateY(this.currentVelocity.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -203,7 +236,21 @@ class FreeCam extends EventDispatcher {
|
||||
return;
|
||||
}
|
||||
|
||||
this.object.translateZ(event.deltaY / 120); // 120 is the lowest mouse wheel rot.
|
||||
// Store last position before zoom
|
||||
this.lastPosition.copy(this.object.position);
|
||||
|
||||
const zoomDelta = event.deltaY / 120;
|
||||
this.currentVelocity.z = MathUtils.lerp(
|
||||
this.currentVelocity.z,
|
||||
zoomDelta,
|
||||
this.smoothingFactor,
|
||||
);
|
||||
this.object.translateZ(this.currentVelocity.z);
|
||||
}
|
||||
|
||||
resetVelocity() {
|
||||
this.currentVelocity.set(0, 0, 0);
|
||||
this.lastPosition.copy(this.object.position);
|
||||
}
|
||||
|
||||
reset() {
|
||||
@@ -217,6 +264,9 @@ class FreeCam extends EventDispatcher {
|
||||
this.moveState.yawRight = 0;
|
||||
this.moveState.down = 0;
|
||||
this.moveState.up = 0;
|
||||
|
||||
this.resetVelocity();
|
||||
|
||||
this.updateMovementVector();
|
||||
}
|
||||
|
||||
@@ -225,17 +275,31 @@ class FreeCam extends EventDispatcher {
|
||||
}
|
||||
|
||||
update(delta: number) {
|
||||
if (!this.enabled || this.isStationary()) {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const moveMulti = delta * this.movementSpeed;
|
||||
|
||||
this.object.translateX(this.moveVector.x * moveMulti);
|
||||
this.object.translateY(this.moveVector.y * moveMulti);
|
||||
this.object.translateZ(this.moveVector.z * moveMulti);
|
||||
// Apply WASD movement with smoothing
|
||||
const targetMovement = new Vector3(
|
||||
this.moveVector.x * moveMulti,
|
||||
this.moveVector.y * moveMulti,
|
||||
this.moveVector.z * moveMulti,
|
||||
);
|
||||
|
||||
// Update camera position in signals
|
||||
// Smooth out the movement
|
||||
this.currentVelocity.lerp(targetMovement, this.smoothingFactor);
|
||||
|
||||
// Apply the smoothed movement
|
||||
this.object.translateX(this.currentVelocity.x);
|
||||
this.object.translateY(this.currentVelocity.y);
|
||||
this.object.translateZ(this.currentVelocity.z);
|
||||
|
||||
// Store last position
|
||||
this.lastPosition.copy(this.object.position);
|
||||
|
||||
// Update camera signals
|
||||
if (selectedCameraId.value) {
|
||||
const pos = this.object.position;
|
||||
const rot = this.object.rotation;
|
||||
|
||||
@@ -585,7 +585,7 @@ export const PromptBox = () => {
|
||||
]}
|
||||
onPanelAction={handleAction}
|
||||
showIconsInList
|
||||
buttonClassName="backdrop-blur-none bg-transparent hover:bg-transparent py-1.5 px-0 pr-1 m-0 hover:opacity-50 transition-opacity duration-100 ring-0 border-none focus:ring-0 outline-none"
|
||||
buttonClassName="bg-transparent hover:bg-transparent py-1.5 px-0 pr-1 m-0 hover:opacity-50 transition-opacity duration-100 ring-0 border-none focus:ring-0 outline-none"
|
||||
triggerIcon={
|
||||
<FontAwesomeIcon icon={faPlus} className="text-xl" />
|
||||
}
|
||||
|
||||
@@ -1,91 +1,91 @@
|
||||
import { ApiManager, ApiResponse } from "./ApiManager";
|
||||
// import { ApiManager, ApiResponse } from "../apis/ApiManager";
|
||||
|
||||
export class CommentsApi extends ApiManager {
|
||||
public CreateComment({
|
||||
commentMarkdown,
|
||||
entityToken,
|
||||
entityType,
|
||||
uuidIdempotencyToken,
|
||||
}: {
|
||||
commentMarkdown: string;
|
||||
entityToken: string;
|
||||
entityType: string;
|
||||
uuidIdempotencyToken: string;
|
||||
}): Promise<ApiResponse<string>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/comments/new`;
|
||||
// export class CommentsApi extends ApiManager {
|
||||
// public CreateComment({
|
||||
// commentMarkdown,
|
||||
// entityToken,
|
||||
// entityType,
|
||||
// uuidIdempotencyToken,
|
||||
// }: {
|
||||
// commentMarkdown: string;
|
||||
// entityToken: string;
|
||||
// entityType: string;
|
||||
// uuidIdempotencyToken: string;
|
||||
// }): Promise<ApiResponse<string>> {
|
||||
// const endpoint = `${this.ApiTargets.BaseApi}/v1/comments/new`;
|
||||
|
||||
const body = {
|
||||
comment_markdown: commentMarkdown,
|
||||
entity_token: entityToken,
|
||||
entity_type: entityType,
|
||||
uuid_idempotency_token: uuidIdempotencyToken,
|
||||
};
|
||||
// const body = {
|
||||
// comment_markdown: commentMarkdown,
|
||||
// entity_token: entityToken,
|
||||
// entity_type: entityType,
|
||||
// uuid_idempotency_token: uuidIdempotencyToken,
|
||||
// };
|
||||
|
||||
return this.post<
|
||||
{
|
||||
comment_markdown: string;
|
||||
entity_token: string;
|
||||
entity_type: string;
|
||||
uuid_idempotency_token: string;
|
||||
},
|
||||
{
|
||||
success?: boolean;
|
||||
comment_token?: string;
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint, body })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
data: response.comment_token,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
// return this.post<
|
||||
// {
|
||||
// comment_markdown: string;
|
||||
// entity_token: string;
|
||||
// entity_type: string;
|
||||
// uuid_idempotency_token: string;
|
||||
// },
|
||||
// {
|
||||
// success?: boolean;
|
||||
// comment_token?: string;
|
||||
// BadInput?: string;
|
||||
// }
|
||||
// >({ endpoint, body })
|
||||
// .then((response) => ({
|
||||
// success: response.success ?? false,
|
||||
// data: response.comment_token,
|
||||
// errorMessage: response.BadInput,
|
||||
// }))
|
||||
// .catch((err) => {
|
||||
// return { success: false, errorMessage: err.message };
|
||||
// });
|
||||
// }
|
||||
|
||||
public DeleteComment({
|
||||
commentToken,
|
||||
}: {
|
||||
commentToken: string;
|
||||
}): Promise<ApiResponse<undefined>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/comments/delete/${commentToken}`;
|
||||
// public DeleteComment({
|
||||
// commentToken,
|
||||
// }: {
|
||||
// commentToken: string;
|
||||
// }): Promise<ApiResponse<undefined>> {
|
||||
// const endpoint = `${this.ApiTargets.BaseApi}/v1/comments/delete/${commentToken}`;
|
||||
|
||||
return this.post<
|
||||
{ as_mod: boolean },
|
||||
{
|
||||
success?: boolean;
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint, body: { as_mod: true } })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
// return this.post<
|
||||
// { as_mod: boolean },
|
||||
// {
|
||||
// success?: boolean;
|
||||
// BadInput?: string;
|
||||
// }
|
||||
// >({ endpoint, body: { as_mod: true } })
|
||||
// .then((response) => ({
|
||||
// success: response.success ?? false,
|
||||
// errorMessage: response.BadInput,
|
||||
// }))
|
||||
// .catch((err) => {
|
||||
// return { success: false, errorMessage: err.message };
|
||||
// });
|
||||
// }
|
||||
|
||||
public ListCommentsByEntity({
|
||||
entityType,
|
||||
entityToken,
|
||||
}: {
|
||||
entityType: string;
|
||||
entityToken: string;
|
||||
}): Promise<ApiResponse<string[]>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/comments/list/${entityType}/${entityToken}`;
|
||||
// public ListCommentsByEntity({
|
||||
// entityType,
|
||||
// entityToken,
|
||||
// }: {
|
||||
// entityType: string;
|
||||
// entityToken: string;
|
||||
// }): Promise<ApiResponse<string[]>> {
|
||||
// const endpoint = `${this.ApiTargets.BaseApi}/v1/comments/list/${entityType}/${entityToken}`;
|
||||
|
||||
return this.get<{
|
||||
success: boolean;
|
||||
comments: string[];
|
||||
}>({ endpoint })
|
||||
.then((response) => ({
|
||||
success: true,
|
||||
data: response.comments,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
}
|
||||
// return this.get<{
|
||||
// success: boolean;
|
||||
// comments: string[];
|
||||
// }>({ endpoint })
|
||||
// .then((response) => ({
|
||||
// success: true,
|
||||
// data: response.comments,
|
||||
// }))
|
||||
// .catch((err) => {
|
||||
// return { success: false, errorMessage: err.message };
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -1,96 +1,96 @@
|
||||
import { ApiManager, ApiResponse } from "./ApiManager";
|
||||
// import { ApiManager, ApiResponse } from "../apis/ApiManager";
|
||||
|
||||
export class FeaturedItemsApi extends ApiManager {
|
||||
public CreateFeaturedItem({
|
||||
entityToken,
|
||||
entityType,
|
||||
}: {
|
||||
entityToken: string;
|
||||
entityType: string;
|
||||
}): Promise<ApiResponse<undefined>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/featured_item/create`;
|
||||
// export class FeaturedItemsApi extends ApiManager {
|
||||
// public CreateFeaturedItem({
|
||||
// entityToken,
|
||||
// entityType,
|
||||
// }: {
|
||||
// entityToken: string;
|
||||
// entityType: string;
|
||||
// }): Promise<ApiResponse<undefined>> {
|
||||
// const endpoint = `${this.ApiTargets.BaseApi}/v1/featured_item/create`;
|
||||
|
||||
const body = {
|
||||
entity_token: entityToken,
|
||||
entity_type: entityType,
|
||||
};
|
||||
// const body = {
|
||||
// entity_token: entityToken,
|
||||
// entity_type: entityType,
|
||||
// };
|
||||
|
||||
return this.post<
|
||||
{
|
||||
entity_token: string;
|
||||
entity_type: string;
|
||||
},
|
||||
{
|
||||
success?: boolean;
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint, body })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
// return this.post<
|
||||
// {
|
||||
// entity_token: string;
|
||||
// entity_type: string;
|
||||
// },
|
||||
// {
|
||||
// success?: boolean;
|
||||
// BadInput?: string;
|
||||
// }
|
||||
// >({ endpoint, body })
|
||||
// .then((response) => ({
|
||||
// success: response.success ?? false,
|
||||
// errorMessage: response.BadInput,
|
||||
// }))
|
||||
// .catch((err) => {
|
||||
// return { success: false, errorMessage: err.message };
|
||||
// });
|
||||
// }
|
||||
|
||||
public DeleteFeaturedItem({
|
||||
entityToken,
|
||||
entityType,
|
||||
}: {
|
||||
entityToken: string;
|
||||
entityType: string;
|
||||
}): Promise<ApiResponse<undefined>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/featured_item/delete`;
|
||||
// public DeleteFeaturedItem({
|
||||
// entityToken,
|
||||
// entityType,
|
||||
// }: {
|
||||
// entityToken: string;
|
||||
// entityType: string;
|
||||
// }): Promise<ApiResponse<undefined>> {
|
||||
// const endpoint = `${this.ApiTargets.BaseApi}/v1/featured_item/delete`;
|
||||
|
||||
const body = {
|
||||
entity_token: entityToken,
|
||||
entity_type: entityType,
|
||||
};
|
||||
// const body = {
|
||||
// entity_token: entityToken,
|
||||
// entity_type: entityType,
|
||||
// };
|
||||
|
||||
return this.delete<
|
||||
{
|
||||
entity_token: string;
|
||||
entity_type: string;
|
||||
},
|
||||
{
|
||||
success?: boolean;
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint, body })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
// return this.delete<
|
||||
// {
|
||||
// entity_token: string;
|
||||
// entity_type: string;
|
||||
// },
|
||||
// {
|
||||
// success?: boolean;
|
||||
// BadInput?: string;
|
||||
// }
|
||||
// >({ endpoint, body })
|
||||
// .then((response) => ({
|
||||
// success: response.success ?? false,
|
||||
// errorMessage: response.BadInput,
|
||||
// }))
|
||||
// .catch((err) => {
|
||||
// return { success: false, errorMessage: err.message };
|
||||
// });
|
||||
// }
|
||||
|
||||
public CheckFeaturedItem({
|
||||
entityType,
|
||||
entityToken,
|
||||
}: {
|
||||
entityType: string;
|
||||
entityToken: string;
|
||||
}): Promise<ApiResponse<boolean>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/featured_item/is_featured/${entityType}/${entityToken}`;
|
||||
// public CheckFeaturedItem({
|
||||
// entityType,
|
||||
// entityToken,
|
||||
// }: {
|
||||
// entityType: string;
|
||||
// entityToken: string;
|
||||
// }): Promise<ApiResponse<boolean>> {
|
||||
// const endpoint = `${this.ApiTargets.BaseApi}/v1/featured_item/is_featured/${entityType}/${entityToken}`;
|
||||
|
||||
return this.post<
|
||||
undefined,
|
||||
{
|
||||
success?: boolean;
|
||||
is_featured?: boolean;
|
||||
BadInput?: string;
|
||||
}
|
||||
>({ endpoint })
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
data: response.is_featured,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => {
|
||||
return { success: false, errorMessage: err.message };
|
||||
});
|
||||
}
|
||||
}
|
||||
// return this.post<
|
||||
// undefined,
|
||||
// {
|
||||
// success?: boolean;
|
||||
// is_featured?: boolean;
|
||||
// BadInput?: string;
|
||||
// }
|
||||
// >({ endpoint })
|
||||
// .then((response) => ({
|
||||
// success: response.success ?? false,
|
||||
// data: response.is_featured,
|
||||
// errorMessage: response.BadInput,
|
||||
// }))
|
||||
// .catch((err) => {
|
||||
// return { success: false, errorMessage: err.message };
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -1,42 +1,43 @@
|
||||
import { ApiManager, ApiResponse } from "./ApiManager";
|
||||
// import { ApiManager, ApiResponse } from "../apis/ApiManager";
|
||||
|
||||
export class ModerationApi extends ApiManager {
|
||||
public async UpdateFeatureFlagsByToken({
|
||||
usernameOrToken,
|
||||
flags,
|
||||
}: {
|
||||
usernameOrToken: string;
|
||||
flags: string[];
|
||||
}): Promise<
|
||||
ApiResponse<{
|
||||
inference_job_token?: string;
|
||||
inference_job_token_type?: string;
|
||||
}>
|
||||
> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/moderation/user_feature_flags/${usernameOrToken}`;
|
||||
|
||||
const body = {
|
||||
action: {
|
||||
AddFlags: {
|
||||
flags,
|
||||
},
|
||||
},
|
||||
};
|
||||
// export class ModerationApi extends ApiManager {
|
||||
// public async UpdateFeatureFlagsByToken({
|
||||
// usernameOrToken,
|
||||
// flags,
|
||||
// }: {
|
||||
// usernameOrToken: string;
|
||||
// flags: string[];
|
||||
// }): Promise<
|
||||
// ApiResponse<{
|
||||
// inference_job_token?: string;
|
||||
// inference_job_token_type?: string;
|
||||
// }>
|
||||
// > {
|
||||
// const endpoint = `${this.ApiTargets.BaseApi}/v1/moderation/user_feature_flags/${usernameOrToken}`;
|
||||
|
||||
return this.post<
|
||||
{ action: { AddFlags: { flags: string[] } } },
|
||||
{ success?: boolean; BadInput?: string }
|
||||
>({
|
||||
endpoint,
|
||||
body: body,
|
||||
})
|
||||
.then((response) => ({
|
||||
success: response.success ?? false,
|
||||
errorMessage: response.BadInput,
|
||||
}))
|
||||
.catch((err) => ({
|
||||
success: false,
|
||||
errorMessage: err.message,
|
||||
}));
|
||||
}
|
||||
}
|
||||
// const body = {
|
||||
// action: {
|
||||
// AddFlags: {
|
||||
// flags,
|
||||
// },
|
||||
// },
|
||||
// };
|
||||
|
||||
// return this.post<
|
||||
// { action: { AddFlags: { flags: string[] } } },
|
||||
// { success?: boolean; BadInput?: string }
|
||||
// >({
|
||||
// endpoint,
|
||||
// body: body,
|
||||
// })
|
||||
// .then((response) => ({
|
||||
// success: response.success ?? false,
|
||||
// errorMessage: response.BadInput,
|
||||
// }))
|
||||
// .catch((err) => ({
|
||||
// success: false,
|
||||
// errorMessage: err.message,
|
||||
// }));
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -36,6 +36,7 @@ export class UsersApi extends ApiManager {
|
||||
}>
|
||||
> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/user/${username}/profile`;
|
||||
console.log("endpoint", endpoint);
|
||||
return this.get<{
|
||||
success: boolean;
|
||||
user?: UserInfo;
|
||||
@@ -62,6 +63,7 @@ export class UsersApi extends ApiManager {
|
||||
usernameOrEmail: string;
|
||||
password: string;
|
||||
}): Promise<ApiResponse<{ signedSession?: string }>> {
|
||||
console.log("Logging in with usernameOrEmail:", usernameOrEmail);
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/login`;
|
||||
const body = {
|
||||
username_or_email: usernameOrEmail,
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.5.0"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user