mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 05:20:58 -05:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddedaeb47e | ||
|
|
525fdb7f80 | ||
|
|
a71b0a8e7a | ||
|
|
d462f65891 | ||
|
|
827bbc613f | ||
|
|
2ba11c9b3d |
+47
-3
@@ -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`\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`,\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 ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -82,6 +82,46 @@
|
||||
"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": {
|
||||
@@ -95,8 +135,12 @@
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "0757d81ba153b2ad2df25df3dd92b655ef08b95f5f83251bf3573f9be3a9f8d1"
|
||||
"hash": "cbdaa45597de14d1eb6dc0f6a95788d56334757bb3d03eda2eb64df3ba2a0144"
|
||||
}
|
||||
+6
@@ -1,3 +1,5 @@
|
||||
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};
|
||||
@@ -30,4 +32,8 @@ 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>,
|
||||
}
|
||||
|
||||
@@ -14,4 +14,16 @@ 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,6 +135,8 @@ 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,6 +135,8 @@ 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,6 +202,11 @@ 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,6 +167,10 @@ 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)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,4 +14,16 @@ 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,5 +8,10 @@ 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,5 +144,9 @@ 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,
|
||||
}
|
||||
}
|
||||
|
||||
+11
-1
@@ -1,7 +1,9 @@
|
||||
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;
|
||||
@@ -19,6 +21,10 @@ 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(
|
||||
@@ -36,7 +42,11 @@ 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`
|
||||
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
|
||||
FROM generic_inference_jobs as j
|
||||
LEFT OUTER JOIN users as u
|
||||
ON j.maybe_creator_user_token = u.token
|
||||
|
||||
+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)]
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd, Deserialize, Serialize, Default, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum InferenceJobExternalThirdParty {
|
||||
/// Fal jobs
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
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,7 +23,17 @@ pub enum CommonResolution {
|
||||
ThreeK,
|
||||
FourK,
|
||||
|
||||
// TODO: Add 480p(?), 720p(?), 1080p, etc. as there are some models that use these
|
||||
/// 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,
|
||||
}
|
||||
|
||||
impl_enum_display_and_debug_using_to_str!(CommonResolution);
|
||||
@@ -37,6 +47,10 @@ 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",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +60,10 @@ 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())),
|
||||
}
|
||||
}
|
||||
@@ -56,6 +74,10 @@ impl CommonResolution {
|
||||
Self::TwoK,
|
||||
Self::ThreeK,
|
||||
Self::FourK,
|
||||
Self::HalfK,
|
||||
Self::FourEightyP,
|
||||
Self::SevenTwentyP,
|
||||
Self::TenEightyP,
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -76,6 +98,10 @@ 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]
|
||||
@@ -84,6 +110,10 @@ 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]
|
||||
@@ -92,6 +122,10 @@ 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]
|
||||
@@ -108,11 +142,15 @@ mod tests {
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
let mut variants = CommonResolution::all_variants();
|
||||
assert_eq!(variants.len(), 4);
|
||||
assert_eq!(variants.len(), 8);
|
||||
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,6 +1,7 @@
|
||||
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;
|
||||
|
||||
@@ -197,6 +197,7 @@ 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;
|
||||
@@ -680,6 +681,7 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
ListUserJobsPathInfo,
|
||||
ListUserJobsResponse,
|
||||
ListUserJobsEntry,
|
||||
InferenceJobExternalThirdParty,
|
||||
ListUserWalletsPathInfo,
|
||||
ListUserWalletsResponse,
|
||||
ListUserWalletsEntry,
|
||||
|
||||
+4
@@ -168,5 +168,9 @@ 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,5 +178,9 @@ 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
@@ -56,6 +56,10 @@ 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 {
|
||||
|
||||
@@ -375,25 +375,53 @@ export class MouseControls {
|
||||
|
||||
if ((event.ctrlKey || event.metaKey) && !this.isProcessing) {
|
||||
const keyLower = event.key.toLowerCase();
|
||||
if (keyLower === "z") {
|
||||
// redo
|
||||
if (keyLower === "z" && !event.shiftKey) {
|
||||
// undo
|
||||
event.preventDefault();
|
||||
this.isProcessing = true;
|
||||
await this.sceneManager?.redo();
|
||||
this.isProcessing = false;
|
||||
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;
|
||||
}
|
||||
} else if (keyLower === "c") {
|
||||
// Copy
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.isProcessing = true;
|
||||
await this.sceneManager?.copy();
|
||||
this.isProcessing = false;
|
||||
try {
|
||||
await this.sceneManager?.copy();
|
||||
} finally {
|
||||
this.isProcessing = false;
|
||||
}
|
||||
} else if (keyLower === "v") {
|
||||
// Paste
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.isProcessing = true;
|
||||
await this.sceneManager?.paste();
|
||||
this.isProcessing = false;
|
||||
try {
|
||||
await this.sceneManager?.paste();
|
||||
} finally {
|
||||
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() {
|
||||
this.undoIndex += 1;
|
||||
if (this.undoIndex >= this.undoStack.length) {
|
||||
this.undoIndex = this.undoStack.length;
|
||||
return;
|
||||
}
|
||||
this.undoIndex += 1;
|
||||
const undoCommand = this.undoStack.at(
|
||||
this.undoStack.length - this.undoIndex,
|
||||
);
|
||||
@@ -131,18 +131,30 @@ 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;
|
||||
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();
|
||||
}
|
||||
|
||||
// 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,
|
||||
);
|
||||
this.undoIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public async create(
|
||||
media_token: string,
|
||||
name: string,
|
||||
@@ -467,6 +479,7 @@ 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,
|
||||
@@ -488,6 +501,7 @@ 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 != "") {
|
||||
@@ -558,7 +572,6 @@ 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.disabledBy === level;
|
||||
hotkeysStatus.value = { ...hotkeysStatus.value, disabledBy: level };
|
||||
}
|
||||
}else{
|
||||
hotkeysStatus.value = {
|
||||
|
||||
@@ -244,6 +244,55 @@ 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 */
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
faMessageCheck,
|
||||
faFrame,
|
||||
faExpand,
|
||||
faChevronDown,
|
||||
faChevronUp,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import {
|
||||
faRectangleVertical,
|
||||
@@ -25,6 +27,7 @@ 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";
|
||||
|
||||
@@ -68,6 +71,17 @@ 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();
|
||||
@@ -178,7 +192,7 @@ export const PromptBox2D = ({
|
||||
value.slice(0, selectionStart) + pastedText + value.slice(selectionEnd);
|
||||
setPrompt(next);
|
||||
requestAnimationFrame(() => {
|
||||
const pos = selectionStart + pastedText.length;
|
||||
const pos = Math.min(selectionStart + pastedText.length, next.length);
|
||||
textareaRef.current?.setSelectionRange(pos, pos);
|
||||
});
|
||||
};
|
||||
@@ -247,9 +261,15 @@ 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);
|
||||
@@ -350,7 +370,7 @@ export const PromptBox2D = ({
|
||||
)}
|
||||
<div
|
||||
className={twMerge(
|
||||
"glass w-[860px] rounded-xl p-4",
|
||||
"glass relative w-[860px] rounded-xl p-4",
|
||||
selectedImageModel?.canUseImagePrompt &&
|
||||
isImageRowVisible &&
|
||||
"rounded-t-none",
|
||||
@@ -390,18 +410,23 @@ export const PromptBox2D = ({
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<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 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>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -483,6 +508,17 @@ 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,6 +8,8 @@ import {
|
||||
faMessageCheck,
|
||||
faMessageXmark,
|
||||
faExpand,
|
||||
faChevronDown,
|
||||
faChevronUp,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import {
|
||||
faRectangleWide,
|
||||
@@ -114,6 +116,17 @@ 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);
|
||||
@@ -163,7 +176,7 @@ export const PromptBox3D = ({
|
||||
>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (textareaRef.current) {
|
||||
if (textareaRef.current && !isExpanded) {
|
||||
textareaRef.current.style.height = "auto";
|
||||
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
|
||||
}
|
||||
@@ -238,7 +251,7 @@ export const PromptBox3D = ({
|
||||
value.slice(0, selectionStart) + pastedText + value.slice(selectionEnd);
|
||||
setPrompt(next);
|
||||
requestAnimationFrame(() => {
|
||||
const pos = selectionStart + pastedText.length;
|
||||
const pos = Math.min(selectionStart + pastedText.length, next.length);
|
||||
textareaRef.current?.setSelectionRange(pos, pos);
|
||||
});
|
||||
};
|
||||
@@ -253,7 +266,13 @@ 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);
|
||||
@@ -548,7 +567,7 @@ export const PromptBox3D = ({
|
||||
)}
|
||||
<div
|
||||
className={twMerge(
|
||||
"glass w-[860px] rounded-xl p-4",
|
||||
"glass relative w-[860px] rounded-xl p-4",
|
||||
isPromptBoxFocused.value ? "!border !border-primary" : "",
|
||||
selectedImageModel?.canUseImagePrompt &&
|
||||
isImageRowVisible &&
|
||||
@@ -594,24 +613,29 @@ export const PromptBox3D = ({
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<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 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>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -749,6 +773,17 @@ 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,6 +15,8 @@ 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";
|
||||
@@ -26,6 +28,7 @@ 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";
|
||||
@@ -82,6 +85,17 @@ 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);
|
||||
@@ -205,7 +219,7 @@ export const PromptBoxEdit = ({
|
||||
value.slice(0, selectionStart) + pastedText + value.slice(selectionEnd);
|
||||
setPrompt(next);
|
||||
requestAnimationFrame(() => {
|
||||
const pos = selectionStart + pastedText.length;
|
||||
const pos = Math.min(selectionStart + pastedText.length, next.length);
|
||||
textareaRef.current?.setSelectionRange(pos, pos);
|
||||
});
|
||||
};
|
||||
@@ -238,9 +252,15 @@ 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);
|
||||
@@ -358,7 +378,7 @@ export const PromptBoxEdit = ({
|
||||
)}
|
||||
<div
|
||||
className={twMerge(
|
||||
"glass w-[860px] rounded-xl p-4",
|
||||
"glass relative w-[860px] rounded-xl p-4",
|
||||
isFocused && "ring-1 ring-primary border-primary",
|
||||
selectedImageModel?.canUseImagePrompt &&
|
||||
isImageRowVisible &&
|
||||
@@ -398,18 +418,23 @@ export const PromptBoxEdit = ({
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<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 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>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -501,6 +526,17 @@ 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,6 +16,8 @@ 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";
|
||||
@@ -96,6 +98,21 @@ 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<
|
||||
@@ -225,7 +242,7 @@ export const PromptBoxImage = ({
|
||||
value.slice(0, selectionStart) + pastedText + value.slice(selectionEnd);
|
||||
setPrompt(next);
|
||||
requestAnimationFrame(() => {
|
||||
const pos = selectionStart + pastedText.length;
|
||||
const pos = Math.min(selectionStart + pastedText.length, next.length);
|
||||
textareaRef.current?.setSelectionRange(pos, pos);
|
||||
});
|
||||
};
|
||||
@@ -234,11 +251,17 @@ 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");
|
||||
@@ -408,7 +431,7 @@ export const PromptBoxImage = ({
|
||||
|
||||
<div
|
||||
className={twMerge(
|
||||
"glass w-[860px] rounded-xl p-4",
|
||||
"glass relative w-[860px] rounded-xl p-4",
|
||||
isImageRowVisible &&
|
||||
selectedModel?.canUseImagePrompt &&
|
||||
"rounded-t-none",
|
||||
@@ -451,18 +474,23 @@ export const PromptBoxImage = ({
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<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 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>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -539,6 +567,17 @@ 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,6 +17,8 @@ import {
|
||||
faWaveformLines,
|
||||
faClock,
|
||||
faTriangleExclamation,
|
||||
faChevronDown,
|
||||
faChevronUp,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import {
|
||||
faCircleInfo,
|
||||
@@ -122,6 +124,21 @@ 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[]>(
|
||||
[],
|
||||
);
|
||||
@@ -444,7 +461,7 @@ export const PromptBoxVideo = ({
|
||||
value.slice(0, selectionStart) + pastedText + value.slice(selectionEnd);
|
||||
setPrompt(next);
|
||||
requestAnimationFrame(() => {
|
||||
const pos = selectionStart + pastedText.length;
|
||||
const pos = Math.min(selectionStart + pastedText.length, next.length);
|
||||
textareaRef.current?.setSelectionRange(pos, pos);
|
||||
});
|
||||
};
|
||||
@@ -477,12 +494,18 @@ 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");
|
||||
@@ -765,7 +788,7 @@ export const PromptBoxVideo = ({
|
||||
)}
|
||||
<div
|
||||
className={twMerge(
|
||||
"glass w-[860px] rounded-xl p-4",
|
||||
"glass relative w-[860px] rounded-xl p-4",
|
||||
isImageRowVisible && "rounded-t-none",
|
||||
isFocused
|
||||
? "ring-1 ring-primary border-primary"
|
||||
@@ -849,7 +872,7 @@ export const PromptBoxVideo = ({
|
||||
</Button>
|
||||
</Tooltip> */}
|
||||
|
||||
<div className="relative flex-1">
|
||||
<div className="promptbox-resize-wrap relative flex-1">
|
||||
{isReferenceMode && hasAnyRefs && (
|
||||
<div
|
||||
ref={highlightRef}
|
||||
@@ -868,7 +891,7 @@ export const PromptBoxVideo = ({
|
||||
: "Describe what you want to happen in the video..."
|
||||
}
|
||||
className={twMerge(
|
||||
"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",
|
||||
"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",
|
||||
isReferenceMode && hasAnyRefs
|
||||
? "text-transparent caret-base-fg"
|
||||
: "text-base-fg",
|
||||
@@ -881,6 +904,9 @@ 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">
|
||||
@@ -1025,6 +1051,17 @@ 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,6 +94,7 @@ 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,6 +50,9 @@ 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;
|
||||
@@ -63,6 +66,7 @@ export class Model {
|
||||
providers?: GenerationProvider[];
|
||||
preferredProvidersByPage?: Partial<Record<string, string[]>>;
|
||||
progressBarTime?: number;
|
||||
maxPromptLength?: number;
|
||||
}) {
|
||||
this.id = args.id;
|
||||
this.tauriId = args.tauriId;
|
||||
@@ -76,6 +80,7 @@ export class Model {
|
||||
this.providers = args.providers;
|
||||
this.preferredProvidersByPage = args.preferredProvidersByPage;
|
||||
this.progressBarTime = args.progressBarTime ?? 20000;
|
||||
this.maxPromptLength = args.maxPromptLength ?? 1000;
|
||||
}
|
||||
|
||||
getProviders(): GenerationProvider[] {
|
||||
|
||||
@@ -23,6 +23,7 @@ export class SplatModel extends Model {
|
||||
tags?: ModelTag[];
|
||||
providers?: GenerationProvider[];
|
||||
progressBarTime?: number;
|
||||
maxPromptLength?: number;
|
||||
}) {
|
||||
super(args);
|
||||
}
|
||||
|
||||
@@ -94,6 +94,7 @@ export class VideoModel extends Model {
|
||||
defaultResolution?: string;
|
||||
supportsSystemPrompt?: boolean;
|
||||
supportsCommonAspectRatio?: boolean;
|
||||
maxPromptLength?: number;
|
||||
}) {
|
||||
super(args);
|
||||
this.startFrame = args.startFrame;
|
||||
|
||||
@@ -27,6 +27,7 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
canUseImagePrompt: false,
|
||||
maxImagePromptCount: 6,
|
||||
progressBarTime: 10000,
|
||||
maxPromptLength: 8000,
|
||||
canChangeAspectRatio: true,
|
||||
defaultAspectRatio: CommonAspectRatio.Square,
|
||||
aspectRatios: [
|
||||
@@ -54,6 +55,7 @@ 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.
|
||||
}),
|
||||
@@ -71,6 +73,7 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
canUseImagePrompt: true,
|
||||
maxImagePromptCount: 6,
|
||||
progressBarTime: 35000,
|
||||
maxPromptLength: 4000,
|
||||
canChangeAspectRatio: true,
|
||||
defaultAspectRatio: CommonAspectRatio.Square,
|
||||
aspectRatios: [
|
||||
@@ -99,6 +102,7 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
canUseImagePrompt: true,
|
||||
maxImagePromptCount: 6,
|
||||
progressBarTime: 10000,
|
||||
maxPromptLength: 4000,
|
||||
canChangeAspectRatio: true,
|
||||
defaultAspectRatio: CommonAspectRatio.Square,
|
||||
aspectRatios: [
|
||||
@@ -123,6 +127,7 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
canUseImagePrompt: true,
|
||||
maxImagePromptCount: 6,
|
||||
progressBarTime: 10000,
|
||||
maxPromptLength: 4000,
|
||||
canChangeAspectRatio: true,
|
||||
defaultAspectRatio: CommonAspectRatio.Square,
|
||||
aspectRatios: [
|
||||
@@ -148,6 +153,7 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
canUseImagePrompt: true,
|
||||
maxImagePromptCount: 6,
|
||||
progressBarTime: 10000,
|
||||
maxPromptLength: 4000,
|
||||
canChangeAspectRatio: true,
|
||||
defaultAspectRatio: CommonAspectRatio.Square,
|
||||
aspectRatios: [
|
||||
@@ -176,6 +182,7 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
canTextToImage: true,
|
||||
tags: [ModelTag.InstructiveEdit],
|
||||
progressBarTime: 25000,
|
||||
maxPromptLength: 2500,
|
||||
// canChangeResolution: true,
|
||||
canChangeAspectRatio: true,
|
||||
defaultAspectRatio: CommonAspectRatio.Square,
|
||||
@@ -210,6 +217,7 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
canTextToImage: true,
|
||||
tags: [ModelTag.InstructiveEdit],
|
||||
progressBarTime: 25000,
|
||||
maxPromptLength: 2500,
|
||||
canChangeResolution: true,
|
||||
canChangeAspectRatio: true,
|
||||
defaultAspectRatio: CommonAspectRatio.Square,
|
||||
@@ -249,6 +257,7 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
canTextToImage: true,
|
||||
tags: [ModelTag.InstructiveEdit],
|
||||
progressBarTime: 25000,
|
||||
maxPromptLength: 2500,
|
||||
canChangeResolution: true,
|
||||
canChangeAspectRatio: true,
|
||||
defaultAspectRatio: CommonAspectRatio.Square,
|
||||
@@ -278,7 +287,7 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
category: "image",
|
||||
creator: ModelCreator.OpenAi,
|
||||
providers: [
|
||||
GenerationProvider.Artcraft,
|
||||
GenerationProvider.Artcraft,
|
||||
GenerationProvider.Sora
|
||||
],
|
||||
selectorName: "GPT Image 1 (GPT-4o)",
|
||||
@@ -291,6 +300,7 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
canUseImagePrompt: true,
|
||||
maxImagePromptCount: 6,
|
||||
progressBarTime: 60000,
|
||||
maxPromptLength: 1500,
|
||||
canChangeAspectRatio: true,
|
||||
}),
|
||||
new ImageModel({
|
||||
@@ -309,6 +319,7 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
canUseImagePrompt: true,
|
||||
maxImagePromptCount: 6,
|
||||
progressBarTime: 60000,
|
||||
maxPromptLength: 1500,
|
||||
canChangeAspectRatio: true,
|
||||
defaultAspectRatio: CommonAspectRatio.Square,
|
||||
aspectRatios: [
|
||||
@@ -333,6 +344,7 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
canUseImagePrompt: true,
|
||||
maxImagePromptCount: 6,
|
||||
progressBarTime: 60000,
|
||||
maxPromptLength: 4000,
|
||||
canChangeAspectRatio: true,
|
||||
defaultAspectRatio: CommonAspectRatio.SquareHd,
|
||||
aspectRatios: [
|
||||
@@ -362,6 +374,7 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
canUseImagePrompt: true,
|
||||
maxImagePromptCount: 6,
|
||||
progressBarTime: 60000,
|
||||
maxPromptLength: 4000,
|
||||
canChangeAspectRatio: true,
|
||||
defaultAspectRatio: CommonAspectRatio.SquareHd,
|
||||
aspectRatios: [
|
||||
@@ -391,6 +404,7 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
canUseImagePrompt: true,
|
||||
maxImagePromptCount: 6,
|
||||
progressBarTime: 60000,
|
||||
maxPromptLength: 4000,
|
||||
canChangeAspectRatio: true,
|
||||
defaultAspectRatio: CommonAspectRatio.SquareHd,
|
||||
aspectRatios: [
|
||||
@@ -414,6 +428,7 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
selectorBadges: ["30 sec."],
|
||||
maxGenerationCount: 4,
|
||||
defaultGenerationCount: 1,
|
||||
maxPromptLength: 800,
|
||||
canTextToImage: false,
|
||||
canEditImages: false,
|
||||
canEditAngles: true,
|
||||
@@ -442,6 +457,7 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
selectorBadges: ["30 sec."],
|
||||
maxGenerationCount: 4,
|
||||
defaultGenerationCount: 1,
|
||||
maxPromptLength: 4000,
|
||||
canTextToImage: false,
|
||||
canEditImages: false,
|
||||
canEditAngles: true,
|
||||
@@ -470,6 +486,7 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
selectorBadges: ["20 sec."],
|
||||
maxGenerationCount: 4,
|
||||
defaultGenerationCount: 4,
|
||||
maxPromptLength: 4000,
|
||||
canEditImages: true,
|
||||
canTextToImage: false,
|
||||
tags: [ModelTag.InstructiveEdit],
|
||||
@@ -486,6 +503,7 @@ 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,
|
||||
@@ -505,6 +523,7 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
selectorBadges: ["10 sec."],
|
||||
maxGenerationCount: 4,
|
||||
defaultGenerationCount: 4,
|
||||
maxPromptLength: 4000,
|
||||
canEditImages: true,
|
||||
usesInpaintingMask: true,
|
||||
editingIsInpainting: true,
|
||||
|
||||
@@ -18,6 +18,7 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
endFrame: false,
|
||||
requiresImage: true,
|
||||
progressBarTime: 50000,
|
||||
maxPromptLength: 4096,
|
||||
sizeOptions: [
|
||||
{
|
||||
tauriValue: "landscape",
|
||||
@@ -49,6 +50,7 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
endFrame: true,
|
||||
requiresImage: true,
|
||||
progressBarTime: 300000,
|
||||
maxPromptLength: 2500,
|
||||
generateWithSound: false,
|
||||
}),
|
||||
new VideoModel({
|
||||
@@ -64,6 +66,7 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
endFrame: true,
|
||||
requiresImage: true,
|
||||
progressBarTime: 300000,
|
||||
maxPromptLength: 2500,
|
||||
generateWithSound: false,
|
||||
}),
|
||||
new VideoModel({
|
||||
@@ -79,6 +82,7 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
endFrame: false,
|
||||
requiresImage: true,
|
||||
progressBarTime: 300000,
|
||||
maxPromptLength: 2500,
|
||||
generateWithSound: false,
|
||||
}),
|
||||
new VideoModel({
|
||||
@@ -94,6 +98,7 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
endFrame: true,
|
||||
requiresImage: false,
|
||||
progressBarTime: 300000,
|
||||
maxPromptLength: 2500,
|
||||
generateWithSound: false,
|
||||
}),
|
||||
new VideoModel({
|
||||
@@ -109,6 +114,7 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
endFrame: false,
|
||||
requiresImage: true,
|
||||
progressBarTime: 300000,
|
||||
maxPromptLength: 2500,
|
||||
generateWithSound: false,
|
||||
}),
|
||||
new VideoModel({
|
||||
@@ -144,6 +150,7 @@ 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({
|
||||
@@ -179,6 +186,7 @@ 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({
|
||||
@@ -194,6 +202,7 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
endFrame: false,
|
||||
requiresImage: true,
|
||||
progressBarTime: 100000,
|
||||
maxPromptLength: 3000,
|
||||
}),
|
||||
new VideoModel({
|
||||
id: "seedance_1p5_pro",
|
||||
@@ -246,6 +255,7 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
supportsSystemPrompt: false,
|
||||
generateWithSound: true,
|
||||
progressBarTime: 180000,
|
||||
maxPromptLength: 3000,
|
||||
}),
|
||||
new VideoModel({
|
||||
id: "seedance_2p0",
|
||||
@@ -299,6 +309,7 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
defaultResolution: "720p",
|
||||
supportsSystemPrompt: false,
|
||||
progressBarTime: 10800000, //3 hours for now
|
||||
maxPromptLength: 3000,
|
||||
}),
|
||||
new VideoModel({
|
||||
id: "sora_2",
|
||||
@@ -326,6 +337,7 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
},
|
||||
],
|
||||
progressBarTime: 100000,
|
||||
maxPromptLength: 2000,
|
||||
}),
|
||||
new VideoModel({
|
||||
id: "sora_2_pro",
|
||||
@@ -352,6 +364,7 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
},
|
||||
],
|
||||
progressBarTime: 100000,
|
||||
maxPromptLength: 2000,
|
||||
}),
|
||||
new VideoModel({
|
||||
id: "veo_2",
|
||||
@@ -367,6 +380,7 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
requiresImage: true,
|
||||
generateWithSound: false,
|
||||
progressBarTime: 100000,
|
||||
maxPromptLength: 5000,
|
||||
}),
|
||||
new VideoModel({
|
||||
id: "veo_3",
|
||||
@@ -382,6 +396,7 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
requiresImage: true,
|
||||
generateWithSound: true,
|
||||
progressBarTime: 100000,
|
||||
maxPromptLength: 5000,
|
||||
}),
|
||||
new VideoModel({
|
||||
id: "veo_3_fast",
|
||||
@@ -397,6 +412,7 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
requiresImage: true,
|
||||
generateWithSound: true,
|
||||
progressBarTime: 100000,
|
||||
maxPromptLength: 5000,
|
||||
}),
|
||||
new VideoModel({
|
||||
id: "veo_3p1",
|
||||
@@ -412,6 +428,7 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
requiresImage: true,
|
||||
generateWithSound: true,
|
||||
progressBarTime: 100000,
|
||||
maxPromptLength: 5000,
|
||||
}),
|
||||
new VideoModel({
|
||||
id: "veo_3p1_fast",
|
||||
@@ -427,6 +444,7 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
requiresImage: true,
|
||||
generateWithSound: true,
|
||||
progressBarTime: 100000,
|
||||
maxPromptLength: 5000,
|
||||
}),
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user