mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-03 13:02:37 -05:00
Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80e428ee60 | ||
|
|
a4e3525080 | ||
|
|
7e8ca70153 | ||
|
|
9152b61cca | ||
|
|
24ccda5a61 | ||
|
|
cdd88ff6d0 | ||
|
|
930d705217 | ||
|
|
d80c3fdfe7 | ||
|
|
7d5c741bc0 | ||
|
|
521c1ebf22 | ||
|
|
d50cb38178 | ||
|
|
7ce0018e13 | ||
|
|
b423e4d524 | ||
|
|
89896611e7 |
+3
-47
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n j.status as `job_status: enums::common::job_status_plus::JobStatusPlus`,\n j.failure_reason as job_failure_reason,\n wle.credits_delta,\n wle.maybe_linked_refund_ledger_token as `maybe_linked_refund_ledger_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,\n j.on_success_result_entity_token as `on_success_result_media_token: tokens::tokens::media_files::MediaFileToken`,\n j.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n wle.token as `wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,\n wle.entry_type as `wallet_ledger_entry_type: enums::by_table::wallet_ledger_entries::wallet_ledger_entry_type::WalletLedgerEntryType`,\n j.maybe_external_third_party as `maybe_external_third_party: enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty`,\n j.maybe_external_third_party_id,\n j.created_at,\n j.updated_at\nFROM generic_inference_jobs as j\nLEFT OUTER JOIN users as u\n ON j.maybe_creator_user_token = u.token\nLEFT OUTER JOIN wallet_ledger_entries as wle\n ON wle.maybe_entity_ref = j.token\nWHERE u.token = ?\nORDER BY j.id DESC\n ",
|
||||
"query": "\nSELECT\n j.status as `job_status: enums::common::job_status_plus::JobStatusPlus`,\n j.failure_reason as job_failure_reason,\n wle.credits_delta,\n wle.maybe_linked_refund_ledger_token as `maybe_linked_refund_ledger_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,\n j.on_success_result_entity_token as `on_success_result_media_token: tokens::tokens::media_files::MediaFileToken`,\n j.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n wle.token as `wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,\n wle.entry_type as `wallet_ledger_entry_type: enums::by_table::wallet_ledger_entries::wallet_ledger_entry_type::WalletLedgerEntryType`\nFROM generic_inference_jobs as j\nLEFT OUTER JOIN users as u\n ON j.maybe_creator_user_token = u.token\nLEFT OUTER JOIN wallet_ledger_entries as wle\n ON wle.maybe_entity_ref = j.token\nWHERE u.token = ?\nORDER BY j.id DESC\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -82,46 +82,6 @@
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "maybe_external_third_party: enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "maybe_external_third_party_id",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 256
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "updated_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -135,12 +95,8 @@
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "cbdaa45597de14d1eb6dc0f6a95788d56334757bb3d03eda2eb64df3ba2a0144"
|
||||
"hash": "0757d81ba153b2ad2df25df3dd92b655ef08b95f5f83251bf3573f9be3a9f8d1"
|
||||
}
|
||||
Generated
-2
@@ -11263,7 +11263,6 @@ dependencies = [
|
||||
"mimetypes",
|
||||
"mysql_queries",
|
||||
"once_cell",
|
||||
"pager",
|
||||
"password",
|
||||
"primitives",
|
||||
"prompt_engineering",
|
||||
@@ -11276,7 +11275,6 @@ dependencies = [
|
||||
"regex",
|
||||
"resend-rs",
|
||||
"reusable_types",
|
||||
"rootly_client",
|
||||
"rust-s3",
|
||||
"rustls 0.23.36",
|
||||
"seedance2pro_client",
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pub mod moderation_send_alert;
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub const MODERATION_SEND_ALERT_PATH: &str = "/v1/moderation/alerts/send";
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct ModerationSendAlertRequest {
|
||||
/// Optional title for the alert. Defaults to "Test Moderation Alert".
|
||||
pub title: Option<String>,
|
||||
|
||||
/// Optional description for the alert. Defaults to "This is a test moderation alert."
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ModerationSendAlertResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
-6
@@ -1,5 +1,3 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty;
|
||||
use enums::by_table::wallet_ledger_entries::wallet_ledger_entry_type::WalletLedgerEntryType;
|
||||
use enums::common::job_status_plus::JobStatusPlus;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
@@ -32,8 +30,4 @@ pub struct ListUserJobsEntry {
|
||||
pub job_token: InferenceJobToken,
|
||||
pub wallet_ledger_entry_token: Option<WalletLedgerEntryToken>,
|
||||
pub wallet_ledger_entry_type: Option<WalletLedgerEntryType>,
|
||||
pub maybe_external_third_party: Option<InferenceJobExternalThirdParty>,
|
||||
pub maybe_external_third_party_id: Option<String>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod alerts;
|
||||
pub mod jobs;
|
||||
pub mod user;
|
||||
pub mod wallet_ledger_entries;
|
||||
|
||||
@@ -14,16 +14,4 @@ pub enum CommonResolution {
|
||||
ThreeK,
|
||||
/// Models: Nano Banana Pro
|
||||
FourK,
|
||||
|
||||
/// Used by Nano Banana 2 (and others)
|
||||
HalfK,
|
||||
|
||||
/// Used by the Seedance family (and others)
|
||||
FourEightyP,
|
||||
|
||||
/// Used by the Seedance family (and others)
|
||||
SevenTwentyP,
|
||||
|
||||
/// Used by the Seedance family (and others)
|
||||
TenEightyP,
|
||||
}
|
||||
|
||||
-2
@@ -135,8 +135,6 @@ fn plan_resolution(
|
||||
// No 3K option; default to 2K
|
||||
Ok(Some(Nb2Res::TwoK))
|
||||
}
|
||||
// HalfK, FourEightyP, SevenTwentyP, TenEightyP — nearest supported is OneK
|
||||
Some(R::HalfK | R::FourEightyP | R::SevenTwentyP | R::TenEightyP) => Ok(Some(Nb2Res::OneK)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-2
@@ -135,8 +135,6 @@ fn plan_resolution(
|
||||
// No 3K option; default to 2K
|
||||
Ok(Some(NbpRes::TwoK))
|
||||
}
|
||||
// HalfK, FourEightyP, SevenTwentyP, TenEightyP — nearest supported is OneK
|
||||
Some(R::HalfK | R::FourEightyP | R::SevenTwentyP | R::TenEightyP) => Ok(Some(NbpRes::OneK)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-5
@@ -202,11 +202,6 @@ fn plan_resolution(
|
||||
Some(CommonResolution::FourK) => Ok(Some(FalNbpResolution::FourK)),
|
||||
// No 3K option; fall back to 2K
|
||||
Some(CommonResolution::ThreeK) => Ok(Some(FalNbpResolution::TwoK)),
|
||||
// Fall back to nearest supported resolution
|
||||
Some(CommonResolution::HalfK) => Ok(Some(FalNbpResolution::OneK)),
|
||||
Some(CommonResolution::FourEightyP) => Ok(Some(FalNbpResolution::OneK)),
|
||||
Some(CommonResolution::SevenTwentyP) => Ok(Some(FalNbpResolution::OneK)),
|
||||
Some(CommonResolution::TenEightyP) => Ok(Some(FalNbpResolution::OneK)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-4
@@ -167,10 +167,6 @@ fn plan_resolution(
|
||||
Some(CommonResolution::TwoK) => Ok(Some(Seedance1p5ProMultiFunctionVideoGenResolution::TenEightyP)),
|
||||
Some(CommonResolution::ThreeK) => Ok(Some(Seedance1p5ProMultiFunctionVideoGenResolution::TenEightyP)),
|
||||
Some(CommonResolution::FourK) => Ok(Some(Seedance1p5ProMultiFunctionVideoGenResolution::TenEightyP)),
|
||||
// HalfK and FourEightyP map to the lowest supported resolution
|
||||
Some(CommonResolution::HalfK | CommonResolution::FourEightyP) => Ok(Some(Seedance1p5ProMultiFunctionVideoGenResolution::FourEightyP)),
|
||||
Some(CommonResolution::SevenTwentyP) => Ok(Some(Seedance1p5ProMultiFunctionVideoGenResolution::SevenTwentyP)),
|
||||
Some(CommonResolution::TenEightyP) => Ok(Some(Seedance1p5ProMultiFunctionVideoGenResolution::TenEightyP)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -685,55 +685,6 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real cookies
|
||||
async fn test_pro_keyframe_with_start_frame() -> AnyhowResult<()> {
|
||||
setup_test_logging(LevelFilter::Trace);
|
||||
let session = test_session()?;
|
||||
|
||||
// Upload a start frame image from our CDN
|
||||
let image_bytes = crate::test_utils::http_download::http_download_to_bytes(
|
||||
test_data::web::image_urls::JUNO_AT_LAKE_IMAGE_URL,
|
||||
).await?;
|
||||
|
||||
let prepare_result = prepare_file_upload(PrepareFileUploadArgs {
|
||||
session: &session,
|
||||
extension: "jpg".to_string(),
|
||||
host_override: None,
|
||||
}).await?;
|
||||
|
||||
let upload_result = upload_file(UploadFileArgs {
|
||||
upload_url: prepare_result.upload_url,
|
||||
file_bytes: image_bytes,
|
||||
host_override: None,
|
||||
}).await?;
|
||||
|
||||
println!("Uploaded start frame: {}", upload_result.public_url);
|
||||
|
||||
let args = GenerateVideoArgs {
|
||||
session: &session,
|
||||
model_type: ModelType::Seedance2Pro,
|
||||
prompt: "The corgi dog watches the lake.".to_string(),
|
||||
resolution: Resolution::Portrait9x16,
|
||||
duration_seconds: 5,
|
||||
batch_count: BatchCount::One,
|
||||
start_frame_url: Some(upload_result.public_url),
|
||||
end_frame_url: None,
|
||||
reference_image_urls: None,
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
use_face_blur_hack: None,
|
||||
host_override: None,
|
||||
};
|
||||
let result = generate_video(args).await?;
|
||||
println!("Task ID: {}", result.task_id);
|
||||
println!("Order ID: {}", result.order_id);
|
||||
assert!(!result.task_id.is_empty());
|
||||
assert!(!result.order_id.is_empty());
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect output.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// --- Seedance 2 Fast tests ---
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -35,11 +35,7 @@ impl KinoviHost {
|
||||
/// Returns the CDN base URL for uploaded/static files (no trailing slash).
|
||||
pub fn cdn_base_url(&self) -> &str {
|
||||
match self {
|
||||
// NB: The API endpoint moved to kinovi.ai but the CDN for uploaded materials
|
||||
// still uses the legacy seedance2-pro.com domain. The generate_video API
|
||||
// expects URLs on this domain.
|
||||
Self::Kinovi => "https://static.seedance2-pro.com",
|
||||
// Self::Kinovi => "https://static.kinovi.ai",
|
||||
Self::Kinovi => "https://static.kinovi.ai",
|
||||
Self::Seedance2Pro => "https://static.seedance2-pro.com",
|
||||
Self::CustomHost { cdn_host, .. } => cdn_host.as_str(),
|
||||
}
|
||||
|
||||
@@ -14,16 +14,4 @@ pub enum CommonResolution {
|
||||
ThreeK,
|
||||
/// Models: Nano Banana Pro
|
||||
FourK,
|
||||
|
||||
/// Used by Nano Banana 2 (and others)
|
||||
HalfK,
|
||||
|
||||
/// Used by the Seedance family (and others)
|
||||
FourEightyP,
|
||||
|
||||
/// Used by the Seedance family (and others)
|
||||
SevenTwentyP,
|
||||
|
||||
/// Used by the Seedance family (and others)
|
||||
TenEightyP,
|
||||
}
|
||||
|
||||
-5
@@ -8,10 +8,5 @@ pub fn resolution_to_artcraft_nano_banana_pro(resolution: CommonResolution) -> N
|
||||
// NB: ThreeK doesn't exist in Nano Banana Pro, use FourK as nearest higher resolution
|
||||
CommonResolution::ThreeK => NanoBananaProMultiFunctionImageGenImageResolution::FourK,
|
||||
CommonResolution::FourK => NanoBananaProMultiFunctionImageGenImageResolution::FourK,
|
||||
// Fall back to nearest supported resolution
|
||||
CommonResolution::HalfK => NanoBananaProMultiFunctionImageGenImageResolution::OneK,
|
||||
CommonResolution::FourEightyP => NanoBananaProMultiFunctionImageGenImageResolution::OneK,
|
||||
CommonResolution::SevenTwentyP => NanoBananaProMultiFunctionImageGenImageResolution::OneK,
|
||||
CommonResolution::TenEightyP => NanoBananaProMultiFunctionImageGenImageResolution::OneK,
|
||||
}
|
||||
}
|
||||
|
||||
-4
@@ -144,9 +144,5 @@ fn convert_desktop_resolution(res: CommonResolution2) -> RouterCommonResolution
|
||||
CommonResolution2::TwoK => RouterCommonResolution::TwoK,
|
||||
CommonResolution2::ThreeK => RouterCommonResolution::ThreeK,
|
||||
CommonResolution2::FourK => RouterCommonResolution::FourK,
|
||||
CommonResolution2::HalfK => RouterCommonResolution::HalfK,
|
||||
CommonResolution2::FourEightyP => RouterCommonResolution::FourEightyP,
|
||||
CommonResolution2::SevenTwentyP => RouterCommonResolution::SevenTwentyP,
|
||||
CommonResolution2::TenEightyP => RouterCommonResolution::TenEightyP,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use log::{debug, info, warn};
|
||||
use log::info;
|
||||
|
||||
use crate::client::pager_client::{PageSentResult, PagerClient};
|
||||
use crate::error::pager_error::PagerError;
|
||||
@@ -15,9 +15,20 @@ use crate::worker::pager_worker_message_queue::PagerWorkerMessageQueue;
|
||||
/// - **Immediate**: `send_page_immediately()` sends inline (blocks until API responds).
|
||||
/// - **Queued**: `enqueue_page()` pushes to a background worker (non-blocking).
|
||||
///
|
||||
/// Build an instance via `PagerBuilder`.
|
||||
/// Build an instance via `PagerBuilder`:
|
||||
/// ```ignore
|
||||
/// // Without worker:
|
||||
/// let pager = PagerBuilder::new()
|
||||
/// .application_name("my-service".to_string())
|
||||
/// .rootly(api_key)
|
||||
/// .build()?;
|
||||
///
|
||||
#[derive(Clone)]
|
||||
/// // With worker:
|
||||
/// let (pager, worker) = PagerBuilder::new()
|
||||
/// .application_name("my-service".to_string())
|
||||
/// .rootly(api_key)
|
||||
/// .build_with_worker()?;
|
||||
/// ```
|
||||
pub struct Pager {
|
||||
client: PagerClient,
|
||||
queue: Option<Arc<PagerWorkerMessageQueue>>,
|
||||
@@ -44,43 +55,21 @@ impl Pager {
|
||||
}
|
||||
|
||||
/// Send a page immediately, blocking until the API responds.
|
||||
/// Returns `Ok(None)` if the pager is configured as NoOp.
|
||||
pub async fn send_page_immediately(
|
||||
&self,
|
||||
notification: NotificationDetails,
|
||||
) -> Result<Option<PageSentResult>, PagerError> {
|
||||
) -> Result<PageSentResult, PagerError> {
|
||||
self.client.send_page(¬ification).await
|
||||
}
|
||||
|
||||
/// Send a page immediately. Logs errors but never fails.
|
||||
/// Returns `None` if the pager is NoOp or if sending failed.
|
||||
pub async fn send_page_immediately_infallible(
|
||||
&self,
|
||||
notification: NotificationDetails,
|
||||
) -> Option<PageSentResult> {
|
||||
match self.send_page_immediately(notification).await {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
warn!("Failure sending page: {:?}", err);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enqueue a page to be sent by the background worker thread.
|
||||
///
|
||||
/// Returns `Ok(())` immediately if the pager is NoOp.
|
||||
/// Returns an error if the worker is not configured (use `build_with_worker()`).
|
||||
/// If the queue is full, the oldest item is dropped.
|
||||
pub fn enqueue_page(
|
||||
&self,
|
||||
notification: NotificationDetails,
|
||||
) -> Result<(), PagerError> {
|
||||
if self.client.is_noop() {
|
||||
debug!("Pager no-op: would have enqueued page: {}", notification.summary);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let queue = self.queue.as_ref()
|
||||
.ok_or(PagerSystemError::QueueNotConfigured)?;
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use log::warn;
|
||||
use rootly_client::creds::rootly_api_key::RootlyApiKey;
|
||||
|
||||
use crate::client::pager::Pager;
|
||||
use crate::client::pager_client::{PagerClient, PagerClientConfig};
|
||||
use crate::error::pager_builder_error::PagerBuilderError;
|
||||
use crate::error::pager_error::PagerError;
|
||||
use crate::worker::pager_worker_message_queue::{new_shared_queue, new_shared_queue_with_capacity, PagerWorkerMessageQueue};
|
||||
use crate::worker::pager_worker::PagerWorker;
|
||||
|
||||
@@ -13,7 +14,6 @@ pub struct PagerBuilder {
|
||||
client_config: Option<PagerClientConfig>,
|
||||
application_name: Option<String>,
|
||||
environment: Option<String>,
|
||||
hostname: Option<String>,
|
||||
queue_capacity: Option<usize>,
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ impl PagerBuilder {
|
||||
client_config: None,
|
||||
application_name: None,
|
||||
environment: None,
|
||||
hostname: None,
|
||||
queue_capacity: None,
|
||||
}
|
||||
}
|
||||
@@ -40,12 +39,6 @@ impl PagerBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the hostname of the machine sending alerts.
|
||||
pub fn hostname(mut self, hostname: String) -> Self {
|
||||
self.hostname = Some(hostname);
|
||||
self
|
||||
}
|
||||
|
||||
/// Configure the Rootly backend. Returns a sub-builder for Rootly-specific options.
|
||||
pub fn rootly(self, api_key: RootlyApiKey) -> RootlyConfigBuilder {
|
||||
RootlyConfigBuilder {
|
||||
@@ -72,25 +65,30 @@ impl PagerBuilder {
|
||||
|
||||
/// Build a `Pager` without a background worker.
|
||||
///
|
||||
/// If no backend was configured, a NoOp pager is returned.
|
||||
pub fn build(self) -> Pager {
|
||||
let client = self.make_client();
|
||||
Pager::new(client)
|
||||
/// Only `send_page_immediately()` will be available.
|
||||
/// Calling `enqueue_page()` will return `PagerServiceError::WorkerNotAvailable`.
|
||||
pub fn build(self) -> Result<Pager, PagerError> {
|
||||
let client = self.make_client()?;
|
||||
Ok(Pager::new(client))
|
||||
}
|
||||
|
||||
/// Build a `Pager` with a background worker thread.
|
||||
///
|
||||
/// If no backend was configured, a NoOp pager is returned (the worker will idle).
|
||||
///
|
||||
/// Returns both the `Pager` (for enqueuing) and the `PagerWorker` (to spawn).
|
||||
/// The caller is responsible for running the worker on a dedicated thread:
|
||||
/// ```ignore
|
||||
/// let (pager, worker) = PagerBuilder::new()
|
||||
/// .application_name("my-service".to_string())
|
||||
/// .rootly(api_key)
|
||||
/// .build_with_worker();
|
||||
/// .build_with_worker()?;
|
||||
///
|
||||
/// std::thread::spawn(move || {
|
||||
/// let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
/// rt.block_on(worker.run());
|
||||
/// });
|
||||
/// ```
|
||||
pub fn build_with_worker(self) -> (Pager, PagerWorker) {
|
||||
let client = self.make_client();
|
||||
pub fn build_with_worker(self) -> Result<(Pager, PagerWorker), PagerError> {
|
||||
let client = self.make_client()?;
|
||||
|
||||
let queue: Arc<PagerWorkerMessageQueue> = match self.queue_capacity {
|
||||
Some(capacity) => new_shared_queue_with_capacity(capacity),
|
||||
@@ -101,19 +99,14 @@ impl PagerBuilder {
|
||||
let shutdown = worker.shutdown_handle();
|
||||
let pager = Pager::with_queue(client, queue, shutdown);
|
||||
|
||||
(pager, worker)
|
||||
Ok((pager, worker))
|
||||
}
|
||||
|
||||
fn make_client(&self) -> PagerClient {
|
||||
let client_config = match self.client_config.clone() {
|
||||
Some(config) => config,
|
||||
None => {
|
||||
warn!("No pager backend configured. Using NoOp pager.");
|
||||
PagerClientConfig::NoOp
|
||||
}
|
||||
};
|
||||
fn make_client(&self) -> Result<PagerClient, PagerError> {
|
||||
let client_config = self.client_config.clone()
|
||||
.ok_or(PagerBuilderError::NoBackendConfigured)?;
|
||||
|
||||
PagerClient::new(client_config, self.application_name.clone(), self.environment.clone(), self.hostname.clone())
|
||||
Ok(PagerClient::new(client_config, self.application_name.clone(), self.environment.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,12 +146,12 @@ impl RootlyConfigBuilder {
|
||||
}
|
||||
|
||||
/// Shortcut: finish Rootly config and build without a worker.
|
||||
pub fn build(self) -> Pager {
|
||||
pub fn build(self) -> Result<Pager, PagerError> {
|
||||
self.done().build()
|
||||
}
|
||||
|
||||
/// Shortcut: finish Rootly config and build with a worker.
|
||||
pub fn build_with_worker(self) -> (Pager, PagerWorker) {
|
||||
pub fn build_with_worker(self) -> Result<(Pager, PagerWorker), PagerError> {
|
||||
self.done().build_with_worker()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use log::{debug, warn};
|
||||
use log::{error, info};
|
||||
|
||||
use rootly_client::creds::rootly_api_key::RootlyApiKey;
|
||||
use rootly_client::requests::create_alert::create_alert::{
|
||||
@@ -20,17 +20,11 @@ pub struct PagerClient {
|
||||
|
||||
/// Environment label (e.g. "production", "staging"). Inserted as a label on alerts.
|
||||
pub environment: Option<String>,
|
||||
|
||||
/// Hostname of the machine sending alerts. Inserted as a label on alerts.
|
||||
pub hostname: Option<String>,
|
||||
}
|
||||
|
||||
/// Configuration for the pager client backend.
|
||||
#[derive(Clone)]
|
||||
pub enum PagerClientConfig {
|
||||
/// No-op backend. All calls are silently ignored.
|
||||
NoOp,
|
||||
|
||||
/// Use Rootly as the paging backend.
|
||||
Rootly {
|
||||
api_key: RootlyApiKey,
|
||||
@@ -50,7 +44,7 @@ pub enum PagerClientConfig {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PageSentResult {
|
||||
/// A backend-specific identifier for the sent notification.
|
||||
pub id: Option<String>,
|
||||
pub id: String,
|
||||
|
||||
/// A short human-readable ID (if the backend provides one).
|
||||
pub short_id: Option<String>,
|
||||
@@ -61,25 +55,14 @@ impl PagerClient {
|
||||
client_config: PagerClientConfig,
|
||||
application_name: Option<String>,
|
||||
environment: Option<String>,
|
||||
hostname: Option<String>,
|
||||
) -> Self {
|
||||
Self { client_config, application_name, environment, hostname }
|
||||
Self { client_config, application_name, environment }
|
||||
}
|
||||
|
||||
pub fn is_noop(&self) -> bool {
|
||||
matches!(self.client_config, PagerClientConfig::NoOp)
|
||||
}
|
||||
|
||||
/// Send a page immediately. Returns `Ok(None)` for NoOp.
|
||||
pub async fn send_page(&self, notification: &NotificationDetails) -> Result<Option<PageSentResult>, PagerError> {
|
||||
/// Send a page immediately.
|
||||
pub async fn send_page(&self, notification: &NotificationDetails) -> Result<PageSentResult, PagerError> {
|
||||
match &self.client_config {
|
||||
PagerClientConfig::NoOp => {
|
||||
debug!("Pager no-op: would have sent page: {}", notification.summary);
|
||||
Ok(None)
|
||||
}
|
||||
PagerClientConfig::Rootly { .. } => {
|
||||
self.send_page_via_rootly(notification).await.map(Some)
|
||||
}
|
||||
PagerClientConfig::Rootly { .. } => self.send_page_via_rootly(notification).await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,15 +75,13 @@ impl PagerClient {
|
||||
alert_urgency_id,
|
||||
notification_target_type,
|
||||
notification_target_id,
|
||||
} = &self.client_config else {
|
||||
return Ok(PageSentResult { id: None, short_id: None });
|
||||
};
|
||||
} = &self.client_config;
|
||||
|
||||
let source = self.application_name
|
||||
.clone()
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
|
||||
debug!("Sending page via Rootly (source={}): {}", source, notification.summary);
|
||||
info!("Sending page via Rootly (source={}): {}", source, notification.summary);
|
||||
|
||||
let mut labels: Vec<(String, String)> = Vec::new();
|
||||
|
||||
@@ -108,23 +89,13 @@ impl PagerClient {
|
||||
labels.push(("environment".to_string(), env.clone()));
|
||||
}
|
||||
|
||||
if let Some(h) = &self.hostname {
|
||||
labels.push(("hostname".to_string(), h.clone()));
|
||||
}
|
||||
|
||||
let labels = if labels.is_empty() { None } else { Some(labels) };
|
||||
|
||||
// Enrich the description with hostname if both are present.
|
||||
let description = match (¬ification.description, &self.hostname) {
|
||||
(Some(desc), Some(h)) => Some(format!("{}\n\nHostname: {}", desc, h)),
|
||||
(desc, _) => desc.clone(),
|
||||
};
|
||||
|
||||
let result = create_alert(CreateAlertArgs {
|
||||
api_key: api_key.clone(),
|
||||
source,
|
||||
summary: notification.summary.clone(),
|
||||
description,
|
||||
description: notification.description.clone(),
|
||||
status: Some("triggered".to_string()),
|
||||
service_ids: None,
|
||||
group_ids: None,
|
||||
@@ -140,14 +111,14 @@ impl PagerClient {
|
||||
|
||||
match result {
|
||||
Ok(success) => {
|
||||
debug!("Page sent successfully via Rootly: id={}, short_id={:?}", success.id, success.short_id);
|
||||
info!("Page sent successfully via Rootly: id={}, short_id={:?}", success.id, success.short_id);
|
||||
Ok(PageSentResult {
|
||||
id: Some(success.id),
|
||||
id: success.id,
|
||||
short_id: success.short_id,
|
||||
})
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("Failed to send page via Rootly: {}", err);
|
||||
error!("Failed to send page via Rootly: {}", err);
|
||||
Err(PagerError::Service(PagerServiceError::RootlyError(err)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod pager_builder_error;
|
||||
pub mod pager_error;
|
||||
pub mod pager_service_error;
|
||||
pub mod pager_system_error;
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
/// Errors from building/configuring a Pager instance.
|
||||
#[derive(Debug)]
|
||||
pub enum PagerBuilderError {
|
||||
/// The pager was not configured with a backend.
|
||||
NoBackendConfigured,
|
||||
}
|
||||
|
||||
impl Error for PagerBuilderError {}
|
||||
|
||||
impl Display for PagerBuilderError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::NoBackendConfigured => write!(f, "No pager backend configured. Call .rootly() or .client_config() on PagerBuilder."),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use crate::error::pager_builder_error::PagerBuilderError;
|
||||
use crate::error::pager_service_error::PagerServiceError;
|
||||
use crate::error::pager_system_error::PagerSystemError;
|
||||
|
||||
/// Top-level error type for the pager library.
|
||||
#[derive(Debug)]
|
||||
pub enum PagerError {
|
||||
/// An error during Pager construction/configuration.
|
||||
Builder(PagerBuilderError),
|
||||
|
||||
/// A third-party paging service (Rootly, etc.) returned an error.
|
||||
Service(PagerServiceError),
|
||||
|
||||
@@ -19,12 +23,19 @@ impl Error for PagerError {}
|
||||
impl Display for PagerError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Builder(err) => write!(f, "PagerBuilderError: {}", err),
|
||||
Self::Service(err) => write!(f, "PagerServiceError: {}", err),
|
||||
Self::System(err) => write!(f, "PagerSystemError: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PagerBuilderError> for PagerError {
|
||||
fn from(err: PagerBuilderError) -> Self {
|
||||
Self::Builder(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PagerServiceError> for PagerError {
|
||||
fn from(err: PagerServiceError) -> Self {
|
||||
Self::Service(err)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
use log::{debug, error, info, warn};
|
||||
use log::{error, info, warn};
|
||||
|
||||
use crate::client::pager_client::PagerClient;
|
||||
use crate::worker::pager_worker_message_queue::PagerWorkerMessageQueue;
|
||||
@@ -67,7 +67,7 @@ impl PagerWorker {
|
||||
continue;
|
||||
}
|
||||
|
||||
debug!("Pager worker processing {} queued notification(s).", items.len());
|
||||
info!("Pager worker processing {} queued notification(s).", items.len());
|
||||
|
||||
for notification in &items {
|
||||
if self.shutdown.load(Ordering::Relaxed) {
|
||||
@@ -76,15 +76,12 @@ impl PagerWorker {
|
||||
}
|
||||
|
||||
match self.client.send_page(notification).await {
|
||||
Ok(Some(success)) => {
|
||||
debug!(
|
||||
"Pager worker sent page: id={:?}, summary={}",
|
||||
Ok(success) => {
|
||||
info!(
|
||||
"Pager worker sent page: id={}, summary={}",
|
||||
success.id, notification.summary
|
||||
);
|
||||
}
|
||||
Ok(None) => {
|
||||
// NoOp — already logged by the client.
|
||||
}
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Pager worker failed to send page for '{}': {}",
|
||||
|
||||
+1
-11
@@ -1,9 +1,7 @@
|
||||
use anyhow::anyhow;
|
||||
use chrono::{DateTime, Utc};
|
||||
use log::warn;
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty;
|
||||
use enums::by_table::wallet_ledger_entries::wallet_ledger_entry_type::WalletLedgerEntryType;
|
||||
use enums::common::job_status_plus::JobStatusPlus;
|
||||
use errors::AnyhowResult;
|
||||
@@ -21,10 +19,6 @@ pub struct UserJobForModerationResult {
|
||||
pub job_token: InferenceJobToken,
|
||||
pub wallet_ledger_entry_token: Option<WalletLedgerEntryToken>,
|
||||
pub wallet_ledger_entry_type: Option<WalletLedgerEntryType>,
|
||||
pub maybe_external_third_party: Option<InferenceJobExternalThirdParty>,
|
||||
pub maybe_external_third_party_id: Option<String>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub async fn list_user_jobs_for_moderation(
|
||||
@@ -42,11 +36,7 @@ SELECT
|
||||
j.on_success_result_entity_token as `on_success_result_media_token: tokens::tokens::media_files::MediaFileToken`,
|
||||
j.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,
|
||||
wle.token as `wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,
|
||||
wle.entry_type as `wallet_ledger_entry_type: enums::by_table::wallet_ledger_entries::wallet_ledger_entry_type::WalletLedgerEntryType`,
|
||||
j.maybe_external_third_party as `maybe_external_third_party: enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty`,
|
||||
j.maybe_external_third_party_id,
|
||||
j.created_at,
|
||||
j.updated_at
|
||||
wle.entry_type as `wallet_ledger_entry_type: enums::by_table::wallet_ledger_entries::wallet_ledger_entry_type::WalletLedgerEntryType`
|
||||
FROM generic_inference_jobs as j
|
||||
LEFT OUTER JOIN users as u
|
||||
ON j.maybe_creator_user_token = u.token
|
||||
|
||||
+1
-3
@@ -13,8 +13,6 @@ use tokens::tokens::users::UserToken;
|
||||
|
||||
use crate::helpers::boolean_converters::{i8_to_bool, nullable_i8_to_bool_default_false, nullable_i8_to_optional_bool};
|
||||
|
||||
const ADMIN_ROLE_SLUG: &str = "admin";
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct SessionUserRecord {
|
||||
pub user_token: UserToken,
|
||||
@@ -92,7 +90,7 @@ impl SessionUserRecord {
|
||||
}
|
||||
|
||||
pub fn is_mod(&self) -> bool {
|
||||
self.can_ban_users || self.user_role_slug == ADMIN_ROLE_SLUG
|
||||
self.can_ban_users
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ use strum::EnumIter;
|
||||
///
|
||||
/// YOU CAN ADD NEW VALUES, BUT DO NOT CHANGE EXISTING VALUES WITHOUT A MIGRATION STRATEGY.
|
||||
#[cfg_attr(test, derive(EnumIter, EnumCount))]
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd, Deserialize, Serialize, Default, utoipa::ToSchema)]
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd, Deserialize, Serialize, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum InferenceJobExternalThirdParty {
|
||||
/// Fal jobs
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
#[cfg(test)]
|
||||
use strum::EnumCount;
|
||||
#[cfg(test)]
|
||||
use strum::EnumIter;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::error::enum_error::EnumError;
|
||||
|
||||
/// Maximum serialized string length for database storage.
|
||||
pub const MAX_LENGTH: usize = 16;
|
||||
|
||||
/// Common quality levels for generation.
|
||||
///
|
||||
/// NB: Keep the max serialized length to 16 characters.
|
||||
#[cfg_attr(test, derive(EnumIter, EnumCount))]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CommonQuality {
|
||||
High,
|
||||
Medium,
|
||||
Low,
|
||||
}
|
||||
|
||||
impl_enum_display_and_debug_using_to_str!(CommonQuality);
|
||||
impl_mysql_enum_coders!(CommonQuality);
|
||||
impl_mysql_from_row!(CommonQuality);
|
||||
|
||||
impl CommonQuality {
|
||||
pub fn to_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::High => "high",
|
||||
Self::Medium => "medium",
|
||||
Self::Low => "low",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(value: &str) -> Result<Self, EnumError> {
|
||||
match value {
|
||||
"high" => Ok(Self::High),
|
||||
"medium" => Ok(Self::Medium),
|
||||
"low" => Ok(Self::Low),
|
||||
_ => Err(EnumError::CouldNotConvertFromString(value.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn all_variants() -> BTreeSet<Self> {
|
||||
BTreeSet::from([
|
||||
Self::High,
|
||||
Self::Medium,
|
||||
Self::Low,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::common::generation::common_quality::CommonQuality;
|
||||
use crate::common::generation::common_quality::MAX_LENGTH;
|
||||
use crate::test_helpers::assert_serialization;
|
||||
|
||||
mod explicit_checks {
|
||||
use super::*;
|
||||
use crate::error::enum_error::EnumError;
|
||||
|
||||
#[test]
|
||||
fn test_serialization() {
|
||||
assert_serialization(CommonQuality::High, "high");
|
||||
assert_serialization(CommonQuality::Medium, "medium");
|
||||
assert_serialization(CommonQuality::Low, "low");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_str() {
|
||||
assert_eq!(CommonQuality::High.to_str(), "high");
|
||||
assert_eq!(CommonQuality::Medium.to_str(), "medium");
|
||||
assert_eq!(CommonQuality::Low.to_str(), "low");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str() {
|
||||
assert_eq!(CommonQuality::from_str("high").unwrap(), CommonQuality::High);
|
||||
assert_eq!(CommonQuality::from_str("medium").unwrap(), CommonQuality::Medium);
|
||||
assert_eq!(CommonQuality::from_str("low").unwrap(), CommonQuality::Low);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str_err() {
|
||||
let result = CommonQuality::from_str("invalid");
|
||||
assert!(result.is_err());
|
||||
if let Err(EnumError::CouldNotConvertFromString(value)) = result {
|
||||
assert_eq!(value, "invalid");
|
||||
} else {
|
||||
panic!("Expected EnumError::CouldNotConvertFromString");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
let mut variants = CommonQuality::all_variants();
|
||||
assert_eq!(variants.len(), 3);
|
||||
assert_eq!(variants.pop_first(), Some(CommonQuality::High));
|
||||
assert_eq!(variants.pop_first(), Some(CommonQuality::Medium));
|
||||
assert_eq!(variants.pop_first(), Some(CommonQuality::Low));
|
||||
assert_eq!(variants.pop_first(), None);
|
||||
}
|
||||
}
|
||||
|
||||
mod mechanical_checks {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn variant_length() {
|
||||
use strum::IntoEnumIterator;
|
||||
assert_eq!(CommonQuality::all_variants().len(), CommonQuality::iter().len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip() {
|
||||
for variant in CommonQuality::all_variants() {
|
||||
assert_eq!(variant, CommonQuality::from_str(variant.to_str()).unwrap());
|
||||
assert_eq!(variant, CommonQuality::from_str(&format!("{}", variant)).unwrap());
|
||||
assert_eq!(variant, CommonQuality::from_str(&format!("{:?}", variant)).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialized_length_ok_for_database() {
|
||||
for variant in CommonQuality::all_variants() {
|
||||
let serialized = variant.to_str();
|
||||
assert!(serialized.len() > 0, "variant {:?} is too short", variant);
|
||||
assert!(serialized.len() <= MAX_LENGTH, "variant {:?} is too long via to_str()", variant);
|
||||
}
|
||||
for variant in CommonQuality::all_variants() {
|
||||
let json = serde_json::to_string(&variant).unwrap().replace('"', "");
|
||||
assert!(json.len() <= MAX_LENGTH, "variant {:?} is too long via JSON: {:?}", variant, json);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialized_names_must_not_contain_dots() {
|
||||
for variant in CommonQuality::all_variants() {
|
||||
let to_str_value = variant.to_str();
|
||||
assert!(!to_str_value.contains('.'), "to_str() for {:?} contains a dot: {:?}", variant, to_str_value);
|
||||
|
||||
let json_value = serde_json::to_string(&variant).unwrap().replace('"', "");
|
||||
assert!(!json_value.contains('.'), "JSON serialization for {:?} contains a dot: {:?}", variant, json_value);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialized_names_must_only_contain_lowercase_alphanumeric_and_underscore() {
|
||||
let valid_pattern = regex::Regex::new(r"^[a-z0-9_]+$").unwrap();
|
||||
|
||||
for variant in CommonQuality::all_variants() {
|
||||
let to_str_value = variant.to_str();
|
||||
assert!(valid_pattern.is_match(to_str_value),
|
||||
"to_str() for {:?} contains invalid characters: {:?} (only a-z, 0-9, _ allowed)", variant, to_str_value);
|
||||
|
||||
let json_value = serde_json::to_string(&variant).unwrap().replace('"', "");
|
||||
assert!(valid_pattern.is_match(&json_value),
|
||||
"JSON serialization for {:?} contains invalid characters: {:?} (only a-z, 0-9, _ allowed)", variant, json_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,17 +23,7 @@ pub enum CommonResolution {
|
||||
ThreeK,
|
||||
FourK,
|
||||
|
||||
/// Used by Nano Banana 2 (and others)
|
||||
HalfK,
|
||||
|
||||
/// Used by the Seedance family (and others)
|
||||
FourEightyP,
|
||||
|
||||
/// Used by the Seedance family (and others)
|
||||
SevenTwentyP,
|
||||
|
||||
/// Used by the Seedance family (and others)
|
||||
TenEightyP,
|
||||
// TODO: Add 480p(?), 720p(?), 1080p, etc. as there are some models that use these
|
||||
}
|
||||
|
||||
impl_enum_display_and_debug_using_to_str!(CommonResolution);
|
||||
@@ -47,10 +37,6 @@ impl CommonResolution {
|
||||
Self::TwoK => "two_k",
|
||||
Self::ThreeK => "three_k",
|
||||
Self::FourK => "four_k",
|
||||
Self::HalfK => "half_k",
|
||||
Self::FourEightyP => "four_eighty_p",
|
||||
Self::SevenTwentyP => "seven_twenty_p",
|
||||
Self::TenEightyP => "ten_eighty_p",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,10 +46,6 @@ impl CommonResolution {
|
||||
"two_k" => Ok(Self::TwoK),
|
||||
"three_k" => Ok(Self::ThreeK),
|
||||
"four_k" => Ok(Self::FourK),
|
||||
"half_k" => Ok(Self::HalfK),
|
||||
"four_eighty_p" => Ok(Self::FourEightyP),
|
||||
"seven_twenty_p" => Ok(Self::SevenTwentyP),
|
||||
"ten_eighty_p" => Ok(Self::TenEightyP),
|
||||
_ => Err(EnumError::CouldNotConvertFromString(value.to_string())),
|
||||
}
|
||||
}
|
||||
@@ -74,10 +56,6 @@ impl CommonResolution {
|
||||
Self::TwoK,
|
||||
Self::ThreeK,
|
||||
Self::FourK,
|
||||
Self::HalfK,
|
||||
Self::FourEightyP,
|
||||
Self::SevenTwentyP,
|
||||
Self::TenEightyP,
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -98,10 +76,6 @@ mod tests {
|
||||
assert_serialization(CommonResolution::TwoK, "two_k");
|
||||
assert_serialization(CommonResolution::ThreeK, "three_k");
|
||||
assert_serialization(CommonResolution::FourK, "four_k");
|
||||
assert_serialization(CommonResolution::HalfK, "half_k");
|
||||
assert_serialization(CommonResolution::FourEightyP, "four_eighty_p");
|
||||
assert_serialization(CommonResolution::SevenTwentyP, "seven_twenty_p");
|
||||
assert_serialization(CommonResolution::TenEightyP, "ten_eighty_p");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -110,10 +84,6 @@ mod tests {
|
||||
assert_eq!(CommonResolution::TwoK.to_str(), "two_k");
|
||||
assert_eq!(CommonResolution::ThreeK.to_str(), "three_k");
|
||||
assert_eq!(CommonResolution::FourK.to_str(), "four_k");
|
||||
assert_eq!(CommonResolution::HalfK.to_str(), "half_k");
|
||||
assert_eq!(CommonResolution::FourEightyP.to_str(), "four_eighty_p");
|
||||
assert_eq!(CommonResolution::SevenTwentyP.to_str(), "seven_twenty_p");
|
||||
assert_eq!(CommonResolution::TenEightyP.to_str(), "ten_eighty_p");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -122,10 +92,6 @@ mod tests {
|
||||
assert_eq!(CommonResolution::from_str("two_k").unwrap(), CommonResolution::TwoK);
|
||||
assert_eq!(CommonResolution::from_str("three_k").unwrap(), CommonResolution::ThreeK);
|
||||
assert_eq!(CommonResolution::from_str("four_k").unwrap(), CommonResolution::FourK);
|
||||
assert_eq!(CommonResolution::from_str("half_k").unwrap(), CommonResolution::HalfK);
|
||||
assert_eq!(CommonResolution::from_str("four_eighty_p").unwrap(), CommonResolution::FourEightyP);
|
||||
assert_eq!(CommonResolution::from_str("seven_twenty_p").unwrap(), CommonResolution::SevenTwentyP);
|
||||
assert_eq!(CommonResolution::from_str("ten_eighty_p").unwrap(), CommonResolution::TenEightyP);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -142,15 +108,11 @@ mod tests {
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
let mut variants = CommonResolution::all_variants();
|
||||
assert_eq!(variants.len(), 8);
|
||||
assert_eq!(variants.len(), 4);
|
||||
assert_eq!(variants.pop_first(), Some(CommonResolution::OneK));
|
||||
assert_eq!(variants.pop_first(), Some(CommonResolution::TwoK));
|
||||
assert_eq!(variants.pop_first(), Some(CommonResolution::ThreeK));
|
||||
assert_eq!(variants.pop_first(), Some(CommonResolution::FourK));
|
||||
assert_eq!(variants.pop_first(), Some(CommonResolution::HalfK));
|
||||
assert_eq!(variants.pop_first(), Some(CommonResolution::FourEightyP));
|
||||
assert_eq!(variants.pop_first(), Some(CommonResolution::SevenTwentyP));
|
||||
assert_eq!(variants.pop_first(), Some(CommonResolution::TenEightyP));
|
||||
assert_eq!(variants.pop_first(), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
pub mod common_aspect_ratio;
|
||||
pub mod common_generation_mode;
|
||||
pub mod common_image_model;
|
||||
pub mod common_quality;
|
||||
pub mod common_model_class;
|
||||
pub mod common_model_type;
|
||||
pub mod common_resolution;
|
||||
|
||||
@@ -65,9 +65,7 @@ memory_caching = { path = "../../../lib/caching/memory_caching" }
|
||||
migration = { path = "../../../schema/database/migration" }
|
||||
mimetypes.workspace = true
|
||||
mysql_queries.workspace = true
|
||||
pager.workspace = true
|
||||
password = { path = "../../../lib/password" }
|
||||
rootly_client.workspace = true
|
||||
primitives = { path = "../../../lib/data/primitives" }
|
||||
prompt_engineering = { path = "../../../lib/deprecated/machine_learning/prompt_engineering" }
|
||||
redis_caching = { path = "../../../lib/caching/redis_caching" }
|
||||
|
||||
@@ -8,10 +8,6 @@ COOKIE_SECURE=false
|
||||
COOKIE_DOMAIN=.jungle.horse
|
||||
WEBSITE_HOMEPAGE_REDIRECT=http://jungle.horse
|
||||
|
||||
# Paging
|
||||
ENABLE_PAGING=true
|
||||
ENABLE_PAGING_FOR_500S=true
|
||||
|
||||
# Test the rate limiter bypass (obviously these are dummy values)
|
||||
AI_STREAMER_USERNAMES="PewDiePie,xQc"
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ pub mod docs;
|
||||
pub mod email;
|
||||
pub mod error;
|
||||
pub mod http_server;
|
||||
pub mod startup;
|
||||
pub mod state;
|
||||
pub mod threads;
|
||||
pub mod util;
|
||||
|
||||
@@ -34,7 +34,6 @@ pub mod docs;
|
||||
pub mod email;
|
||||
pub mod error;
|
||||
pub mod http_server;
|
||||
pub mod startup;
|
||||
pub mod state;
|
||||
pub mod threads;
|
||||
pub mod util;
|
||||
|
||||
@@ -197,7 +197,6 @@ use enums::by_table::comments::comment_entity_type::CommentEntityType;
|
||||
use enums::by_table::featured_items::featured_item_entity_type::FeaturedItemEntityType;
|
||||
use enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
|
||||
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
|
||||
use enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty;
|
||||
use enums::by_table::media_files::media_file_animation_type::MediaFileAnimationType;
|
||||
use enums::by_table::media_files::media_file_class::MediaFileClass;
|
||||
use enums::by_table::media_files::media_file_engine_category::MediaFileEngineCategory;
|
||||
@@ -279,7 +278,6 @@ use artcraft_api_defs::subscriptions::get_session_subscription::*;
|
||||
use artcraft_api_defs::media_file::list_batch_generated_media_files::*;
|
||||
// Handler modules with locally-defined types
|
||||
use crate::http_server::endpoints::moderation::info::moderator_token_info_handler::*;
|
||||
use artcraft_api_defs::moderation::alerts::moderation_send_alert::*;
|
||||
use artcraft_api_defs::moderation::user::list_subscribing_users_by_signup_date::*;
|
||||
use artcraft_api_defs::moderation::user::list_users_by_signup_date::*;
|
||||
use artcraft_api_defs::moderation::user::user_lookup::*;
|
||||
@@ -469,7 +467,6 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
// TTS
|
||||
crate::http_server::endpoints::tts::enqueue_infer_f5_tts_handler::enqueue_infer_f5_tts_handler::enqueue_infer_f5_tts_handler,
|
||||
// Moderation
|
||||
crate::http_server::endpoints::moderation::alerts::moderation_send_alert_handler::moderation_send_alert_handler,
|
||||
crate::http_server::endpoints::moderation::info::moderator_token_info_handler::moderator_get_token_info_handler,
|
||||
crate::http_server::endpoints::moderation::user::moderator_list_subscribing_users_by_signup_date::moderator_list_subscribing_users_by_signup_date_handler,
|
||||
crate::http_server::endpoints::moderation::user::moderator_list_users_by_signup_date::moderator_list_users_by_signup_date_handler,
|
||||
@@ -664,8 +661,6 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
EditUserFeatureFlagsError,
|
||||
EditUserFeatureFlagsOption,
|
||||
EditUserFeatureFlagsRequest,
|
||||
ModerationSendAlertRequest,
|
||||
ModerationSendAlertResponse,
|
||||
ModeratorListSubscribingUsersBySignupDateRequest,
|
||||
ModeratorListSubscribingUsersBySignupDateResponse,
|
||||
ModeratorListSubscribingUsersBySignupDateEntry,
|
||||
@@ -681,7 +676,6 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
ListUserJobsPathInfo,
|
||||
ListUserJobsResponse,
|
||||
ListUserJobsEntry,
|
||||
InferenceJobExternalThirdParty,
|
||||
ListUserWalletsPathInfo,
|
||||
ListUserWalletsResponse,
|
||||
ListUserWalletsEntry,
|
||||
|
||||
-4
@@ -168,9 +168,5 @@ fn map_resolution(res: CommonResolution) -> RouterResolution {
|
||||
CommonResolution::TwoK => RouterResolution::TwoK,
|
||||
CommonResolution::ThreeK => RouterResolution::ThreeK,
|
||||
CommonResolution::FourK => RouterResolution::FourK,
|
||||
CommonResolution::HalfK => RouterResolution::HalfK,
|
||||
CommonResolution::FourEightyP => RouterResolution::FourEightyP,
|
||||
CommonResolution::SevenTwentyP => RouterResolution::SevenTwentyP,
|
||||
CommonResolution::TenEightyP => RouterResolution::TenEightyP,
|
||||
}
|
||||
}
|
||||
|
||||
-4
@@ -178,9 +178,5 @@ fn map_resolution(res: CommonResolution) -> RouterResolution {
|
||||
CommonResolution::TwoK => RouterResolution::TwoK,
|
||||
CommonResolution::ThreeK => RouterResolution::ThreeK,
|
||||
CommonResolution::FourK => RouterResolution::FourK,
|
||||
CommonResolution::HalfK => RouterResolution::HalfK,
|
||||
CommonResolution::FourEightyP => RouterResolution::FourEightyP,
|
||||
CommonResolution::SevenTwentyP => RouterResolution::SevenTwentyP,
|
||||
CommonResolution::TenEightyP => RouterResolution::TenEightyP,
|
||||
}
|
||||
}
|
||||
|
||||
-11
@@ -46,7 +46,6 @@ use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use url::Url;
|
||||
use url_utils::extension::extract_extension_from_url::{extract_extension_from_url, ExtractExtensions};
|
||||
use pager::notification::notification_details::NotificationDetails;
|
||||
use crate::http_server::session::lookup::user_session_feature_flags::UserSessionFeatureFlags;
|
||||
|
||||
// ======================== Result of a successful generation ========================
|
||||
@@ -220,11 +219,6 @@ pub async fn seedance_2p0_multi_function_video_gen_handler(
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("Regular session fallback also failed for user {:?}: {:?}", user_token, err);
|
||||
if let Err(page_err) = server_state.pager.enqueue_page(
|
||||
NotificationDetails::from_error_with_context("Seedance 2.0 generation failed (whitelist + fallback)", &err)
|
||||
) {
|
||||
warn!("Failed to enqueue pager alert: {:?}", page_err);
|
||||
}
|
||||
refund_wallet_after_api_failure(&deduction_result.ledger_entry_token, &mut mysql_connection).await?;
|
||||
return Err(CommonWebError::ServerError);
|
||||
}
|
||||
@@ -251,11 +245,6 @@ pub async fn seedance_2p0_multi_function_video_gen_handler(
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
warn!("Error calling seedance2pro generate_video: {:?}", err);
|
||||
if let Err(page_err) = server_state.pager.enqueue_page(
|
||||
NotificationDetails::from_error_with_context("Seedance 2.0 generation failed", &err)
|
||||
) {
|
||||
warn!("Failed to enqueue pager alert: {:?}", page_err);
|
||||
}
|
||||
refund_wallet_after_api_failure(&deduction_result.ledger_entry_token, &mut mysql_connection).await?;
|
||||
return Err(CommonWebError::ServerError);
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pub mod moderation_send_alert_handler;
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::Json;
|
||||
use actix_web::{web, HttpRequest};
|
||||
use log::warn;
|
||||
|
||||
use artcraft_api_defs::moderation::alerts::moderation_send_alert::{
|
||||
ModerationSendAlertRequest,
|
||||
ModerationSendAlertResponse,
|
||||
};
|
||||
use pager::client::pager::Pager;
|
||||
use pager::notification::notification_details::NotificationDetails;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, UseDatabase};
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
/// Send a test alert via the pager system. Moderators only.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
tag = "Moderation",
|
||||
path = "/v1/moderation/alerts/send",
|
||||
request_body = ModerationSendAlertRequest,
|
||||
responses(
|
||||
(status = 200, description = "Success", body = ModerationSendAlertResponse),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 500, description = "Server error"),
|
||||
),
|
||||
)]
|
||||
pub async fn moderation_send_alert_handler(
|
||||
http_request: HttpRequest,
|
||||
request: Json<ModerationSendAlertRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<ModerationSendAlertResponse>, CommonWebError> {
|
||||
|
||||
let _user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
|
||||
.await
|
||||
.map_err(|_| CommonWebError::NotAuthorized)?;
|
||||
|
||||
let title = request.title.clone()
|
||||
.unwrap_or_else(|| "Test Moderation Alert".to_string());
|
||||
|
||||
let description = request.description.clone()
|
||||
.unwrap_or_else(|| "This is a test moderation alert.".to_string());
|
||||
|
||||
let notification = NotificationDetails::with_summary_and_description(title, description);
|
||||
|
||||
server_state.pager
|
||||
.enqueue_page(notification)
|
||||
.map_err(|err| {
|
||||
warn!("moderation_send_alert error: {:?}", err);
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
Ok(Json(ModerationSendAlertResponse {
|
||||
success: true,
|
||||
}))
|
||||
}
|
||||
-4
@@ -56,10 +56,6 @@ pub async fn list_user_jobs_handler(
|
||||
job_token: row.job_token,
|
||||
wallet_ledger_entry_token: row.wallet_ledger_entry_token,
|
||||
wallet_ledger_entry_type: row.wallet_ledger_entry_type,
|
||||
maybe_external_third_party: row.maybe_external_third_party,
|
||||
maybe_external_third_party_id: row.maybe_external_third_party_id,
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at,
|
||||
}).collect();
|
||||
|
||||
Ok(Json(ListUserJobsResponse {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod alerts;
|
||||
pub mod info;
|
||||
pub mod ip_bans;
|
||||
pub mod jobs;
|
||||
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
use pager::client::pager::Pager;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use super::enqueue_alert::enqueue_alert;
|
||||
|
||||
/// Check `CommonWebError` and alert on server errors.
|
||||
/// Returns `true` if the error was handled (alerted or intentionally skipped).
|
||||
pub(super) fn check_common_web_error(
|
||||
pager: &Pager,
|
||||
method: &str,
|
||||
path: &str,
|
||||
error: &CommonWebError,
|
||||
) -> bool {
|
||||
match error {
|
||||
CommonWebError::ServerError => {
|
||||
enqueue_alert(
|
||||
pager,
|
||||
format!("CommonWebError::ServerError on {} {}", method, path),
|
||||
format!(
|
||||
"A CommonWebError::ServerError was returned.\n\n\
|
||||
Endpoint: {} {}\n\
|
||||
Error: {:?}\n\
|
||||
Time: {}",
|
||||
method, path, error,
|
||||
chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC"),
|
||||
),
|
||||
);
|
||||
true
|
||||
}
|
||||
// Don't alert on client errors (400, 401, 404, 402).
|
||||
CommonWebError::BadInputWithSimpleMessage(_) => true,
|
||||
CommonWebError::NotFound => true,
|
||||
CommonWebError::NotAuthorized => true,
|
||||
CommonWebError::PaymentRequired => true,
|
||||
}
|
||||
}
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
use pager::client::pager::Pager;
|
||||
|
||||
use super::enqueue_alert::enqueue_alert;
|
||||
|
||||
/// Fallback alerting based on HTTP status code when no typed error matched.
|
||||
pub(super) fn check_status_code_fallback(
|
||||
pager: &Pager,
|
||||
method: &str,
|
||||
path: &str,
|
||||
status_code: u16,
|
||||
) {
|
||||
match status_code {
|
||||
500 => {
|
||||
enqueue_alert(
|
||||
pager,
|
||||
format!("HTTP 500: {} {}", method, path),
|
||||
format!(
|
||||
"An untyped HTTP 500 response was returned (no typed error matched).\n\n\
|
||||
Endpoint: {} {}\n\
|
||||
Time: {}",
|
||||
method, path,
|
||||
chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC"),
|
||||
),
|
||||
);
|
||||
}
|
||||
// Add more status-based rules here:
|
||||
// 503 => { enqueue_alert(...); }
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
use log::{debug, warn};
|
||||
|
||||
use pager::client::pager::Pager;
|
||||
use pager::notification::notification_details::NotificationDetails;
|
||||
|
||||
pub(super) fn enqueue_alert(pager: &Pager, summary: String, description: String) {
|
||||
let notification = NotificationDetails::with_summary_and_description(summary, description);
|
||||
|
||||
if let Err(err) = pager.enqueue_page(notification) {
|
||||
warn!("Error alerting middleware: failed to enqueue page: {:?}", err);
|
||||
} else {
|
||||
debug!("Error alerting middleware: enqueued alert");
|
||||
}
|
||||
}
|
||||
-153
@@ -1,153 +0,0 @@
|
||||
use std::future::{Future, Ready, ready};
|
||||
use std::pin::Pin;
|
||||
|
||||
use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform};
|
||||
use actix_web::Error;
|
||||
|
||||
use pager::client::pager::Pager;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::state::flags::paging_flags::PagingFlags;
|
||||
use super::check_common_web_error::check_common_web_error;
|
||||
use super::check_status_code_fallback::check_status_code_fallback;
|
||||
|
||||
// ======================== Transform (factory) ========================
|
||||
|
||||
/// Middleware that intercepts error responses and enqueues pager alerts.
|
||||
///
|
||||
/// Inspects errors in two ways:
|
||||
/// 1. **Typed error matching** via downcast (e.g. `CommonWebError::ServerError`)
|
||||
/// 2. **Status code fallback** for untyped 500s that slip through
|
||||
///
|
||||
/// Both `PagingFlags.is_paging_enabled` and `PagingFlags.is_paging_for_500s_enabled`
|
||||
/// must be true for any alerting to fire. Otherwise the middleware is a passthrough.
|
||||
///
|
||||
/// To add new error types, create a new `check_*.rs` matcher module and
|
||||
/// add a downcast branch in `check_ok_response_for_alerts` / `check_err_for_alerts`.
|
||||
#[derive(Clone)]
|
||||
pub struct ErrorAlertingMiddleware {
|
||||
pager: Pager,
|
||||
paging_flags: PagingFlags,
|
||||
}
|
||||
|
||||
impl ErrorAlertingMiddleware {
|
||||
pub fn new(pager: Pager, paging_flags: PagingFlags) -> Self {
|
||||
Self { pager, paging_flags }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, B> Transform<S, ServiceRequest> for ErrorAlertingMiddleware
|
||||
where
|
||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
|
||||
S::Future: 'static,
|
||||
B: 'static,
|
||||
{
|
||||
type Response = ServiceResponse<B>;
|
||||
type Error = Error;
|
||||
type InitError = ();
|
||||
type Transform = ErrorAlertingService<S>;
|
||||
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
||||
|
||||
fn new_transform(&self, service: S) -> Self::Future {
|
||||
ready(Ok(ErrorAlertingService {
|
||||
service,
|
||||
pager: self.pager.clone(),
|
||||
paging_flags: self.paging_flags.clone(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== Service (per-request) ========================
|
||||
|
||||
pub struct ErrorAlertingService<S> {
|
||||
service: S,
|
||||
pager: Pager,
|
||||
paging_flags: PagingFlags,
|
||||
}
|
||||
|
||||
impl<S, B> Service<ServiceRequest> for ErrorAlertingService<S>
|
||||
where
|
||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
|
||||
S::Future: 'static,
|
||||
B: 'static,
|
||||
{
|
||||
type Response = ServiceResponse<B>;
|
||||
type Error = Error;
|
||||
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
|
||||
|
||||
actix_service::forward_ready!(service);
|
||||
|
||||
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||
// Fast path: if paging is disabled, skip all inspection.
|
||||
if !self.paging_flags.is_paging_enabled || !self.paging_flags.is_paging_for_500s_enabled {
|
||||
return Box::pin(self.service.call(req));
|
||||
}
|
||||
|
||||
let method = req.method().to_string();
|
||||
let path = req.path().to_string();
|
||||
let pager = self.pager.clone();
|
||||
let fut = self.service.call(req);
|
||||
|
||||
Box::pin(async move {
|
||||
match fut.await {
|
||||
Ok(res) => {
|
||||
check_ok_response_for_alerts(&pager, &method, &path, &res);
|
||||
Ok(res)
|
||||
}
|
||||
Err(err) => {
|
||||
check_err_for_alerts(&pager, &method, &path, &err);
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== Response inspection ========================
|
||||
|
||||
/// Inspect a successful `ServiceResponse` that may contain an error response.
|
||||
///
|
||||
/// Actix-web converts `ResponseError` types into HTTP responses and stashes
|
||||
/// the original error in `response.error()`. We can downcast from there.
|
||||
fn check_ok_response_for_alerts<B>(
|
||||
pager: &Pager,
|
||||
method: &str,
|
||||
path: &str,
|
||||
response: &ServiceResponse<B>,
|
||||
) {
|
||||
let status = response.status();
|
||||
|
||||
// Try to get the original typed error from the response (if it came from ResponseError).
|
||||
if let Some(err) = response.response().error() {
|
||||
// --- Typed matchers (add new check_*.rs modules and downcast branches here) ---
|
||||
|
||||
if let Some(common_err) = err.as_error::<CommonWebError>() {
|
||||
if check_common_web_error(pager, method, path, common_err) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Status code fallback ---
|
||||
check_status_code_fallback(pager, method, path, status.as_u16());
|
||||
}
|
||||
|
||||
/// Inspect an `actix_web::Error` returned from the handler or inner middleware.
|
||||
fn check_err_for_alerts(
|
||||
pager: &Pager,
|
||||
method: &str,
|
||||
path: &str,
|
||||
err: &Error,
|
||||
) {
|
||||
// --- Typed matchers (add new check_*.rs modules and downcast branches here) ---
|
||||
|
||||
if let Some(common_err) = err.as_error::<CommonWebError>() {
|
||||
if check_common_web_error(pager, method, path, common_err) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Status code fallback ---
|
||||
let status = err.as_response_error().status_code();
|
||||
check_status_code_fallback(pager, method, path, status.as_u16());
|
||||
}
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
mod check_common_web_error;
|
||||
mod check_status_code_fallback;
|
||||
mod enqueue_alert;
|
||||
|
||||
pub mod error_alerting_middleware;
|
||||
@@ -1,2 +1 @@
|
||||
pub mod error_alerting_middleware;
|
||||
pub mod pushback_filter_middleware;
|
||||
|
||||
-7
@@ -35,7 +35,6 @@ use crate::http_server::endpoints::moderation::user::moderator_list_subscribing_
|
||||
use crate::http_server::endpoints::moderation::user::moderator_list_users_by_signup_date::moderator_list_users_by_signup_date_handler;
|
||||
use crate::http_server::endpoints::moderation::user::moderator_user_lookup_by_stripe_customer_id_handler::moderator_user_lookup_by_stripe_customer_id_handler;
|
||||
use crate::http_server::endpoints::moderation::user::moderator_user_lookup_handler::moderator_user_lookup_handler;
|
||||
use crate::http_server::endpoints::moderation::alerts::moderation_send_alert_handler::moderation_send_alert_handler;
|
||||
use crate::http_server::endpoints::moderation::user_feature_flags::edit_user_feature_flags_handler::edit_user_feature_flags_handler;
|
||||
|
||||
pub fn add_moderator_routes<T, B> (app: App<T>) -> App<T>
|
||||
@@ -50,12 +49,6 @@ pub fn add_moderator_routes<T, B> (app: App<T>) -> App<T>
|
||||
>,
|
||||
{
|
||||
app.service(web::scope("/v1/moderation")
|
||||
.service(web::scope("/alerts")
|
||||
.service(web::resource("/send")
|
||||
.route(web::post().to(moderation_send_alert_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
)
|
||||
.service(web::resource("/user_feature_flags/{username_or_token}")
|
||||
.route(web::post().to(edit_user_feature_flags_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@ pub async fn require_moderator(
|
||||
}
|
||||
};
|
||||
|
||||
if !user_session.is_mod() {
|
||||
if !user_session.can_ban_users {
|
||||
warn!("user is not a moderator: {:?}", user_session.user_token.as_str());
|
||||
return Err(RequireModeratorError::NotAuthorized);
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ use elasticsearch::http::transport::Transport;
|
||||
use elasticsearch::Elasticsearch;
|
||||
use errors::AnyhowResult;
|
||||
use fal_client::creds::fal_api_key::FalApiKey;
|
||||
use log::{info, warn};
|
||||
use log::info;
|
||||
use memory_caching::arc_ttl_sieve::ArcTtlSieve;
|
||||
use memory_caching::single_item_ttl_cache::SingleItemTtlCache;
|
||||
use mysql_queries::mediators::badge_granter::BadgeGranter;
|
||||
@@ -72,7 +72,6 @@ use crate::configs::app_startup::redis_rate_limiters::configure_redis_rate_limit
|
||||
use crate::configs::connect_to_database::connect_to_database;
|
||||
use crate::configs::static_api_tokens::StaticApiTokenSet;
|
||||
use crate::http_server::cookies::anonymous_visitor_tracking::avt_cookie_manager::AvtCookieManager;
|
||||
use crate::http_server::middleware::error_alerting_middleware::error_alerting_middleware::ErrorAlertingMiddleware;
|
||||
use crate::http_server::middleware::pushback_filter_middleware::PushbackFilter;
|
||||
use crate::http_server::routes::add_routes::add_routes;
|
||||
use crate::http_server::session::session_checker::SessionChecker;
|
||||
@@ -80,7 +79,6 @@ use crate::http_server::web_utils::handle_multipart_error::handle_multipart_erro
|
||||
use crate::http_server::web_utils::scoped_temp_dir_creator::ScopedTempDirCreator;
|
||||
use crate::state::certs::google_sign_in_cert::GoogleSignInCert;
|
||||
use crate::state::memory_cache::model_token_to_info_cache::ModelTokenToInfoCache;
|
||||
use crate::startup::build_pager::build_pager;
|
||||
use crate::state::server_state::{DurableInMemoryCaches, EnvConfig, EphemeralInMemoryCaches, FalData, InMemoryCaches, OpenAiData, ResendData, Seedance2ProData, ServerInfo, ServerState, StaticFeatureFlags, StripeSettings, TrollBans, WorldLabsData};
|
||||
use crate::threads::db_health_checker_thread::db_health_check_status::HealthCheckStatus;
|
||||
use crate::threads::db_health_checker_thread::db_health_checker_thread::db_health_checker_thread;
|
||||
@@ -95,7 +93,6 @@ pub mod configs;
|
||||
pub mod email;
|
||||
pub mod error;
|
||||
pub mod http_server;
|
||||
pub mod startup;
|
||||
pub mod state;
|
||||
pub mod threads;
|
||||
pub mod util;
|
||||
@@ -331,21 +328,6 @@ async fn main() -> AnyhowResult<()> {
|
||||
poll_model_token_info_thread(model_token_info_cache2, mysql_pool5).await;
|
||||
});
|
||||
|
||||
let server_environment = ServerEnvironment::from_str(&easyenv::get_env_string_required("SERVER_ENVIRONMENT")?)
|
||||
.ok_or(anyhow!("invalid server environment"))?;
|
||||
|
||||
let server_environment_typed = match server_environment {
|
||||
ServerEnvironment::Production => server_environment::ServerEnvironment::Production,
|
||||
ServerEnvironment::Development => server_environment::ServerEnvironment::Development,
|
||||
};
|
||||
|
||||
let (pager, pager_worker, paging_flags) = build_pager(server_environment_typed, &server_hostname);
|
||||
|
||||
info!("Spawning pager worker thread.");
|
||||
|
||||
tokio_runtime.spawn(async move {
|
||||
pager_worker.run().await;
|
||||
});
|
||||
|
||||
let stripe_configs = StripeConfig {
|
||||
checkout: StripeCheckoutConfigs {
|
||||
@@ -363,6 +345,9 @@ async fn main() -> AnyhowResult<()> {
|
||||
},
|
||||
};
|
||||
|
||||
let server_environment = ServerEnvironment::from_str(&easyenv::get_env_string_required("SERVER_ENVIRONMENT")?)
|
||||
.ok_or(anyhow!("invalid server environment"))?;
|
||||
|
||||
let service_feature_flags = StaticFeatureFlags {
|
||||
// Permanent (control plane / safety) flags : messaging
|
||||
maybe_status_alert_category: easyenv::get_env_string_optional("FF_STATUS_ALERT_CATEGORY"),
|
||||
@@ -392,9 +377,6 @@ async fn main() -> AnyhowResult<()> {
|
||||
// Disable voice features
|
||||
disable_tts: easyenv::get_env_bool_or_default("DISABLE_TTS", false),
|
||||
disable_voice_conversion: easyenv::get_env_bool_or_default("DISABLE_VOICE_CONVERSION", false),
|
||||
|
||||
// Paging
|
||||
paging: paging_flags,
|
||||
};
|
||||
|
||||
let third_party_url_redirector = ThirdPartyUrlRedirector::new(server_environment);
|
||||
@@ -409,6 +391,11 @@ async fn main() -> AnyhowResult<()> {
|
||||
.unwrap_or(String::from("unknown"))
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
let server_environment_typed = match server_environment {
|
||||
ServerEnvironment::Production => server_environment::ServerEnvironment::Production,
|
||||
ServerEnvironment::Development => server_environment::ServerEnvironment::Development,
|
||||
};
|
||||
|
||||
let fal_api_key = FalApiKey::new(easyenv::get_env_string_required("FAL_API_KEY")?);
|
||||
let fal_webhook_url = easyenv::get_env_string_required("FAL_WEBHOOK_URL")?;
|
||||
@@ -486,7 +473,6 @@ async fn main() -> AnyhowResult<()> {
|
||||
worldlabs: WorldLabsData {
|
||||
api_key: worldlabs_api_key,
|
||||
},
|
||||
pager,
|
||||
caches: InMemoryCaches {
|
||||
durable: DurableInMemoryCaches {
|
||||
model_token_info: model_token_info_cache,
|
||||
@@ -629,15 +615,6 @@ pub async fn serve(server_state: ServerState) -> AnyhowResult<()>
|
||||
ServerEnvironment::Production => server_environment::ServerEnvironment::Production,
|
||||
};
|
||||
|
||||
let paging_flags_for_middleware = server_state.flags.paging.clone();
|
||||
let pager_for_middleware = server_state.pager.clone();
|
||||
let enable_error_alerting = paging_flags_for_middleware.is_paging_enabled
|
||||
&& paging_flags_for_middleware.is_paging_for_500s_enabled;
|
||||
|
||||
if enable_error_alerting {
|
||||
info!("Error alerting middleware is ENABLED (ENABLE_PAGING=true, ENABLE_PAGING_FOR_500S=true).");
|
||||
}
|
||||
|
||||
let server_state_arc = web::Data::new(Arc::new(server_state));
|
||||
|
||||
let disabled_endpoints = read_disabled_endpoints();
|
||||
@@ -675,7 +652,6 @@ pub async fn serve(server_state: ServerState) -> AnyhowResult<()>
|
||||
.app_data(web::Data::new(server_state_arc.stripe.clone().client.clone()))
|
||||
.app_data(web::Data::new(server_state_arc.third_party_url_redirector))
|
||||
.app_data(web::Data::new(server_state_arc.google_sign_in_cert.clone()))
|
||||
.app_data(web::Data::new(server_state_arc.pager.clone()))
|
||||
.app_data(web::Data::new(old_server_environment))
|
||||
.app_data(web::Data::new(new_server_environment))
|
||||
.app_data(web::Data::from(product_lookup)) // NB: Data::from(Arc<T>) for dynamic dispatch
|
||||
@@ -695,10 +671,6 @@ pub async fn serve(server_state: ServerState) -> AnyhowResult<()>
|
||||
.wrap(DefaultHeaders::new()
|
||||
.header("X-Backend-Hostname", &hostname)
|
||||
.header("X-Build-Sha", server_state_arc.server_info.build_sha.clone()))
|
||||
.wrap(middleware::Condition::new(
|
||||
enable_error_alerting,
|
||||
ErrorAlertingMiddleware::new(pager_for_middleware.clone(), paging_flags_for_middleware.clone()),
|
||||
))
|
||||
.wrap(PushbackFilter::new(&server_state_arc.flags.clone()))
|
||||
.wrap(DisabledEndpointFilter::new(disabled_endpoints.clone()))
|
||||
.wrap(BannedIpFilter::new(ip_ban_list))
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
use log::{info, warn};
|
||||
use pager::client::pager::Pager;
|
||||
use pager::client::pager_builder::PagerBuilder;
|
||||
use pager::worker::pager_worker::PagerWorker;
|
||||
use rootly_client::creds::rootly_api_key::RootlyApiKey;
|
||||
|
||||
use crate::state::flags::paging_flags::PagingFlags;
|
||||
|
||||
pub fn build_pager(
|
||||
server_environment: server_environment::ServerEnvironment,
|
||||
hostname: &str,
|
||||
) -> (Pager, PagerWorker, PagingFlags) {
|
||||
let paging_flags = PagingFlags::from_env();
|
||||
|
||||
info!("Paging flags: {:?}", paging_flags);
|
||||
|
||||
let environment = if server_environment.is_deployed_in_production() {
|
||||
"production"
|
||||
} else {
|
||||
"development"
|
||||
};
|
||||
|
||||
let builder = PagerBuilder::new()
|
||||
.application_name("storyteller-web".to_string())
|
||||
.environment(environment.to_string())
|
||||
.hostname(hostname.to_string());
|
||||
|
||||
// If paging is globally disabled, use a NoOp pager regardless of API key.
|
||||
if !paging_flags.is_paging_enabled {
|
||||
warn!("ENABLE_PAGING is false. Pager will be NoOp.");
|
||||
let (pager, worker) = builder.build_with_worker();
|
||||
return (pager, worker, paging_flags);
|
||||
}
|
||||
|
||||
let maybe_api_key = easyenv::get_env_string_optional("ROOTLY_API_KEY");
|
||||
|
||||
let (pager, worker) = match maybe_api_key {
|
||||
Some(api_key) => {
|
||||
info!("Rootly API key found. Configuring pager with Rootly backend.");
|
||||
build_rootly_pager(builder, api_key)
|
||||
}
|
||||
None => {
|
||||
warn!("ROOTLY_API_KEY not set. Pager will not send real pages.");
|
||||
builder.build_with_worker()
|
||||
}
|
||||
};
|
||||
|
||||
(pager, worker, paging_flags)
|
||||
}
|
||||
|
||||
fn build_rootly_pager(builder: PagerBuilder, api_key: String) -> (Pager, PagerWorker) {
|
||||
let mut rootly_builder = builder
|
||||
.rootly(RootlyApiKey::new(api_key));
|
||||
|
||||
if let Some(urgency_id) = easyenv::get_env_string_optional("ROOTLY_ALERT_URGENCY_ID") {
|
||||
rootly_builder = rootly_builder.alert_urgency_id(urgency_id);
|
||||
}
|
||||
|
||||
let target_type = easyenv::get_env_string_optional("ROOTLY_NOTIFICATION_TARGET_TYPE");
|
||||
let target_id = easyenv::get_env_string_optional("ROOTLY_NOTIFICATION_TARGET_ID");
|
||||
|
||||
if let (Some(t_type), Some(t_id)) = (target_type, target_id) {
|
||||
rootly_builder = rootly_builder.notification_target(t_type, t_id);
|
||||
}
|
||||
|
||||
rootly_builder.build_with_worker()
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod build_pager;
|
||||
@@ -1 +0,0 @@
|
||||
pub mod paging_flags;
|
||||
@@ -1,21 +0,0 @@
|
||||
/// Flags controlling the pager system.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PagingFlags {
|
||||
/// Master switch for paging. If false, no pages are sent.
|
||||
/// Env var: ENABLE_PAGING (default: false)
|
||||
pub is_paging_enabled: bool,
|
||||
|
||||
/// If true, the error alerting middleware will enqueue pages for HTTP 500s.
|
||||
/// Requires `is_paging_enabled` to also be true.
|
||||
/// Env var: ENABLE_PAGING_FOR_500S (default: false)
|
||||
pub is_paging_for_500s_enabled: bool,
|
||||
}
|
||||
|
||||
impl PagingFlags {
|
||||
pub fn from_env() -> Self {
|
||||
Self {
|
||||
is_paging_enabled: easyenv::get_env_bool_or_default("ENABLE_PAGING", false),
|
||||
is_paging_for_500s_enabled: easyenv::get_env_bool_or_default("ENABLE_PAGING_FOR_500S", false),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
pub mod cached_queries;
|
||||
pub mod flags;
|
||||
pub mod certs;
|
||||
pub mod memory_cache;
|
||||
pub mod server_state;
|
||||
@@ -9,7 +9,6 @@ use elasticsearch::Elasticsearch;
|
||||
use fal_client::creds::fal_api_key::FalApiKey;
|
||||
use memory_caching::arc_ttl_sieve::ArcTtlSieve;
|
||||
use memory_caching::single_item_ttl_cache::SingleItemTtlCache;
|
||||
use pager::client::pager::Pager;
|
||||
use mysql_queries::mediators::badge_granter::BadgeGranter;
|
||||
use mysql_queries::mediators::firehose_publisher::FirehosePublisher;
|
||||
use mysql_queries::queries::generic_inference::web::get_pending_inference_job_count::InferenceQueueLengthResult;
|
||||
@@ -33,7 +32,6 @@ use crate::http_server::endpoints::stats::result_transformer::CacheableQueueStat
|
||||
use crate::http_server::endpoints::tts::list_tts_models::TtsModelRecordForResponse;
|
||||
use crate::http_server::endpoints::voice_conversion::list_voice_conversion_models_handler::VoiceConversionModel;
|
||||
use crate::http_server::session::session_checker::SessionChecker;
|
||||
use crate::state::flags::paging_flags::PagingFlags;
|
||||
use crate::http_server::web_utils::redis_rate_limiter::RedisRateLimiter;
|
||||
use crate::http_server::web_utils::scoped_temp_dir_creator::ScopedTempDirCreator;
|
||||
use crate::state::certs::google_sign_in_cert::GoogleSignInCert;
|
||||
@@ -102,8 +100,6 @@ pub struct ServerState {
|
||||
|
||||
pub worldlabs: WorldLabsData,
|
||||
|
||||
pub pager: Pager,
|
||||
|
||||
/// Where to store audio uploads for w2l
|
||||
pub audio_uploads_bucket_root: String,
|
||||
|
||||
@@ -299,9 +295,6 @@ pub struct StaticFeatureFlags {
|
||||
/// Disable voice conversion endpoints
|
||||
/// If true, respond with 429.
|
||||
pub disable_voice_conversion: bool,
|
||||
|
||||
/// Paging system flags.
|
||||
pub paging: PagingFlags,
|
||||
}
|
||||
|
||||
/// Instead of top level service denial, these are bans against entities that instead return
|
||||
|
||||
@@ -271,8 +271,8 @@ export default function CreateImage() {
|
||||
try {
|
||||
const imageMediaTokens = selectedModel.canUseImagePrompt
|
||||
? referenceImages
|
||||
.map((img) => img.mediaToken)
|
||||
.filter((t) => t.length > 0)
|
||||
.map((img) => img.mediaToken)
|
||||
.filter((t) => t.length > 0)
|
||||
: undefined;
|
||||
|
||||
const result = await enqueueImageGeneration({
|
||||
@@ -476,7 +476,7 @@ export default function CreateImage() {
|
||||
length: Math.max(
|
||||
0,
|
||||
4 -
|
||||
Math.max(1, Math.min(4, batch.requestedCount)),
|
||||
Math.max(1, Math.min(4, batch.requestedCount)),
|
||||
),
|
||||
}).map((_, i) => (
|
||||
<div
|
||||
@@ -556,7 +556,7 @@ export default function CreateImage() {
|
||||
|
||||
{/* ── Prompt box (fixed bottom center) ───────────────────── */}
|
||||
<div
|
||||
className="animate-fade-in-up fixed bottom-6 left-0 right-0 z-30 mx-auto w-full max-w-[900px] px-4"
|
||||
className="animate-fade-in-up fixed bottom-6 left-0 right-0 z-30 mx-auto w-full max-w-[730px] px-4"
|
||||
style={{ animationDelay: "150ms" }}
|
||||
>
|
||||
<PromptBox
|
||||
|
||||
@@ -219,9 +219,9 @@ export default function CreateVideo() {
|
||||
(): PopoverItem[] | null =>
|
||||
selectedModel.durationOptions
|
||||
? selectedModel.durationOptions.map((d) => ({
|
||||
label: `${d}s`,
|
||||
selected: d === (duration ?? selectedModel.defaultDuration),
|
||||
}))
|
||||
label: `${d}s`,
|
||||
selected: d === (duration ?? selectedModel.defaultDuration),
|
||||
}))
|
||||
: null,
|
||||
[selectedModel, duration],
|
||||
);
|
||||
@@ -231,9 +231,9 @@ export default function CreateVideo() {
|
||||
(): PopoverItem[] | null =>
|
||||
selectedModel.resolutionOptions
|
||||
? selectedModel.resolutionOptions.map((r) => ({
|
||||
label: r,
|
||||
selected: r === (resolution ?? selectedModel.defaultResolution),
|
||||
}))
|
||||
label: r,
|
||||
selected: r === (resolution ?? selectedModel.defaultResolution),
|
||||
}))
|
||||
: null,
|
||||
[selectedModel, resolution],
|
||||
);
|
||||
@@ -243,17 +243,17 @@ export default function CreateVideo() {
|
||||
(): PopoverItem[] | null =>
|
||||
supportsRefMode
|
||||
? [
|
||||
{
|
||||
label: "Keyframe",
|
||||
description: "First/Last frame",
|
||||
selected: inputMode === "keyframe",
|
||||
},
|
||||
{
|
||||
label: "Reference",
|
||||
description: "Multi-media ref",
|
||||
selected: inputMode === "reference",
|
||||
},
|
||||
]
|
||||
{
|
||||
label: "Keyframe",
|
||||
description: "First/Last frame",
|
||||
selected: inputMode === "keyframe",
|
||||
},
|
||||
{
|
||||
label: "Reference",
|
||||
description: "Multi-media ref",
|
||||
selected: inputMode === "reference",
|
||||
},
|
||||
]
|
||||
: null,
|
||||
[supportsRefMode, inputMode],
|
||||
);
|
||||
@@ -424,8 +424,8 @@ export default function CreateVideo() {
|
||||
const referenceImageMediaTokens =
|
||||
isReferenceMode && referenceImages.length > 0
|
||||
? referenceImages
|
||||
.map((img) => img.mediaToken)
|
||||
.filter((t) => t.length > 0)
|
||||
.map((img) => img.mediaToken)
|
||||
.filter((t) => t.length > 0)
|
||||
: undefined;
|
||||
|
||||
const referenceVideoMediaTokens =
|
||||
@@ -680,7 +680,7 @@ export default function CreateVideo() {
|
||||
|
||||
{/* ── Prompt box (fixed bottom center) ───────────────────── */}
|
||||
<div
|
||||
className="animate-fade-in-up fixed bottom-6 left-0 right-0 z-30 mx-auto w-full max-w-[900px] px-4"
|
||||
className="animate-fade-in-up fixed bottom-6 left-0 right-0 z-30 mx-auto w-full max-w-[730px] px-4"
|
||||
style={{ animationDelay: "150ms" }}
|
||||
>
|
||||
<PromptBox
|
||||
@@ -882,7 +882,7 @@ export default function CreateVideo() {
|
||||
maxSelect={Math.max(
|
||||
1,
|
||||
(isReferenceMode ? (selectedModel.maxReferenceImages ?? 3) : 1) -
|
||||
referenceImages.length,
|
||||
referenceImages.length,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -375,53 +375,25 @@ export class MouseControls {
|
||||
|
||||
if ((event.ctrlKey || event.metaKey) && !this.isProcessing) {
|
||||
const keyLower = event.key.toLowerCase();
|
||||
if (keyLower === "z" && !event.shiftKey) {
|
||||
// undo
|
||||
event.preventDefault();
|
||||
if (keyLower === "z") {
|
||||
// redo
|
||||
this.isProcessing = true;
|
||||
try {
|
||||
await this.sceneManager?.undo();
|
||||
} finally {
|
||||
this.isProcessing = false;
|
||||
}
|
||||
} else if (keyLower === "z" && event.shiftKey) {
|
||||
// redo (Ctrl+Shift+Z / Cmd+Shift+Z)
|
||||
event.preventDefault();
|
||||
this.isProcessing = true;
|
||||
try {
|
||||
await this.sceneManager?.redo();
|
||||
} finally {
|
||||
this.isProcessing = false;
|
||||
}
|
||||
} else if (keyLower === "y") {
|
||||
// redo (Ctrl+Y on Windows/Linux, Cmd+Y on macOS)
|
||||
event.preventDefault();
|
||||
this.isProcessing = true;
|
||||
try {
|
||||
await this.sceneManager?.redo();
|
||||
} finally {
|
||||
this.isProcessing = false;
|
||||
}
|
||||
await this.sceneManager?.redo();
|
||||
this.isProcessing = false;
|
||||
} else if (keyLower === "c") {
|
||||
// Copy
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.isProcessing = true;
|
||||
try {
|
||||
await this.sceneManager?.copy();
|
||||
} finally {
|
||||
this.isProcessing = false;
|
||||
}
|
||||
await this.sceneManager?.copy();
|
||||
this.isProcessing = false;
|
||||
} else if (keyLower === "v") {
|
||||
// Paste
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.isProcessing = true;
|
||||
try {
|
||||
await this.sceneManager?.paste();
|
||||
} finally {
|
||||
this.isProcessing = false;
|
||||
}
|
||||
await this.sceneManager?.paste();
|
||||
this.isProcessing = false;
|
||||
} else if (event.key === "0") {
|
||||
// Stats Menu
|
||||
this.enable_stats();
|
||||
|
||||
@@ -118,10 +118,10 @@ export class SceneManager implements SceneManagerAPI {
|
||||
}
|
||||
|
||||
public async undo() {
|
||||
if (this.undoIndex >= this.undoStack.length) {
|
||||
return;
|
||||
}
|
||||
this.undoIndex += 1;
|
||||
if (this.undoIndex >= this.undoStack.length) {
|
||||
this.undoIndex = this.undoStack.length;
|
||||
}
|
||||
const undoCommand = this.undoStack.at(
|
||||
this.undoStack.length - this.undoIndex,
|
||||
);
|
||||
@@ -131,28 +131,16 @@ export class SceneManager implements SceneManagerAPI {
|
||||
}
|
||||
|
||||
public async redo() {
|
||||
if (this.undoIndex <= 0) {
|
||||
return;
|
||||
}
|
||||
const undoCommand = this.undoStack.at(
|
||||
this.undoStack.length - this.undoIndex,
|
||||
);
|
||||
await undoCommand?.redo();
|
||||
this.undoIndex -= 1;
|
||||
this.updateOutliner(); // In the future we will address this because of its relational issues with editor and the current class.
|
||||
this.lastSceneState = this.getSceneState();
|
||||
}
|
||||
|
||||
// Drop any redo-future before pushing a fresh command so the new action
|
||||
// replaces the branch of history the user had undone past.
|
||||
private dropRedoHistory() {
|
||||
if (this.undoIndex > 0) {
|
||||
this.undoStack = this.undoStack.slice(
|
||||
0,
|
||||
this.undoStack.length - this.undoIndex,
|
||||
);
|
||||
if (this.undoIndex <= -1) {
|
||||
this.undoIndex = 0;
|
||||
}
|
||||
this.updateOutliner(); // In the future we will address this because of its relational issues with editor and the current class.
|
||||
this.lastSceneState = this.getSceneState();
|
||||
}
|
||||
|
||||
public async create(
|
||||
@@ -479,7 +467,6 @@ export class SceneManager implements SceneManagerAPI {
|
||||
}
|
||||
|
||||
public async add_creation_undostack(object: THREE.Object3D) {
|
||||
this.dropRedoHistory();
|
||||
this.undoStack.push(
|
||||
new CreationSceneItem(this, {
|
||||
object_uuid: object.uuid,
|
||||
@@ -501,7 +488,6 @@ export class SceneManager implements SceneManagerAPI {
|
||||
const is_transform = this.is_transformation(sceneState);
|
||||
const is_userdata = this.is_userdata(sceneState);
|
||||
|
||||
this.dropRedoHistory();
|
||||
this.undoStack = this.undoStack.slice(-16);
|
||||
|
||||
if (is_deleted != "") {
|
||||
@@ -572,6 +558,7 @@ export class SceneManager implements SceneManagerAPI {
|
||||
}
|
||||
}
|
||||
}
|
||||
this.undoIndex = 0;
|
||||
this.lastSceneState = sceneState;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export const isHotkeyDisabled = ()=>{
|
||||
export const disableHotkeyInput = (level: number)=>{
|
||||
if(hotkeysStatus.value.disabled === true){
|
||||
if (level > hotkeysStatus.value.disabledBy){
|
||||
hotkeysStatus.value = { ...hotkeysStatus.value, disabledBy: level };
|
||||
hotkeysStatus.value.disabledBy === level;
|
||||
}
|
||||
}else{
|
||||
hotkeysStatus.value = {
|
||||
|
||||
@@ -358,7 +358,7 @@ const TextToImage = ({ imageMediaId, imageUrl }: TextToImageProps) => {
|
||||
)}
|
||||
|
||||
<animated.div
|
||||
className="fixed left-1/2 z-20 -translate-x-1/2"
|
||||
className="fixed left-1/2 z-20 w-[730px] -translate-x-1/2"
|
||||
style={promptAnim}
|
||||
>
|
||||
{showPromptAtBottom && batches.length > 0 && (
|
||||
|
||||
@@ -58,9 +58,9 @@ const ImageToVideo = ({ imageMediaId, imageUrl }: ImageToVideoProps) => {
|
||||
|
||||
const jobContext: JobContextType = {
|
||||
jobTokens: [],
|
||||
addJobToken: () => { },
|
||||
removeJobToken: () => { },
|
||||
clearJobTokens: () => { },
|
||||
addJobToken: () => {},
|
||||
removeJobToken: () => {},
|
||||
clearJobTokens: () => {},
|
||||
};
|
||||
|
||||
useVideoGenerationCompleteEvent(
|
||||
@@ -124,10 +124,10 @@ const ImageToVideo = ({ imageMediaId, imageUrl }: ImageToVideoProps) => {
|
||||
className={twMerge(
|
||||
"relative z-20 mb-52 flex flex-col items-center justify-center text-center drop-shadow-xl",
|
||||
imageRowVisible &&
|
||||
(inputMode === "reference" &&
|
||||
(inputMode === "reference" &&
|
||||
selectedVideoModel?.supportsReferenceMode
|
||||
? "mb-[30rem]"
|
||||
: "mb-80"),
|
||||
? "mb-[30rem]"
|
||||
: "mb-80"),
|
||||
)}
|
||||
>
|
||||
<h1 className="text-7xl font-bold text-base-fg">Generate Video</h1>
|
||||
@@ -182,7 +182,7 @@ const ImageToVideo = ({ imageMediaId, imageUrl }: ImageToVideoProps) => {
|
||||
)} */}
|
||||
|
||||
<animated.div
|
||||
className="fixed left-1/2 z-20 w-[860px] -translate-x-1/2"
|
||||
className="fixed left-1/2 z-20 w-[730px] -translate-x-1/2"
|
||||
style={promptAnim}
|
||||
>
|
||||
{/* TODO: Uncomment when backend supports history frontend id */}
|
||||
|
||||
@@ -244,55 +244,6 @@ img {
|
||||
}
|
||||
|
||||
/* Hide native scrollbars while preserving scrollability */
|
||||
/* Minimal thin scrollbar for prompt textareas */
|
||||
.promptbox-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
|
||||
}
|
||||
.promptbox-scrollbar::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
.promptbox-scrollbar::-webkit-scrollbar:vertical {
|
||||
width: 4px;
|
||||
}
|
||||
.promptbox-scrollbar::-webkit-scrollbar:horizontal {
|
||||
height: 4px;
|
||||
}
|
||||
.promptbox-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.promptbox-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 9999px;
|
||||
}
|
||||
.promptbox-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
.promptbox-scrollbar::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
.promptbox-scrollbar::-webkit-resizer {
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
.promptbox-resize-wrap {
|
||||
position: relative;
|
||||
}
|
||||
.promptbox-resize-wrap::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
right: 2px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
pointer-events: none;
|
||||
background:
|
||||
linear-gradient(135deg,
|
||||
transparent 40%, rgba(255, 255, 255, 0.5) 40%, rgba(255, 255, 255, 0.5) 45%, transparent 45%,
|
||||
transparent 55%, rgba(255, 255, 255, 0.5) 55%, rgba(255, 255, 255, 0.5) 60%, transparent 60%,
|
||||
transparent 70%, rgba(255, 255, 255, 0.5) 70%, rgba(255, 255, 255, 0.5) 75%, transparent 75%);
|
||||
}
|
||||
|
||||
.scrollbar-hidden {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
|
||||
@@ -751,7 +751,7 @@ export const ImagePromptRow = ({
|
||||
)}
|
||||
<div
|
||||
className={twMerge(
|
||||
"absolute left-0 glass w-[860px] rounded-t-xl flex",
|
||||
"absolute left-0 glass w-[730px] rounded-t-xl flex",
|
||||
showVideoReferenceSection ? "-top-[144px]" : "-top-[72px]",
|
||||
className,
|
||||
)}
|
||||
@@ -880,56 +880,56 @@ export const ImagePromptRow = ({
|
||||
})}
|
||||
{referenceImages.length + uploadingImages.length <
|
||||
maxImagePromptCount && (
|
||||
<Tooltip
|
||||
interactive={true}
|
||||
position="top"
|
||||
delay={100}
|
||||
className="bg-ui-controls text-base-fg border border-ui-panel-border p-2 -mb-0.5"
|
||||
closeOnClick={true}
|
||||
content={
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{allowUpload && (
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleUploadClickStart}
|
||||
icon={faPlus}
|
||||
className="w-full"
|
||||
>
|
||||
Upload
|
||||
</Button>
|
||||
)}
|
||||
<Tooltip
|
||||
interactive={true}
|
||||
position="top"
|
||||
delay={100}
|
||||
className="bg-ui-controls text-base-fg border border-ui-panel-border p-2 -mb-0.5"
|
||||
closeOnClick={true}
|
||||
content={
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{allowUpload && (
|
||||
<Button
|
||||
variant="action"
|
||||
onClick={() => {
|
||||
setGalleryTarget("start");
|
||||
setIsGalleryModalOpen(true);
|
||||
}}
|
||||
icon={faImages}
|
||||
className="w-full bg-base-fg/10 hover:bg-base-fg/20"
|
||||
variant="primary"
|
||||
onClick={handleUploadClickStart}
|
||||
icon={faPlus}
|
||||
className="w-full"
|
||||
>
|
||||
Pick from library
|
||||
Upload
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="action"
|
||||
className="bg-ui-controls/40 hover:bg-ui-controls/60 aspect-square w-full overflow-hidden rounded-lg w-14 border-dashed border-2 border-black/5 dark:border-white/25 transition-all"
|
||||
onClick={() => {
|
||||
if (allowUpload) handleUploadClickStart();
|
||||
else {
|
||||
)}
|
||||
<Button
|
||||
variant="action"
|
||||
onClick={() => {
|
||||
setGalleryTarget("start");
|
||||
setIsGalleryModalOpen(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faPlus}
|
||||
className="text-2xl opacity-80 text-base-fg"
|
||||
/>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
}}
|
||||
icon={faImages}
|
||||
className="w-full bg-base-fg/10 hover:bg-base-fg/20"
|
||||
>
|
||||
Pick from library
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="action"
|
||||
className="bg-ui-controls/40 hover:bg-ui-controls/60 aspect-square w-full overflow-hidden rounded-lg w-14 border-dashed border-2 border-black/5 dark:border-white/25 transition-all"
|
||||
onClick={() => {
|
||||
if (allowUpload) handleUploadClickStart();
|
||||
else {
|
||||
setGalleryTarget("start");
|
||||
setIsGalleryModalOpen(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faPlus}
|
||||
className="text-2xl opacity-80 text-base-fg"
|
||||
/>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{isVideo && showEndFrameSection && (
|
||||
|
||||
@@ -9,8 +9,6 @@ import {
|
||||
faMessageCheck,
|
||||
faFrame,
|
||||
faExpand,
|
||||
faChevronDown,
|
||||
faChevronUp,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import {
|
||||
faRectangleVertical,
|
||||
@@ -27,7 +25,6 @@ import { twMerge } from "tailwind-merge";
|
||||
import { GenerationProvider } from "@storyteller/api-enums";
|
||||
import { GenerationCountPicker } from "./common/GenerationCountPicker";
|
||||
import { StoreApi, UseBoundStore } from "zustand";
|
||||
import { toast } from "@storyteller/ui-toaster";
|
||||
|
||||
export type AspectRatio = "wide" | "tall" | "square";
|
||||
|
||||
@@ -71,17 +68,6 @@ export const PromptBox2D = ({
|
||||
useSignals();
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const toggleExpand = () => {
|
||||
setIsExpanded((prev) => {
|
||||
const next = !prev;
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = next ? "300px" : "auto";
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
const [content, setContent] = useState<React.ReactNode>("");
|
||||
|
||||
//const { lastRenderedBitmap } = useCanvasSignal();
|
||||
@@ -192,7 +178,7 @@ export const PromptBox2D = ({
|
||||
value.slice(0, selectionStart) + pastedText + value.slice(selectionEnd);
|
||||
setPrompt(next);
|
||||
requestAnimationFrame(() => {
|
||||
const pos = Math.min(selectionStart + pastedText.length, next.length);
|
||||
const pos = selectionStart + pastedText.length;
|
||||
textareaRef.current?.setSelectionRange(pos, pos);
|
||||
});
|
||||
};
|
||||
@@ -261,15 +247,9 @@ export const PromptBox2D = ({
|
||||
// console.log("generateResponse", generateResponse);
|
||||
// };
|
||||
|
||||
const maxLen = selectedImageModel?.maxPromptLength ?? 1000;
|
||||
|
||||
const handleGenerate = async () => {
|
||||
const busy = Boolean(isEnqueueing ?? internalEnqueueing);
|
||||
if (busy || isDisabled || !prompt.trim()) return;
|
||||
if (prompt.length > maxLen) {
|
||||
toast.error(`Prompt exceeds the ${maxLen} character limit for this model`);
|
||||
return;
|
||||
}
|
||||
setInternalEnqueueing(true);
|
||||
const timeout = setTimeout(() => {
|
||||
setInternalEnqueueing(false);
|
||||
@@ -370,10 +350,10 @@ export const PromptBox2D = ({
|
||||
)}
|
||||
<div
|
||||
className={twMerge(
|
||||
"glass relative w-[860px] rounded-xl p-4",
|
||||
"glass w-[730px] rounded-xl p-4",
|
||||
selectedImageModel?.canUseImagePrompt &&
|
||||
isImageRowVisible &&
|
||||
"rounded-t-none",
|
||||
isImageRowVisible &&
|
||||
"rounded-t-none",
|
||||
)}
|
||||
>
|
||||
<div className="flex justify-center gap-2">
|
||||
@@ -410,23 +390,18 @@ export const PromptBox2D = ({
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<div className="promptbox-resize-wrap relative flex-1">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
rows={1}
|
||||
placeholder="Describe your image..."
|
||||
className={`promptbox-scrollbar text-md mb-2 min-h-[2.5em] w-full resize-y overflow-y-auto rounded bg-transparent pb-2 pr-2 pt-1 text-base-fg placeholder-base-fg/60 focus:outline-none ${isExpanded ? "max-h-[300px]" : "max-h-[5.5em]"}`}
|
||||
value={prompt}
|
||||
onChange={handleChange}
|
||||
onPaste={handlePaste}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={() => { }}
|
||||
onBlur={() => { }}
|
||||
/>
|
||||
<span className={`absolute -bottom-1 right-0 text-[10px] tabular-nums ${prompt.length > maxLen ? "text-red-500" : "text-base-fg/40"}`}>
|
||||
{prompt.length} / {maxLen}
|
||||
</span>
|
||||
</div>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
rows={1}
|
||||
placeholder="Describe your image..."
|
||||
className="text-md mb-2 max-h-[5.5em] flex-1 resize-none overflow-y-auto rounded bg-transparent pb-2 pr-2 pt-1 text-base-fg placeholder-base-fg/60 focus:outline-none"
|
||||
value={prompt}
|
||||
onChange={handleChange}
|
||||
onPaste={handlePaste}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={() => {}}
|
||||
onBlur={() => {}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -508,17 +483,6 @@ export const PromptBox2D = ({
|
||||
</GenerateButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute -bottom-1 left-1/2 -translate-x-1/2">
|
||||
<Tooltip content={isExpanded ? "Collapse" : "Expand"} position="top" className="-mb-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleExpand}
|
||||
className="text-base-fg/30 hover:text-base-fg/90 transition-colors px-3 py-0.5"
|
||||
>
|
||||
<FontAwesomeIcon icon={isExpanded ? faChevronUp : faChevronDown} className="text-xs" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -8,8 +8,6 @@ import {
|
||||
faMessageCheck,
|
||||
faMessageXmark,
|
||||
faExpand,
|
||||
faChevronDown,
|
||||
faChevronUp,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import {
|
||||
faRectangleWide,
|
||||
@@ -72,11 +70,11 @@ interface PromptBox3DProps {
|
||||
selectedImageModel?: ImageModel;
|
||||
selectedProvider?: GenerationProvider;
|
||||
snapshotCurrentFrame:
|
||||
| ((shouldDownload?: boolean) => {
|
||||
base64Snapshot: string;
|
||||
file: File;
|
||||
} | null)
|
||||
| undefined;
|
||||
| ((shouldDownload?: boolean) => {
|
||||
base64Snapshot: string;
|
||||
file: File;
|
||||
} | null)
|
||||
| undefined;
|
||||
credits?: number | null;
|
||||
}
|
||||
|
||||
@@ -116,17 +114,6 @@ export const PromptBox3D = ({
|
||||
const resolution = usePrompt3DStore((s) => s.resolution);
|
||||
const setResolution = usePrompt3DStore((s) => s.setResolution);
|
||||
const [isEnqueueing, setIsEnqueueing] = useState(false);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const toggleExpand = () => {
|
||||
setIsExpanded((prev) => {
|
||||
const next = !prev;
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = next ? "300px" : "auto";
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
const referenceImages = usePrompt3DStore((s) => s.referenceImages);
|
||||
const setReferenceImages = usePrompt3DStore((s) => s.setReferenceImages);
|
||||
const [showImagePrompts, setShowImagePrompts] = useState(false);
|
||||
@@ -176,7 +163,7 @@ export const PromptBox3D = ({
|
||||
>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (textareaRef.current && !isExpanded) {
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = "auto";
|
||||
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
|
||||
}
|
||||
@@ -251,7 +238,7 @@ export const PromptBox3D = ({
|
||||
value.slice(0, selectionStart) + pastedText + value.slice(selectionEnd);
|
||||
setPrompt(next);
|
||||
requestAnimationFrame(() => {
|
||||
const pos = Math.min(selectionStart + pastedText.length, next.length);
|
||||
const pos = selectionStart + pastedText.length;
|
||||
textareaRef.current?.setSelectionRange(pos, pos);
|
||||
});
|
||||
};
|
||||
@@ -266,13 +253,7 @@ export const PromptBox3D = ({
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const maxLen = selectedImageModel?.maxPromptLength ?? 1000;
|
||||
|
||||
const handleEnqueue = async () => {
|
||||
if (prompt.length > maxLen) {
|
||||
toast.error(`Prompt exceeds the ${maxLen} character limit for this model`);
|
||||
return;
|
||||
}
|
||||
gtagEvent("enqueue_3d");
|
||||
const isDesktop = IsDesktopApp();
|
||||
console.log("Is Desktop?", isDesktop);
|
||||
@@ -567,11 +548,11 @@ export const PromptBox3D = ({
|
||||
)}
|
||||
<div
|
||||
className={twMerge(
|
||||
"glass relative w-[860px] rounded-xl p-4",
|
||||
"glass w-[730px] rounded-xl p-4",
|
||||
isPromptBoxFocused.value ? "!border !border-primary" : "",
|
||||
selectedImageModel?.canUseImagePrompt &&
|
||||
isImageRowVisible &&
|
||||
"rounded-t-none",
|
||||
isImageRowVisible &&
|
||||
"rounded-t-none",
|
||||
)}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
@@ -613,29 +594,24 @@ export const PromptBox3D = ({
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<div className="promptbox-resize-wrap relative flex-1">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
rows={1}
|
||||
placeholder="Describe your image..."
|
||||
className={`promptbox-scrollbar text-md mb-2 min-h-[2.5em] w-full resize-y overflow-y-auto rounded bg-transparent pb-2 pr-2 pt-1 text-base-fg placeholder-base-fg/60 focus:outline-none ${isExpanded ? "max-h-[300px]" : "max-h-[5.5em]"}`}
|
||||
value={prompt}
|
||||
onChange={handleChange}
|
||||
onPaste={handlePaste}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={() => {
|
||||
disableHotkeyInput(DomLevels.INPUT);
|
||||
isPromptBoxFocused.value = true;
|
||||
}}
|
||||
onBlur={() => {
|
||||
enableHotkeyInput(DomLevels.INPUT);
|
||||
isPromptBoxFocused.value = false;
|
||||
}}
|
||||
/>
|
||||
<span className={`absolute -bottom-1 right-0 text-[10px] tabular-nums ${prompt.length > maxLen ? "text-red-500" : "text-base-fg/40"}`}>
|
||||
{prompt.length} / {maxLen}
|
||||
</span>
|
||||
</div>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
rows={1}
|
||||
placeholder="Describe your image..."
|
||||
className="text-md mb-2 max-h-[5.5em] min-h-[36px] flex-1 resize-none overflow-y-auto rounded bg-transparent pb-2 pr-2 pt-1 text-base-fg placeholder-base-fg/60 focus:outline-none"
|
||||
value={prompt}
|
||||
onChange={handleChange}
|
||||
onPaste={handlePaste}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={() => {
|
||||
disableHotkeyInput(DomLevels.INPUT);
|
||||
isPromptBoxFocused.value = true;
|
||||
}}
|
||||
onBlur={() => {
|
||||
enableHotkeyInput(DomLevels.INPUT);
|
||||
isPromptBoxFocused.value = false;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -773,17 +749,6 @@ export const PromptBox3D = ({
|
||||
</GenerateButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute -bottom-1 left-1/2 -translate-x-1/2">
|
||||
<Tooltip content={isExpanded ? "Collapse" : "Expand"} position="top" className="-mb-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleExpand}
|
||||
className="text-base-fg/30 hover:text-base-fg/90 transition-colors px-3 py-0.5"
|
||||
>
|
||||
<FontAwesomeIcon icon={isExpanded ? faChevronUp : faChevronDown} className="text-xs" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<CameraSettingsModal
|
||||
isOpen={isCameraSettingsOpen}
|
||||
|
||||
@@ -15,8 +15,6 @@ import {
|
||||
faRedo,
|
||||
faExpand,
|
||||
faArrowsUpDownLeftRight,
|
||||
faChevronDown,
|
||||
faChevronUp,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Button, ToggleButton, GenerateButton } from "@storyteller/ui-button";
|
||||
@@ -28,7 +26,6 @@ import { CommonAspectRatio, ImageModel } from "@storyteller/model-list";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { ImagePromptRow, type UploadImageFn } from "./ImagePromptRow";
|
||||
import { RefImage, usePromptEditStore } from "./promptStore";
|
||||
import { toast } from "@storyteller/ui-toaster";
|
||||
import { GenerationProvider } from "@storyteller/api-enums";
|
||||
import { AspectRatioPicker } from "./common/AspectRatioPicker";
|
||||
import { GenerationCountPicker } from "./common/GenerationCountPicker";
|
||||
@@ -85,17 +82,6 @@ export const PromptBoxEdit = ({
|
||||
const [prompt, setPrompt] = useState("");
|
||||
const [useSystemPrompt, setUseSystemPrompt] = useState(true);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const toggleExpand = () => {
|
||||
setIsExpanded((prev) => {
|
||||
const next = !prev;
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = next ? "300px" : "auto";
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
const [internalEnqueueing, setInternalEnqueueing] = useState(false);
|
||||
const referenceImages = usePromptEditStore((s) => s.referenceImages);
|
||||
const setReferenceImages = usePromptEditStore((s) => s.setReferenceImages);
|
||||
@@ -219,7 +205,7 @@ export const PromptBoxEdit = ({
|
||||
value.slice(0, selectionStart) + pastedText + value.slice(selectionEnd);
|
||||
setPrompt(next);
|
||||
requestAnimationFrame(() => {
|
||||
const pos = Math.min(selectionStart + pastedText.length, next.length);
|
||||
const pos = selectionStart + pastedText.length;
|
||||
textareaRef.current?.setSelectionRange(pos, pos);
|
||||
});
|
||||
};
|
||||
@@ -252,15 +238,9 @@ export const PromptBoxEdit = ({
|
||||
// );
|
||||
// };
|
||||
|
||||
const maxLen = selectedImageModel?.maxPromptLength ?? 1000;
|
||||
|
||||
const handleGenerate = async () => {
|
||||
const busy = Boolean(isEnqueueing ?? internalEnqueueing);
|
||||
if (busy || isDisabled || !prompt.trim()) return;
|
||||
if (prompt.length > maxLen) {
|
||||
toast.error(`Prompt exceeds the ${maxLen} character limit for this model`);
|
||||
return;
|
||||
}
|
||||
setInternalEnqueueing(true);
|
||||
const timeout = setTimeout(() => {
|
||||
setInternalEnqueueing(false);
|
||||
@@ -282,23 +262,10 @@ export const PromptBoxEdit = ({
|
||||
|
||||
const modes = isNanoBananaModel
|
||||
? [
|
||||
{
|
||||
value: "marker",
|
||||
icon: faPen,
|
||||
text: "Marker",
|
||||
},
|
||||
{
|
||||
value: "eraser",
|
||||
icon: faEraser,
|
||||
text: "Eraser",
|
||||
},
|
||||
]
|
||||
: supportsMaskedInpainting
|
||||
? [
|
||||
{
|
||||
value: "edit",
|
||||
icon: faEdit,
|
||||
text: "Edit Region",
|
||||
value: "marker",
|
||||
icon: faPen,
|
||||
text: "Marker",
|
||||
},
|
||||
{
|
||||
value: "eraser",
|
||||
@@ -306,18 +273,31 @@ export const PromptBoxEdit = ({
|
||||
text: "Eraser",
|
||||
},
|
||||
]
|
||||
: supportsMaskedInpainting
|
||||
? [
|
||||
{
|
||||
value: "edit",
|
||||
icon: faEdit,
|
||||
text: "Edit Region",
|
||||
},
|
||||
{
|
||||
value: "eraser",
|
||||
icon: faEraser,
|
||||
text: "Eraser",
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
value: "edit",
|
||||
icon: faEdit,
|
||||
text: "Edit Region",
|
||||
},
|
||||
{
|
||||
value: "select",
|
||||
icon: faMousePointer,
|
||||
text: "Select",
|
||||
},
|
||||
];
|
||||
{
|
||||
value: "edit",
|
||||
icon: faEdit,
|
||||
text: "Edit Region",
|
||||
},
|
||||
{
|
||||
value: "select",
|
||||
icon: faMousePointer,
|
||||
text: "Select",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -327,8 +307,8 @@ export const PromptBoxEdit = ({
|
||||
className={twMerge(
|
||||
"glass w-fit mx-auto rounded-xl px-2 py-2 flex items-center gap-3",
|
||||
selectedImageModel?.canUseImagePrompt &&
|
||||
isImageRowVisible &&
|
||||
"mb-[72px]",
|
||||
isImageRowVisible &&
|
||||
"mb-[72px]",
|
||||
)}
|
||||
>
|
||||
<ButtonIconSelect
|
||||
@@ -372,17 +352,17 @@ export const PromptBoxEdit = ({
|
||||
referenceImages={referenceImages}
|
||||
setReferenceImages={setReferenceImages}
|
||||
uploadImage={uploadImage}
|
||||
onImageClick={() => { }}
|
||||
onImageClick={() => {}}
|
||||
className=""
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={twMerge(
|
||||
"glass relative w-[860px] rounded-xl p-4",
|
||||
"glass w-[730px] rounded-xl p-4",
|
||||
isFocused && "ring-1 ring-primary border-primary",
|
||||
selectedImageModel?.canUseImagePrompt &&
|
||||
isImageRowVisible &&
|
||||
"rounded-t-none",
|
||||
isImageRowVisible &&
|
||||
"rounded-t-none",
|
||||
)}
|
||||
>
|
||||
<div className="flex justify-center gap-2">
|
||||
@@ -418,23 +398,18 @@ export const PromptBoxEdit = ({
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<div className="promptbox-resize-wrap relative flex-1">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
rows={1}
|
||||
placeholder="Write what you want to change in your image and click generate..."
|
||||
className={`promptbox-scrollbar text-md mb-2 min-h-[2.5em] w-full resize-y overflow-y-auto rounded bg-transparent pb-2 pr-2 pt-1 text-white placeholder-white placeholder:text-white/60 focus:outline-none ${isExpanded ? "max-h-[300px]" : "max-h-[5.5em]"}`}
|
||||
value={prompt}
|
||||
onChange={handleChange}
|
||||
onPaste={handlePaste}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
/>
|
||||
<span className={`absolute -bottom-1 right-0 text-[10px] tabular-nums ${prompt.length > maxLen ? "text-red-500" : "text-white/40"}`}>
|
||||
{prompt.length} / {maxLen}
|
||||
</span>
|
||||
</div>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
rows={1}
|
||||
placeholder="Write what you want to change in your image and click generate..."
|
||||
className="text-md mb-2 max-h-[5.5em] flex-1 resize-none overflow-y-auto rounded bg-transparent pb-2 pr-2 pt-1 text-white placeholder-white placeholder:text-white/60 focus:outline-none"
|
||||
value={prompt}
|
||||
onChange={handleChange}
|
||||
onPaste={handlePaste}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -526,17 +501,6 @@ export const PromptBoxEdit = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute -bottom-1 left-1/2 -translate-x-1/2">
|
||||
<Tooltip content={isExpanded ? "Collapse" : "Expand"} position="top" className="-mb-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleExpand}
|
||||
className="text-white/30 hover:text-white/90 transition-colors px-3 py-0.5"
|
||||
>
|
||||
<FontAwesomeIcon icon={isExpanded ? faChevronUp : faChevronDown} className="text-xs" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -16,8 +16,6 @@ import {
|
||||
faMessageXmark,
|
||||
faMessageCheck,
|
||||
faExpand,
|
||||
faChevronDown,
|
||||
faChevronUp,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { CommonAspectRatio, ImageModel } from "@storyteller/model-list";
|
||||
@@ -98,21 +96,6 @@ export const PromptBoxImage = ({
|
||||
const setGenerationCount = usePromptImageStore((s) => s.setGenerationCount);
|
||||
const [isEnqueueing, setIsEnqueueing] = useState(false);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const toggleExpand = () => {
|
||||
setIsExpanded((prev) => {
|
||||
const next = !prev;
|
||||
if (textareaRef.current) {
|
||||
if (next) {
|
||||
textareaRef.current.style.height = `${window.innerHeight - 300}px`;
|
||||
} else {
|
||||
textareaRef.current.style.height = "auto";
|
||||
}
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
const referenceImages = usePromptImageStore((s) => s.referenceImages);
|
||||
const setReferenceImages = usePromptImageStore((s) => s.setReferenceImages);
|
||||
const [uploadingImages, _setUploadingImages] = useState<
|
||||
@@ -242,7 +225,7 @@ export const PromptBoxImage = ({
|
||||
value.slice(0, selectionStart) + pastedText + value.slice(selectionEnd);
|
||||
setPrompt(next);
|
||||
requestAnimationFrame(() => {
|
||||
const pos = Math.min(selectionStart + pastedText.length, next.length);
|
||||
const pos = selectionStart + pastedText.length;
|
||||
textareaRef.current?.setSelectionRange(pos, pos);
|
||||
});
|
||||
};
|
||||
@@ -251,17 +234,11 @@ export const PromptBoxImage = ({
|
||||
setPrompt(e.target.value);
|
||||
};
|
||||
|
||||
const maxLen = selectedModel?.maxPromptLength ?? 1000;
|
||||
|
||||
const handleEnqueue = async () => {
|
||||
if (!prompt.trim()) {
|
||||
console.warn("Cannot generate image: prompt is empty");
|
||||
return;
|
||||
}
|
||||
if (prompt.length > maxLen) {
|
||||
toast.error(`Prompt exceeds the ${maxLen} character limit for this model`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedModel) {
|
||||
console.warn("Cannot generate image: no model selected");
|
||||
@@ -431,7 +408,7 @@ export const PromptBoxImage = ({
|
||||
|
||||
<div
|
||||
className={twMerge(
|
||||
"glass relative w-[860px] rounded-xl p-4",
|
||||
"glass w-[730px] rounded-xl p-4",
|
||||
isImageRowVisible &&
|
||||
selectedModel?.canUseImagePrompt &&
|
||||
"rounded-t-none",
|
||||
@@ -474,23 +451,18 @@ export const PromptBoxImage = ({
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<div className="promptbox-resize-wrap relative flex-1">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
rows={1}
|
||||
placeholder="Describe what you want in the image..."
|
||||
className="promptbox-scrollbar text-md mb-2 min-h-[2.5em] w-full resize-y overflow-y-auto rounded bg-transparent pb-2 pr-2 pt-1 text-base-fg placeholder-base-fg/60 focus:outline-none"
|
||||
value={prompt}
|
||||
onChange={handleChange}
|
||||
onPaste={handlePaste}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
/>
|
||||
<span className={`absolute -bottom-1 right-0 text-[10px] tabular-nums ${prompt.length > maxLen ? "text-red-500" : "text-base-fg/40"}`}>
|
||||
{prompt.length} / {maxLen}
|
||||
</span>
|
||||
</div>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
rows={1}
|
||||
placeholder="Describe what you want in the image..."
|
||||
className="text-md mb-2 min-h-[2.5em] flex-1 resize-y overflow-y-auto rounded bg-transparent pb-2 pr-2 pt-1 text-base-fg placeholder-base-fg/60 focus:outline-none"
|
||||
value={prompt}
|
||||
onChange={handleChange}
|
||||
onPaste={handlePaste}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -567,17 +539,6 @@ export const PromptBoxImage = ({
|
||||
</GenerateButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute -bottom-1 left-1/2 -translate-x-1/2">
|
||||
<Tooltip content={isExpanded ? "Collapse" : "Expand"} position="top" className="-mb-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleExpand}
|
||||
className="text-base-fg/30 hover:text-base-fg/90 transition-colors px-3 py-0.5"
|
||||
>
|
||||
<FontAwesomeIcon icon={isExpanded ? faChevronUp : faChevronDown} className="text-xs" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -17,8 +17,6 @@ import {
|
||||
faWaveformLines,
|
||||
faClock,
|
||||
faTriangleExclamation,
|
||||
faChevronDown,
|
||||
faChevronUp,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import {
|
||||
faCircleInfo,
|
||||
@@ -124,21 +122,6 @@ export const PromptBoxVideo = ({
|
||||
const setGenerationCount = usePromptVideoStore((s) => s.setGenerationCount);
|
||||
const [isEnqueueing, setIsEnqueueing] = useState(false);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const toggleExpand = () => {
|
||||
setIsExpanded((prev) => {
|
||||
const next = !prev;
|
||||
if (textareaRef.current) {
|
||||
if (next) {
|
||||
textareaRef.current.style.height = `${window.innerHeight - 300}px`;
|
||||
} else {
|
||||
textareaRef.current.style.height = "auto";
|
||||
}
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
const [selectedGalleryImages, setSelectedGalleryImages] = useState<string[]>(
|
||||
[],
|
||||
);
|
||||
@@ -461,7 +444,7 @@ export const PromptBoxVideo = ({
|
||||
value.slice(0, selectionStart) + pastedText + value.slice(selectionEnd);
|
||||
setPrompt(next);
|
||||
requestAnimationFrame(() => {
|
||||
const pos = Math.min(selectionStart + pastedText.length, next.length);
|
||||
const pos = selectionStart + pastedText.length;
|
||||
textareaRef.current?.setSelectionRange(pos, pos);
|
||||
});
|
||||
};
|
||||
@@ -494,18 +477,12 @@ export const PromptBoxVideo = ({
|
||||
mentionAnchorRef.current = null;
|
||||
};
|
||||
|
||||
const maxLen = selectedModel?.maxPromptLength ?? 1000;
|
||||
|
||||
const handleEnqueue = async () => {
|
||||
if (!prompt.trim()) {
|
||||
console.warn("Cannot generate video: prompt is empty");
|
||||
toast.error("Please enter a prompt to generate video");
|
||||
return;
|
||||
}
|
||||
if (prompt.length > maxLen) {
|
||||
toast.error(`Prompt exceeds the ${maxLen} character limit for this model`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedModel) {
|
||||
console.warn("Cannot generate video: no model selected");
|
||||
@@ -788,7 +765,7 @@ export const PromptBoxVideo = ({
|
||||
)}
|
||||
<div
|
||||
className={twMerge(
|
||||
"glass relative w-[860px] rounded-xl p-4",
|
||||
"glass w-[730px] rounded-xl p-4",
|
||||
isImageRowVisible && "rounded-t-none",
|
||||
isFocused
|
||||
? "ring-1 ring-primary border-primary"
|
||||
@@ -872,7 +849,7 @@ export const PromptBoxVideo = ({
|
||||
</Button>
|
||||
</Tooltip> */}
|
||||
|
||||
<div className="promptbox-resize-wrap relative flex-1">
|
||||
<div className="relative flex-1">
|
||||
{isReferenceMode && hasAnyRefs && (
|
||||
<div
|
||||
ref={highlightRef}
|
||||
@@ -891,7 +868,7 @@ export const PromptBoxVideo = ({
|
||||
: "Describe what you want to happen in the video..."
|
||||
}
|
||||
className={twMerge(
|
||||
"promptbox-scrollbar text-md relative mb-2 min-h-[2.5em] w-full resize-y overflow-y-auto rounded bg-transparent pb-2 pr-2 pt-1 placeholder-base-fg/60 focus:outline-none",
|
||||
"text-md relative mb-2 min-h-[2.5em] w-full resize-y overflow-y-auto rounded bg-transparent pb-2 pr-2 pt-1 placeholder-base-fg/60 focus:outline-none",
|
||||
isReferenceMode && hasAnyRefs
|
||||
? "text-transparent caret-base-fg"
|
||||
: "text-base-fg",
|
||||
@@ -904,9 +881,6 @@ export const PromptBoxVideo = ({
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
/>
|
||||
<span className={`absolute -bottom-1 right-0 text-[10px] tabular-nums ${prompt.length > maxLen ? "text-red-500" : "text-base-fg/40"}`}>
|
||||
{prompt.length} / {maxLen}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center justify-between gap-2">
|
||||
@@ -1051,17 +1025,6 @@ export const PromptBoxVideo = ({
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute -bottom-1 left-1/2 -translate-x-1/2">
|
||||
<Tooltip content={isExpanded ? "Collapse" : "Expand"} position="top" className="-mb-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleExpand}
|
||||
className="text-base-fg/30 hover:text-base-fg/90 transition-colors px-3 py-0.5"
|
||||
>
|
||||
<FontAwesomeIcon icon={isExpanded ? faChevronUp : faChevronDown} className="text-xs" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{selectedModel?.id === "seedance_2p0" && (
|
||||
<div className="flex items-start gap-2.5 rounded-lg border border-yellow-500/40 bg-yellow-500/10 px-3.5 py-2.5 text-xs text-yellow-200">
|
||||
|
||||
@@ -94,7 +94,6 @@ export class ImageModel extends Model {
|
||||
resolutions?: CommonResolution[];
|
||||
defaultAspectRatio?: CommonAspectRatio;
|
||||
defaultResolution?: CommonResolution;
|
||||
maxPromptLength?: number;
|
||||
}) {
|
||||
if (args.maxGenerationCount < 1) {
|
||||
throw new Error("maxGenerationCount must be at least 1");
|
||||
|
||||
@@ -50,9 +50,6 @@ export class Model {
|
||||
// Time in milliseconds for the fake progress bar to reach 100% (UI-only)
|
||||
readonly progressBarTime: number;
|
||||
|
||||
// Maximum character length for the prompt text input
|
||||
readonly maxPromptLength: number;
|
||||
|
||||
protected constructor(args: {
|
||||
id: string;
|
||||
tauriId: string;
|
||||
@@ -66,7 +63,6 @@ export class Model {
|
||||
providers?: GenerationProvider[];
|
||||
preferredProvidersByPage?: Partial<Record<string, string[]>>;
|
||||
progressBarTime?: number;
|
||||
maxPromptLength?: number;
|
||||
}) {
|
||||
this.id = args.id;
|
||||
this.tauriId = args.tauriId;
|
||||
@@ -80,7 +76,6 @@ export class Model {
|
||||
this.providers = args.providers;
|
||||
this.preferredProvidersByPage = args.preferredProvidersByPage;
|
||||
this.progressBarTime = args.progressBarTime ?? 20000;
|
||||
this.maxPromptLength = args.maxPromptLength ?? 1000;
|
||||
}
|
||||
|
||||
getProviders(): GenerationProvider[] {
|
||||
|
||||
@@ -23,7 +23,6 @@ export class SplatModel extends Model {
|
||||
tags?: ModelTag[];
|
||||
providers?: GenerationProvider[];
|
||||
progressBarTime?: number;
|
||||
maxPromptLength?: number;
|
||||
}) {
|
||||
super(args);
|
||||
}
|
||||
|
||||
@@ -94,7 +94,6 @@ export class VideoModel extends Model {
|
||||
defaultResolution?: string;
|
||||
supportsSystemPrompt?: boolean;
|
||||
supportsCommonAspectRatio?: boolean;
|
||||
maxPromptLength?: number;
|
||||
}) {
|
||||
super(args);
|
||||
this.startFrame = args.startFrame;
|
||||
|
||||
@@ -27,7 +27,6 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
canUseImagePrompt: false,
|
||||
maxImagePromptCount: 6,
|
||||
progressBarTime: 10000,
|
||||
maxPromptLength: 8000,
|
||||
canChangeAspectRatio: true,
|
||||
defaultAspectRatio: CommonAspectRatio.Square,
|
||||
aspectRatios: [
|
||||
@@ -55,7 +54,6 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
canUseImagePrompt: false,
|
||||
maxImagePromptCount: 6,
|
||||
progressBarTime: 45000,
|
||||
maxPromptLength: 6000,
|
||||
canChangeAspectRatio: true,
|
||||
// TODO: Rust client doesn't support aspect ratios for MidJourney yet.
|
||||
}),
|
||||
@@ -73,7 +71,6 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
canUseImagePrompt: true,
|
||||
maxImagePromptCount: 6,
|
||||
progressBarTime: 35000,
|
||||
maxPromptLength: 4000,
|
||||
canChangeAspectRatio: true,
|
||||
defaultAspectRatio: CommonAspectRatio.Square,
|
||||
aspectRatios: [
|
||||
@@ -102,7 +99,6 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
canUseImagePrompt: true,
|
||||
maxImagePromptCount: 6,
|
||||
progressBarTime: 10000,
|
||||
maxPromptLength: 4000,
|
||||
canChangeAspectRatio: true,
|
||||
defaultAspectRatio: CommonAspectRatio.Square,
|
||||
aspectRatios: [
|
||||
@@ -127,7 +123,6 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
canUseImagePrompt: true,
|
||||
maxImagePromptCount: 6,
|
||||
progressBarTime: 10000,
|
||||
maxPromptLength: 4000,
|
||||
canChangeAspectRatio: true,
|
||||
defaultAspectRatio: CommonAspectRatio.Square,
|
||||
aspectRatios: [
|
||||
@@ -153,7 +148,6 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
canUseImagePrompt: true,
|
||||
maxImagePromptCount: 6,
|
||||
progressBarTime: 10000,
|
||||
maxPromptLength: 4000,
|
||||
canChangeAspectRatio: true,
|
||||
defaultAspectRatio: CommonAspectRatio.Square,
|
||||
aspectRatios: [
|
||||
@@ -182,7 +176,6 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
canTextToImage: true,
|
||||
tags: [ModelTag.InstructiveEdit],
|
||||
progressBarTime: 25000,
|
||||
maxPromptLength: 2500,
|
||||
// canChangeResolution: true,
|
||||
canChangeAspectRatio: true,
|
||||
defaultAspectRatio: CommonAspectRatio.Square,
|
||||
@@ -217,7 +210,6 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
canTextToImage: true,
|
||||
tags: [ModelTag.InstructiveEdit],
|
||||
progressBarTime: 25000,
|
||||
maxPromptLength: 2500,
|
||||
canChangeResolution: true,
|
||||
canChangeAspectRatio: true,
|
||||
defaultAspectRatio: CommonAspectRatio.Square,
|
||||
@@ -257,7 +249,6 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
canTextToImage: true,
|
||||
tags: [ModelTag.InstructiveEdit],
|
||||
progressBarTime: 25000,
|
||||
maxPromptLength: 2500,
|
||||
canChangeResolution: true,
|
||||
canChangeAspectRatio: true,
|
||||
defaultAspectRatio: CommonAspectRatio.Square,
|
||||
@@ -287,7 +278,7 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
category: "image",
|
||||
creator: ModelCreator.OpenAi,
|
||||
providers: [
|
||||
GenerationProvider.Artcraft,
|
||||
GenerationProvider.Artcraft,
|
||||
GenerationProvider.Sora
|
||||
],
|
||||
selectorName: "GPT Image 1 (GPT-4o)",
|
||||
@@ -300,7 +291,6 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
canUseImagePrompt: true,
|
||||
maxImagePromptCount: 6,
|
||||
progressBarTime: 60000,
|
||||
maxPromptLength: 1500,
|
||||
canChangeAspectRatio: true,
|
||||
}),
|
||||
new ImageModel({
|
||||
@@ -319,7 +309,6 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
canUseImagePrompt: true,
|
||||
maxImagePromptCount: 6,
|
||||
progressBarTime: 60000,
|
||||
maxPromptLength: 1500,
|
||||
canChangeAspectRatio: true,
|
||||
defaultAspectRatio: CommonAspectRatio.Square,
|
||||
aspectRatios: [
|
||||
@@ -344,7 +333,6 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
canUseImagePrompt: true,
|
||||
maxImagePromptCount: 6,
|
||||
progressBarTime: 60000,
|
||||
maxPromptLength: 4000,
|
||||
canChangeAspectRatio: true,
|
||||
defaultAspectRatio: CommonAspectRatio.SquareHd,
|
||||
aspectRatios: [
|
||||
@@ -374,7 +362,6 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
canUseImagePrompt: true,
|
||||
maxImagePromptCount: 6,
|
||||
progressBarTime: 60000,
|
||||
maxPromptLength: 4000,
|
||||
canChangeAspectRatio: true,
|
||||
defaultAspectRatio: CommonAspectRatio.SquareHd,
|
||||
aspectRatios: [
|
||||
@@ -404,7 +391,6 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
canUseImagePrompt: true,
|
||||
maxImagePromptCount: 6,
|
||||
progressBarTime: 60000,
|
||||
maxPromptLength: 4000,
|
||||
canChangeAspectRatio: true,
|
||||
defaultAspectRatio: CommonAspectRatio.SquareHd,
|
||||
aspectRatios: [
|
||||
@@ -428,7 +414,6 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
selectorBadges: ["30 sec."],
|
||||
maxGenerationCount: 4,
|
||||
defaultGenerationCount: 1,
|
||||
maxPromptLength: 800,
|
||||
canTextToImage: false,
|
||||
canEditImages: false,
|
||||
canEditAngles: true,
|
||||
@@ -457,7 +442,6 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
selectorBadges: ["30 sec."],
|
||||
maxGenerationCount: 4,
|
||||
defaultGenerationCount: 1,
|
||||
maxPromptLength: 4000,
|
||||
canTextToImage: false,
|
||||
canEditImages: false,
|
||||
canEditAngles: true,
|
||||
@@ -486,7 +470,6 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
selectorBadges: ["20 sec."],
|
||||
maxGenerationCount: 4,
|
||||
defaultGenerationCount: 4,
|
||||
maxPromptLength: 4000,
|
||||
canEditImages: true,
|
||||
canTextToImage: false,
|
||||
tags: [ModelTag.InstructiveEdit],
|
||||
@@ -503,7 +486,6 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
selectorDescription: "Fast inpainting",
|
||||
selectorBadges: ["30 sec."],
|
||||
maxGenerationCount: 1, // NB: Fal only allows one image for some reason!
|
||||
maxPromptLength: 4000,
|
||||
defaultGenerationCount: 1, // NB: Fal only allows one image for some reason!
|
||||
canEditImages: true,
|
||||
usesInpaintingMask: true,
|
||||
@@ -523,7 +505,6 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
selectorBadges: ["10 sec."],
|
||||
maxGenerationCount: 4,
|
||||
defaultGenerationCount: 4,
|
||||
maxPromptLength: 4000,
|
||||
canEditImages: true,
|
||||
usesInpaintingMask: true,
|
||||
editingIsInpainting: true,
|
||||
|
||||
@@ -18,7 +18,6 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
endFrame: false,
|
||||
requiresImage: true,
|
||||
progressBarTime: 50000,
|
||||
maxPromptLength: 4096,
|
||||
sizeOptions: [
|
||||
{
|
||||
tauriValue: "landscape",
|
||||
@@ -50,7 +49,6 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
endFrame: true,
|
||||
requiresImage: true,
|
||||
progressBarTime: 300000,
|
||||
maxPromptLength: 2500,
|
||||
generateWithSound: false,
|
||||
}),
|
||||
new VideoModel({
|
||||
@@ -66,7 +64,6 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
endFrame: true,
|
||||
requiresImage: true,
|
||||
progressBarTime: 300000,
|
||||
maxPromptLength: 2500,
|
||||
generateWithSound: false,
|
||||
}),
|
||||
new VideoModel({
|
||||
@@ -82,7 +79,6 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
endFrame: false,
|
||||
requiresImage: true,
|
||||
progressBarTime: 300000,
|
||||
maxPromptLength: 2500,
|
||||
generateWithSound: false,
|
||||
}),
|
||||
new VideoModel({
|
||||
@@ -98,7 +94,6 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
endFrame: true,
|
||||
requiresImage: false,
|
||||
progressBarTime: 300000,
|
||||
maxPromptLength: 2500,
|
||||
generateWithSound: false,
|
||||
}),
|
||||
new VideoModel({
|
||||
@@ -114,7 +109,6 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
endFrame: false,
|
||||
requiresImage: true,
|
||||
progressBarTime: 300000,
|
||||
maxPromptLength: 2500,
|
||||
generateWithSound: false,
|
||||
}),
|
||||
new VideoModel({
|
||||
@@ -150,7 +144,6 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
durationOptions: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
||||
defaultDuration: 5,
|
||||
progressBarTime: 300000,
|
||||
maxPromptLength: 2500,
|
||||
generateWithSound: true,
|
||||
}),
|
||||
new VideoModel({
|
||||
@@ -186,7 +179,6 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
durationOptions: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
||||
defaultDuration: 5,
|
||||
progressBarTime: 300000,
|
||||
maxPromptLength: 2500,
|
||||
generateWithSound: true,
|
||||
}),
|
||||
new VideoModel({
|
||||
@@ -202,7 +194,6 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
endFrame: false,
|
||||
requiresImage: true,
|
||||
progressBarTime: 100000,
|
||||
maxPromptLength: 3000,
|
||||
}),
|
||||
new VideoModel({
|
||||
id: "seedance_1p5_pro",
|
||||
@@ -255,7 +246,6 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
supportsSystemPrompt: false,
|
||||
generateWithSound: true,
|
||||
progressBarTime: 180000,
|
||||
maxPromptLength: 3000,
|
||||
}),
|
||||
new VideoModel({
|
||||
id: "seedance_2p0",
|
||||
@@ -300,16 +290,15 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
durationOptions: [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
||||
defaultDuration: 5,
|
||||
supportsReferenceMode: true,
|
||||
maxReferenceImages: 9,
|
||||
maxReferenceVideos: 3,
|
||||
maxReferenceImages: 6,
|
||||
maxReferenceVideos: 2,
|
||||
maxVideoRefDuration: 15,
|
||||
maxReferenceAudios: 3,
|
||||
maxReferenceAudios: 2,
|
||||
maxAudioRefDuration: 15,
|
||||
resolutionOptions: ["480p", "720p"],
|
||||
defaultResolution: "720p",
|
||||
supportsSystemPrompt: false,
|
||||
progressBarTime: 10800000, //3 hours for now
|
||||
maxPromptLength: 3000,
|
||||
}),
|
||||
new VideoModel({
|
||||
id: "sora_2",
|
||||
@@ -337,7 +326,6 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
},
|
||||
],
|
||||
progressBarTime: 100000,
|
||||
maxPromptLength: 2000,
|
||||
}),
|
||||
new VideoModel({
|
||||
id: "sora_2_pro",
|
||||
@@ -364,7 +352,6 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
},
|
||||
],
|
||||
progressBarTime: 100000,
|
||||
maxPromptLength: 2000,
|
||||
}),
|
||||
new VideoModel({
|
||||
id: "veo_2",
|
||||
@@ -380,7 +367,6 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
requiresImage: true,
|
||||
generateWithSound: false,
|
||||
progressBarTime: 100000,
|
||||
maxPromptLength: 5000,
|
||||
}),
|
||||
new VideoModel({
|
||||
id: "veo_3",
|
||||
@@ -396,7 +382,6 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
requiresImage: true,
|
||||
generateWithSound: true,
|
||||
progressBarTime: 100000,
|
||||
maxPromptLength: 5000,
|
||||
}),
|
||||
new VideoModel({
|
||||
id: "veo_3_fast",
|
||||
@@ -412,7 +397,6 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
requiresImage: true,
|
||||
generateWithSound: true,
|
||||
progressBarTime: 100000,
|
||||
maxPromptLength: 5000,
|
||||
}),
|
||||
new VideoModel({
|
||||
id: "veo_3p1",
|
||||
@@ -428,7 +412,6 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
requiresImage: true,
|
||||
generateWithSound: true,
|
||||
progressBarTime: 100000,
|
||||
maxPromptLength: 5000,
|
||||
}),
|
||||
new VideoModel({
|
||||
id: "veo_3p1_fast",
|
||||
@@ -444,7 +427,6 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
requiresImage: true,
|
||||
generateWithSound: true,
|
||||
progressBarTime: 100000,
|
||||
maxPromptLength: 5000,
|
||||
}),
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user