1588 lines
57 KiB
Rust
1588 lines
57 KiB
Rust
use std::collections::BTreeMap;
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json::Value;
|
|
|
|
use crate::external_generation::ExternalGenerationJobStatusRecord;
|
|
|
|
pub const EDITOR_ASSET_LABEL_MAX_CHARS: usize = 80;
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CreateDirectUploadTicketRequest {
|
|
pub legacy_prefix: String,
|
|
#[serde(default)]
|
|
pub path_segments: Vec<String>,
|
|
pub file_name: String,
|
|
#[serde(default)]
|
|
pub content_type: Option<String>,
|
|
#[serde(default)]
|
|
pub access: Option<DirectUploadObjectAccess>,
|
|
#[serde(default)]
|
|
pub metadata: BTreeMap<String, String>,
|
|
#[serde(default)]
|
|
pub max_size_bytes: Option<u64>,
|
|
#[serde(default)]
|
|
pub expire_seconds: Option<u64>,
|
|
#[serde(default)]
|
|
pub success_action_status: Option<u16>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct GetReadUrlQuery {
|
|
#[serde(default)]
|
|
pub object_key: Option<String>,
|
|
#[serde(default)]
|
|
pub legacy_public_path: Option<String>,
|
|
#[serde(default)]
|
|
pub expire_seconds: Option<u64>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum ConfirmAssetObjectAccessPolicy {
|
|
Private,
|
|
PublicRead,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum DirectUploadObjectAccess {
|
|
Public,
|
|
Private,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ConfirmAssetObjectRequest {
|
|
#[serde(default)]
|
|
pub bucket: Option<String>,
|
|
pub object_key: String,
|
|
#[serde(default)]
|
|
pub content_type: Option<String>,
|
|
#[serde(default)]
|
|
pub content_length: Option<u64>,
|
|
#[serde(default)]
|
|
pub content_hash: Option<String>,
|
|
pub asset_kind: String,
|
|
#[serde(default)]
|
|
pub access_policy: Option<ConfirmAssetObjectAccessPolicy>,
|
|
#[serde(default)]
|
|
pub source_job_id: Option<String>,
|
|
#[serde(default)]
|
|
pub owner_user_id: Option<String>,
|
|
#[serde(default)]
|
|
pub profile_id: Option<String>,
|
|
#[serde(default)]
|
|
pub entity_id: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct BindAssetObjectRequest {
|
|
pub asset_object_id: String,
|
|
pub entity_kind: String,
|
|
pub entity_id: String,
|
|
pub slot: String,
|
|
pub asset_kind: String,
|
|
#[serde(default)]
|
|
pub owner_user_id: Option<String>,
|
|
#[serde(default)]
|
|
pub profile_id: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct AssetHistoryQuery {
|
|
pub kind: String,
|
|
#[serde(default)]
|
|
pub limit: Option<u32>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct AssetHistoryEntryPayload {
|
|
pub asset_object_id: String,
|
|
pub asset_kind: String,
|
|
pub image_src: String,
|
|
pub owner_user_id: Option<String>,
|
|
pub owner_label: String,
|
|
pub profile_id: Option<String>,
|
|
pub entity_id: Option<String>,
|
|
pub created_at: String,
|
|
pub updated_at: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct AssetHistoryListResponse {
|
|
pub assets: Vec<AssetHistoryEntryPayload>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
pub enum CharacterVisualSourceMode {
|
|
TextToImage,
|
|
ImageToImage,
|
|
Upload,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CharacterVisualGenerateRequest {
|
|
pub character_id: String,
|
|
pub source_mode: CharacterVisualSourceMode,
|
|
pub prompt_text: String,
|
|
#[serde(default)]
|
|
pub reference_image_data_urls: Vec<String>,
|
|
pub candidate_count: u32,
|
|
pub image_model: String,
|
|
pub size: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CharacterVisualDraftPayload {
|
|
pub id: String,
|
|
pub label: String,
|
|
pub image_src: String,
|
|
pub width: u32,
|
|
pub height: u32,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CharacterVisualGenerateResponse {
|
|
pub ok: bool,
|
|
pub task_id: String,
|
|
pub model: String,
|
|
pub prompt: String,
|
|
pub drafts: Vec<CharacterVisualDraftPayload>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum CharacterAssetJobStatusText {
|
|
Queued,
|
|
Running,
|
|
Completed,
|
|
Failed,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CharacterAssetJobStatusPayload {
|
|
pub task_id: String,
|
|
pub kind: String,
|
|
pub status: CharacterAssetJobStatusText,
|
|
pub character_id: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub animation: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub strategy: Option<String>,
|
|
pub model: String,
|
|
pub prompt: String,
|
|
pub created_at: String,
|
|
pub updated_at: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub result: Option<Value>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub error_message: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CharacterVisualPublishRequest {
|
|
pub character_id: String,
|
|
pub source_mode: CharacterVisualSourceMode,
|
|
#[serde(default)]
|
|
pub prompt_text: Option<String>,
|
|
pub selected_preview_source: String,
|
|
#[serde(default)]
|
|
pub preview_sources: Vec<String>,
|
|
pub width: u32,
|
|
pub height: u32,
|
|
#[serde(default)]
|
|
pub update_character_override: Option<bool>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CharacterVisualPublishResponse {
|
|
pub ok: bool,
|
|
pub asset_id: String,
|
|
pub portrait_path: String,
|
|
pub override_map: Value,
|
|
pub save_message: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CharacterAnimationTemplatePayload {
|
|
pub id: String,
|
|
pub label: String,
|
|
pub animation: String,
|
|
pub prompt_suffix: String,
|
|
pub notes: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CharacterAnimationTemplatesResponse {
|
|
pub ok: bool,
|
|
pub templates: Vec<CharacterAnimationTemplatePayload>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CharacterAnimationImportVideoRequest {
|
|
pub character_id: String,
|
|
pub animation: String,
|
|
pub video_source: String,
|
|
#[serde(default)]
|
|
pub source_label: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CharacterAnimationImportVideoResponse {
|
|
pub ok: bool,
|
|
pub imported_video_path: String,
|
|
pub draft_id: String,
|
|
pub save_message: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
pub enum CharacterAnimationStrategy {
|
|
ImageSequence,
|
|
ImageToVideo,
|
|
MotionTransfer,
|
|
ReferenceToVideo,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CharacterAnimationGenerateRequest {
|
|
pub character_id: String,
|
|
pub strategy: CharacterAnimationStrategy,
|
|
pub animation: String,
|
|
pub prompt_text: String,
|
|
#[serde(default)]
|
|
pub character_brief_text: Option<String>,
|
|
#[serde(default)]
|
|
pub action_template_id: Option<String>,
|
|
pub visual_source: String,
|
|
#[serde(default)]
|
|
pub reference_image_data_urls: Vec<String>,
|
|
#[serde(default)]
|
|
pub reference_video_data_urls: Vec<String>,
|
|
#[serde(default)]
|
|
pub last_frame_image_data_url: Option<String>,
|
|
pub frame_count: u32,
|
|
pub fps: u32,
|
|
pub duration_seconds: u32,
|
|
#[serde(rename = "loop")]
|
|
pub loop_: bool,
|
|
pub use_chroma_key: bool,
|
|
pub resolution: String,
|
|
pub ratio: String,
|
|
pub image_sequence_model: String,
|
|
pub video_model: String,
|
|
pub reference_video_model: String,
|
|
pub motion_transfer_model: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CharacterAnimationGenerateResponse {
|
|
pub ok: bool,
|
|
pub task_id: String,
|
|
pub strategy: CharacterAnimationStrategy,
|
|
pub model: String,
|
|
pub prompt: String,
|
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
pub image_sources: Vec<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub preview_video_path: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct EditorCharacterAnimationGenerateRequest {
|
|
pub source_layer_id: String,
|
|
pub source_image_src: String,
|
|
pub source_width: u32,
|
|
pub source_height: u32,
|
|
pub prompt_text: String,
|
|
/// 纯色抠像背景色(hex)。None/auto 走自动决策;通常继承源角色图的背景色。
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub screen_color: Option<String>,
|
|
pub resolution: String,
|
|
pub ratio: String,
|
|
pub frame_count: u32,
|
|
pub duration_seconds: u32,
|
|
pub model: String,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub project_id: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub canvas_completion: Option<EditorCanvasGenerationCompletionPayload>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub generation_inputs: Option<Value>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub source_resource_id: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub asset_folder_id: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub asset_label: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct EditorCharacterAnimationFramePayload {
|
|
pub frame_index: u32,
|
|
pub image_src: String,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub object_key: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub asset_object_id: Option<String>,
|
|
pub width: u32,
|
|
pub height: u32,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct EditorCharacterAnimationGenerateResponse {
|
|
pub ok: bool,
|
|
pub task_id: String,
|
|
pub model: String,
|
|
pub prompt: String,
|
|
pub preview_video_path: String,
|
|
pub frames: Vec<EditorCharacterAnimationFramePayload>,
|
|
pub frame_count: u32,
|
|
pub duration_seconds: u32,
|
|
pub frame_width: u32,
|
|
pub frame_height: u32,
|
|
pub fps: u32,
|
|
pub price_mud_points: u32,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub project: Option<Value>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub queue_state: Option<ExternalGenerationJobStatusRecord>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct EditorVideoGenerateRequest {
|
|
pub prompt: String,
|
|
pub model: String,
|
|
pub aspect_ratio: String,
|
|
pub duration_seconds: u32,
|
|
pub resolution: String,
|
|
pub mode: String,
|
|
pub sound: String,
|
|
#[serde(default = "default_editor_video_web_search_enabled")]
|
|
pub web_search_enabled: bool,
|
|
#[serde(default)]
|
|
pub reference_image_srcs: Vec<String>,
|
|
#[serde(default)]
|
|
pub reference_video_srcs: Vec<String>,
|
|
#[serde(default)]
|
|
pub reference_audio_srcs: Vec<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub project_id: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub canvas_completion: Option<EditorCanvasGenerationCompletionPayload>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub generation_inputs: Option<Value>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub source_resource_id: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub asset_kind: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub asset_folder_id: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub asset_label: Option<String>,
|
|
}
|
|
|
|
fn default_editor_video_web_search_enabled() -> bool {
|
|
true
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct EditorVideoGenerateResponse {
|
|
pub ok: bool,
|
|
pub video_src: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub thumbnail_src: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub object_key: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub asset_object_id: Option<String>,
|
|
pub width: u32,
|
|
pub height: u32,
|
|
pub source_type: String,
|
|
pub prompt: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub actual_prompt: Option<String>,
|
|
pub model: String,
|
|
pub task_id: String,
|
|
pub duration_seconds: u32,
|
|
pub resolution: String,
|
|
pub price_mud_points: u32,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub project: Option<Value>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub resource: Option<Value>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub asset: Option<Value>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub queue_state: Option<ExternalGenerationJobStatusRecord>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct EditorCanvasGenerationPlaceholderPayload {
|
|
pub x: f64,
|
|
pub y: f64,
|
|
pub width: f64,
|
|
pub height: f64,
|
|
pub original_width: f64,
|
|
pub original_height: f64,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct EditorCanvasGenerationCompletionPayload {
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub dialog_id: Option<String>,
|
|
pub title: String,
|
|
pub placeholder: EditorCanvasGenerationPlaceholderPayload,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct EditorIconSpritesheetGenerateRequest {
|
|
pub reference_image_src: String,
|
|
#[serde(default)]
|
|
pub reference_image_srcs: Vec<String>,
|
|
pub icon_descriptions: Vec<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub model: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub aspect_ratio: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub image_size: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct EditorIconSpritesheetIconPayload {
|
|
pub name: String,
|
|
pub image_src: String,
|
|
pub width: u32,
|
|
pub height: u32,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct EditorIconSpritesheetSliceWarningPayload {
|
|
pub code: String,
|
|
pub reason: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct EditorIconSpritesheetGenerateResponse {
|
|
pub spritesheet_image_src: String,
|
|
pub spritesheet_width: u32,
|
|
pub spritesheet_height: u32,
|
|
pub icon_image_srcs: Vec<EditorIconSpritesheetIconPayload>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub slice_warning: Option<EditorIconSpritesheetSliceWarningPayload>,
|
|
pub prompt: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub actual_prompt: Option<String>,
|
|
pub model: String,
|
|
pub task_id: String,
|
|
pub price_mud_points: u32,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct EditorSoundEffectGenerateRequest {
|
|
pub prompt: String,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub model: Option<String>,
|
|
pub duration: u8,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub project_id: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub canvas_completion: Option<EditorCanvasGenerationCompletionPayload>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub generation_inputs: Option<Value>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub asset_folder_id: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub asset_label: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct EditorBackgroundMusicGenerateRequest {
|
|
pub gpt_description_prompt: String,
|
|
pub make_instrumental: bool,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub project_id: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub canvas_completion: Option<EditorCanvasGenerationCompletionPayload>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub generation_inputs: Option<Value>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub asset_folder_id: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub asset_label: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct EditorAudioGenerateResponse {
|
|
pub ok: bool,
|
|
pub audio_src: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub object_key: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub asset_object_id: Option<String>,
|
|
pub width: u32,
|
|
pub height: u32,
|
|
pub source_type: String,
|
|
pub prompt: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub actual_prompt: Option<String>,
|
|
pub model: String,
|
|
pub task_id: String,
|
|
pub price_mud_points: u32,
|
|
pub audio_kind: String,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub project: Option<Value>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub resource: Option<Value>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub asset: Option<Value>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub queue_state: Option<ExternalGenerationJobStatusRecord>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CharacterAnimationDraftPayload {
|
|
#[serde(default)]
|
|
pub frames_data_urls: Vec<String>,
|
|
pub fps: u32,
|
|
#[serde(rename = "loop")]
|
|
pub loop_: bool,
|
|
pub frame_width: u32,
|
|
pub frame_height: u32,
|
|
#[serde(default)]
|
|
pub frame_count: Option<u32>,
|
|
#[serde(default)]
|
|
pub apply_chroma_key: Option<bool>,
|
|
#[serde(default)]
|
|
pub sample_start_ratio: Option<f32>,
|
|
#[serde(default)]
|
|
pub sample_end_ratio: Option<f32>,
|
|
#[serde(default)]
|
|
pub preview_video_path: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CharacterAnimationPublishRequest {
|
|
pub character_id: String,
|
|
pub visual_asset_id: String,
|
|
pub animations: BTreeMap<String, CharacterAnimationDraftPayload>,
|
|
#[serde(default)]
|
|
pub update_character_override: Option<bool>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CharacterAnimationPublishResponse {
|
|
pub ok: bool,
|
|
pub animation_set_id: String,
|
|
pub override_map: Value,
|
|
pub animation_map: Value,
|
|
pub save_message: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CharacterWorkflowCachePayload {
|
|
pub character_id: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub cache_scope_id: Option<String>,
|
|
pub visual_prompt_text: String,
|
|
pub animation_prompt_text: String,
|
|
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
|
pub animation_prompt_text_by_key: BTreeMap<String, String>,
|
|
pub visual_drafts: Vec<CharacterVisualDraftPayload>,
|
|
pub selected_visual_draft_id: String,
|
|
pub selected_animation: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub image_src: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub generated_visual_asset_id: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub generated_animation_set_id: Option<String>,
|
|
#[serde(default)]
|
|
pub animation_map: Option<Value>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub updated_at: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CharacterWorkflowCacheSaveRequest {
|
|
pub character_id: String,
|
|
#[serde(default)]
|
|
pub cache_scope_id: Option<String>,
|
|
#[serde(default)]
|
|
pub visual_prompt_text: Option<String>,
|
|
#[serde(default)]
|
|
pub animation_prompt_text: Option<String>,
|
|
#[serde(default)]
|
|
pub animation_prompt_text_by_key: BTreeMap<String, String>,
|
|
#[serde(default)]
|
|
pub visual_drafts: Vec<CharacterVisualDraftPayload>,
|
|
#[serde(default)]
|
|
pub selected_visual_draft_id: Option<String>,
|
|
#[serde(default)]
|
|
pub selected_animation: Option<String>,
|
|
#[serde(default)]
|
|
pub image_src: Option<String>,
|
|
#[serde(default)]
|
|
pub generated_visual_asset_id: Option<String>,
|
|
#[serde(default)]
|
|
pub generated_animation_set_id: Option<String>,
|
|
#[serde(default)]
|
|
pub animation_map: Option<Value>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CharacterWorkflowCacheGetResponse {
|
|
pub ok: bool,
|
|
pub cache: Option<CharacterWorkflowCachePayload>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CharacterAssetRolePromptInput {
|
|
pub id: String,
|
|
#[serde(default)]
|
|
pub name: String,
|
|
#[serde(default)]
|
|
pub title: String,
|
|
#[serde(default)]
|
|
pub role: String,
|
|
#[serde(default)]
|
|
pub visual_description: Option<String>,
|
|
#[serde(default)]
|
|
pub action_description: Option<String>,
|
|
#[serde(default)]
|
|
pub scene_visual_description: Option<String>,
|
|
#[serde(default)]
|
|
pub description: Option<String>,
|
|
#[serde(default)]
|
|
pub backstory: Option<String>,
|
|
#[serde(default)]
|
|
pub personality: Option<String>,
|
|
#[serde(default)]
|
|
pub motivation: Option<String>,
|
|
#[serde(default)]
|
|
pub combat_style: Option<String>,
|
|
#[serde(default)]
|
|
pub tags: Vec<String>,
|
|
#[serde(default)]
|
|
pub image_src: Option<String>,
|
|
#[serde(default)]
|
|
pub generated_visual_asset_id: Option<String>,
|
|
#[serde(default)]
|
|
pub generated_animation_set_id: Option<String>,
|
|
#[serde(default)]
|
|
pub animation_map: Option<Value>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CharacterRolePromptBundlePayload {
|
|
pub visual_prompt_text: String,
|
|
pub animation_prompt_text: String,
|
|
pub scene_prompt_text: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CharacterRoleAssetWorkflowPayload {
|
|
pub role: CharacterAssetRolePromptInput,
|
|
pub default_prompt_bundle: CharacterRolePromptBundlePayload,
|
|
pub visual_prompt_text: String,
|
|
pub animation_prompt_text: String,
|
|
pub animation_prompt_text_by_key: BTreeMap<String, String>,
|
|
pub visual_drafts: Vec<CharacterVisualDraftPayload>,
|
|
pub selected_visual_draft_id: String,
|
|
pub selected_animation: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub image_src: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub generated_visual_asset_id: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub generated_animation_set_id: Option<String>,
|
|
#[serde(default)]
|
|
pub animation_map: Option<Value>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub updated_at: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CharacterRoleAssetWorkflowResolveRequest {
|
|
#[serde(default)]
|
|
pub cache_scope_id: Option<String>,
|
|
pub role: CharacterAssetRolePromptInput,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CharacterRoleAssetWorkflowResponse {
|
|
pub ok: bool,
|
|
pub cache: Option<CharacterWorkflowCachePayload>,
|
|
pub workflow: CharacterRoleAssetWorkflowPayload,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CharacterWorkflowCacheSaveResponse {
|
|
pub ok: bool,
|
|
pub cache: CharacterWorkflowCachePayload,
|
|
pub save_message: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CreateDirectUploadTicketResponse {
|
|
pub upload: DirectUploadTicketPayload,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct DirectUploadTicketPayload {
|
|
pub signature_version: String,
|
|
pub provider: String,
|
|
pub bucket: String,
|
|
pub endpoint: String,
|
|
pub host: String,
|
|
pub object_key: String,
|
|
pub legacy_public_path: String,
|
|
#[serde(default)]
|
|
pub content_type: Option<String>,
|
|
pub access: DirectUploadObjectAccess,
|
|
pub key_prefix: String,
|
|
pub expires_at: String,
|
|
pub max_size_bytes: u64,
|
|
pub success_action_status: u16,
|
|
pub form_fields: DirectUploadTicketFormFields,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
pub struct DirectUploadTicketFormFields {
|
|
pub key: String,
|
|
pub policy: String,
|
|
#[serde(rename = "x-oss-signature-version")]
|
|
pub signature_version: String,
|
|
#[serde(rename = "x-oss-credential")]
|
|
pub credential: String,
|
|
#[serde(rename = "x-oss-date")]
|
|
pub date: String,
|
|
#[serde(rename = "x-oss-signature")]
|
|
pub signature: String,
|
|
#[serde(rename = "success_action_status")]
|
|
pub success_action_status: String,
|
|
#[serde(rename = "Content-Type", skip_serializing_if = "Option::is_none")]
|
|
pub content_type: Option<String>,
|
|
#[serde(rename = "Cache-Control", skip_serializing_if = "Option::is_none")]
|
|
pub cache_control: Option<String>,
|
|
#[serde(flatten)]
|
|
pub metadata: BTreeMap<String, String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct GetAssetReadUrlResponse {
|
|
pub read: AssetReadUrlPayload,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct AssetReadUrlPayload {
|
|
pub provider: String,
|
|
pub bucket: String,
|
|
pub endpoint: String,
|
|
pub host: String,
|
|
pub object_key: String,
|
|
pub expires_at: String,
|
|
pub signed_url: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ConfirmAssetObjectResponse {
|
|
pub asset_object: AssetObjectPayload,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct AssetObjectPayload {
|
|
pub asset_object_id: String,
|
|
pub bucket: String,
|
|
pub object_key: String,
|
|
pub access_policy: String,
|
|
#[serde(default)]
|
|
pub content_type: Option<String>,
|
|
pub content_length: u64,
|
|
#[serde(default)]
|
|
pub content_hash: Option<String>,
|
|
pub version: u32,
|
|
#[serde(default)]
|
|
pub source_job_id: Option<String>,
|
|
#[serde(default)]
|
|
pub owner_user_id: Option<String>,
|
|
#[serde(default)]
|
|
pub profile_id: Option<String>,
|
|
#[serde(default)]
|
|
pub entity_id: Option<String>,
|
|
pub asset_kind: String,
|
|
pub created_at: String,
|
|
pub updated_at: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct BindAssetObjectResponse {
|
|
pub asset_binding: AssetBindingPayload,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct AssetBindingPayload {
|
|
pub binding_id: String,
|
|
pub asset_object_id: String,
|
|
pub entity_kind: String,
|
|
pub entity_id: String,
|
|
pub slot: String,
|
|
pub asset_kind: String,
|
|
#[serde(default)]
|
|
pub owner_user_id: Option<String>,
|
|
#[serde(default)]
|
|
pub profile_id: Option<String>,
|
|
pub created_at: String,
|
|
pub updated_at: String,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use serde_json::json;
|
|
|
|
#[test]
|
|
fn confirm_asset_object_access_policy_uses_snake_case() {
|
|
let payload = serde_json::to_value(ConfirmAssetObjectAccessPolicy::PublicRead)
|
|
.expect("payload should serialize");
|
|
|
|
assert_eq!(payload, json!("public_read"));
|
|
}
|
|
|
|
#[test]
|
|
fn bind_asset_object_request_uses_camel_case_fields() {
|
|
let payload = serde_json::to_value(BindAssetObjectRequest {
|
|
asset_object_id: "assetobj_1".to_string(),
|
|
entity_kind: "character".to_string(),
|
|
entity_id: "npc_1".to_string(),
|
|
slot: "primary_visual".to_string(),
|
|
asset_kind: "character_visual".to_string(),
|
|
owner_user_id: Some("user_1".to_string()),
|
|
profile_id: Some("profile_1".to_string()),
|
|
})
|
|
.expect("payload should serialize");
|
|
|
|
assert_eq!(
|
|
payload,
|
|
json!({
|
|
"assetObjectId": "assetobj_1",
|
|
"entityKind": "character",
|
|
"entityId": "npc_1",
|
|
"slot": "primary_visual",
|
|
"assetKind": "character_visual",
|
|
"ownerUserId": "user_1",
|
|
"profileId": "profile_1"
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn direct_upload_ticket_response_keeps_form_fields_shape() {
|
|
let payload = serde_json::to_value(CreateDirectUploadTicketResponse {
|
|
upload: DirectUploadTicketPayload {
|
|
signature_version: "v4".to_string(),
|
|
provider: "aliyun-oss".to_string(),
|
|
bucket: "genarrative-assets".to_string(),
|
|
endpoint: "oss-cn-shanghai.aliyuncs.com".to_string(),
|
|
host: "https://genarrative-assets.oss-cn-shanghai.aliyuncs.com".to_string(),
|
|
object_key: "generated-characters/hero/master.png".to_string(),
|
|
legacy_public_path: "/generated-characters/hero/master.png".to_string(),
|
|
content_type: Some("image/png".to_string()),
|
|
access: DirectUploadObjectAccess::Private,
|
|
key_prefix: "generated-characters/hero".to_string(),
|
|
expires_at: "2026-04-21T00:00:00Z".to_string(),
|
|
max_size_bytes: 1024,
|
|
success_action_status: 200,
|
|
form_fields: DirectUploadTicketFormFields {
|
|
key: "generated-characters/hero/master.png".to_string(),
|
|
policy: "policy".to_string(),
|
|
signature_version: "OSS4-HMAC-SHA256".to_string(),
|
|
credential: "ak/20260507/cn-shanghai/oss/aliyun_v4_request".to_string(),
|
|
date: "20260507T120000Z".to_string(),
|
|
signature: "sig".to_string(),
|
|
success_action_status: "200".to_string(),
|
|
content_type: Some("image/png".to_string()),
|
|
cache_control: Some("public, max-age=31536000, immutable".to_string()),
|
|
metadata: BTreeMap::from([(
|
|
"x-oss-meta-asset-kind".to_string(),
|
|
"character_visual".to_string(),
|
|
)]),
|
|
},
|
|
},
|
|
})
|
|
.expect("payload should serialize");
|
|
|
|
assert_eq!(payload["upload"]["signatureVersion"], json!("v4"));
|
|
assert_eq!(
|
|
payload["upload"]["formFields"]["x-oss-signature-version"],
|
|
json!("OSS4-HMAC-SHA256")
|
|
);
|
|
assert_eq!(
|
|
payload["upload"]["formFields"]["x-oss-credential"],
|
|
json!("ak/20260507/cn-shanghai/oss/aliyun_v4_request")
|
|
);
|
|
assert_eq!(
|
|
payload["upload"]["formFields"]["x-oss-meta-asset-kind"],
|
|
json!("character_visual")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn confirm_asset_object_response_uses_camel_case_fields() {
|
|
let payload = serde_json::to_value(ConfirmAssetObjectResponse {
|
|
asset_object: AssetObjectPayload {
|
|
asset_object_id: "assetobj_1".to_string(),
|
|
bucket: "genarrative-assets".to_string(),
|
|
object_key: "generated-characters/hero/master.png".to_string(),
|
|
access_policy: "private".to_string(),
|
|
content_type: Some("image/png".to_string()),
|
|
content_length: 1024,
|
|
content_hash: Some("etag-1".to_string()),
|
|
version: 1,
|
|
source_job_id: Some("job_1".to_string()),
|
|
owner_user_id: Some("user_1".to_string()),
|
|
profile_id: Some("profile_1".to_string()),
|
|
entity_id: Some("entity_1".to_string()),
|
|
asset_kind: "character_visual".to_string(),
|
|
created_at: "1.000000Z".to_string(),
|
|
updated_at: "1.000000Z".to_string(),
|
|
},
|
|
})
|
|
.expect("payload should serialize");
|
|
|
|
assert_eq!(payload["assetObject"]["assetObjectId"], json!("assetobj_1"));
|
|
assert_eq!(payload["assetObject"]["accessPolicy"], json!("private"));
|
|
assert_eq!(payload["assetObject"]["contentLength"], json!(1024));
|
|
}
|
|
|
|
#[test]
|
|
fn character_visual_source_mode_uses_legacy_kebab_case() {
|
|
let payload = serde_json::to_value(CharacterVisualSourceMode::ImageToImage)
|
|
.expect("source mode should serialize");
|
|
|
|
assert_eq!(payload, json!("image-to-image"));
|
|
}
|
|
|
|
#[test]
|
|
fn character_visual_generate_response_keeps_legacy_shape() {
|
|
let payload = serde_json::to_value(CharacterVisualGenerateResponse {
|
|
ok: true,
|
|
task_id: "visual_1".to_string(),
|
|
model: "rust-svg-character-visual".to_string(),
|
|
prompt: "角色提示词".to_string(),
|
|
drafts: vec![CharacterVisualDraftPayload {
|
|
id: "candidate-1".to_string(),
|
|
label: "候选 1".to_string(),
|
|
image_src: "/generated-character-drafts/hero/visual/visual_1/candidate-01.svg"
|
|
.to_string(),
|
|
width: 1024,
|
|
height: 1024,
|
|
}],
|
|
})
|
|
.expect("response should serialize");
|
|
|
|
assert_eq!(payload["ok"], json!(true));
|
|
assert_eq!(payload["taskId"], json!("visual_1"));
|
|
assert_eq!(
|
|
payload["drafts"][0]["imageSrc"],
|
|
json!("/generated-character-drafts/hero/visual/visual_1/candidate-01.svg")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn character_animation_templates_response_keeps_legacy_shape() {
|
|
let payload = serde_json::to_value(CharacterAnimationTemplatesResponse {
|
|
ok: true,
|
|
templates: vec![CharacterAnimationTemplatePayload {
|
|
id: "idle_loop".to_string(),
|
|
label: "待机循环".to_string(),
|
|
animation: "idle".to_string(),
|
|
prompt_suffix: "保持呼吸感。".to_string(),
|
|
notes: "默认待机模板。".to_string(),
|
|
}],
|
|
})
|
|
.expect("response should serialize");
|
|
|
|
assert_eq!(payload["ok"], json!(true));
|
|
assert_eq!(payload["templates"][0]["id"], json!("idle_loop"));
|
|
assert_eq!(
|
|
payload["templates"][0]["promptSuffix"],
|
|
json!("保持呼吸感。")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn character_animation_import_video_response_keeps_legacy_shape() {
|
|
let payload =
|
|
serde_json::to_value(CharacterAnimationImportVideoResponse {
|
|
ok: true,
|
|
imported_video_path:
|
|
"/generated-character-drafts/hero/animation/idle/import-1/reference.mp4"
|
|
.to_string(),
|
|
draft_id: "animation-import-1".to_string(),
|
|
save_message: "参考视频已导入 OSS 草稿区。".to_string(),
|
|
})
|
|
.expect("response should serialize");
|
|
|
|
assert_eq!(
|
|
payload["importedVideoPath"],
|
|
json!("/generated-character-drafts/hero/animation/idle/import-1/reference.mp4")
|
|
);
|
|
assert_eq!(payload["draftId"], json!("animation-import-1"));
|
|
}
|
|
|
|
#[test]
|
|
fn editor_character_animation_request_uses_expected_camel_case_shape() {
|
|
let payload = serde_json::to_value(EditorCharacterAnimationGenerateRequest {
|
|
source_layer_id: "layer-1".to_string(),
|
|
source_image_src: "/generated-characters/hero/master.png".to_string(),
|
|
source_width: 768,
|
|
source_height: 1024,
|
|
prompt_text: "待机呼吸".to_string(),
|
|
screen_color: None,
|
|
resolution: "720p".to_string(),
|
|
ratio: "same".to_string(),
|
|
frame_count: 48,
|
|
duration_seconds: 6,
|
|
model: "seedance2.0-fast".to_string(),
|
|
project_id: Some("editor-project-1".to_string()),
|
|
canvas_completion: Some(EditorCanvasGenerationCompletionPayload {
|
|
dialog_id: Some("dialog-1".to_string()),
|
|
title: "角色动作".to_string(),
|
|
placeholder: EditorCanvasGenerationPlaceholderPayload {
|
|
x: 10.0,
|
|
y: 20.0,
|
|
width: 420.0,
|
|
height: 420.0,
|
|
original_width: 1024.0,
|
|
original_height: 1024.0,
|
|
},
|
|
}),
|
|
generation_inputs: None,
|
|
source_resource_id: Some("resource-source".to_string()),
|
|
asset_folder_id: Some("project".to_string()),
|
|
asset_label: Some("勇者奔跑".to_string()),
|
|
})
|
|
.expect("request should serialize");
|
|
|
|
assert_eq!(payload["sourceLayerId"], json!("layer-1"));
|
|
assert_eq!(
|
|
payload["sourceImageSrc"],
|
|
json!("/generated-characters/hero/master.png")
|
|
);
|
|
assert!(payload.get("priceMudPoints").is_none());
|
|
assert_eq!(payload["model"], json!("seedance2.0-fast"));
|
|
assert_eq!(payload["projectId"], json!("editor-project-1"));
|
|
assert_eq!(payload["canvasCompletion"]["dialogId"], json!("dialog-1"));
|
|
assert_eq!(payload["sourceResourceId"], json!("resource-source"));
|
|
assert_eq!(payload["assetFolderId"], json!("project"));
|
|
assert_eq!(payload["assetLabel"], json!("勇者奔跑"));
|
|
}
|
|
|
|
#[test]
|
|
fn editor_character_animation_response_includes_frames_and_preview_video() {
|
|
let payload = serde_json::to_value(EditorCharacterAnimationGenerateResponse {
|
|
ok: true,
|
|
task_id: "task-1".to_string(),
|
|
model: "seedance2.0-fast".to_string(),
|
|
prompt: "生成游戏角色动画".to_string(),
|
|
preview_video_path: "/generated-character-drafts/editor/layer/preview.mp4".to_string(),
|
|
frames: vec![EditorCharacterAnimationFramePayload {
|
|
frame_index: 1,
|
|
image_src: "/generated-animations/editor/layer/frame01.png".to_string(),
|
|
object_key: Some("generated-animations/editor/layer/frame01.png".to_string()),
|
|
asset_object_id: Some("asset-object-frame01".to_string()),
|
|
width: 768,
|
|
height: 1024,
|
|
}],
|
|
frame_count: 1,
|
|
duration_seconds: 4,
|
|
frame_width: 768,
|
|
frame_height: 1024,
|
|
fps: 8,
|
|
price_mud_points: 40,
|
|
project: None,
|
|
queue_state: None,
|
|
})
|
|
.expect("response should serialize");
|
|
|
|
assert_eq!(
|
|
payload["previewVideoPath"],
|
|
json!("/generated-character-drafts/editor/layer/preview.mp4")
|
|
);
|
|
assert_eq!(
|
|
payload["frames"][0]["imageSrc"],
|
|
json!("/generated-animations/editor/layer/frame01.png")
|
|
);
|
|
assert_eq!(
|
|
payload["frames"][0]["objectKey"],
|
|
json!("generated-animations/editor/layer/frame01.png")
|
|
);
|
|
assert_eq!(
|
|
payload["frames"][0]["assetObjectId"],
|
|
json!("asset-object-frame01")
|
|
);
|
|
assert_eq!(payload["fps"], json!(8));
|
|
}
|
|
|
|
#[test]
|
|
fn editor_video_request_supports_lovart_model_contract() {
|
|
let payload = serde_json::to_value(EditorVideoGenerateRequest {
|
|
prompt: "让角色向镜头挥手".to_string(),
|
|
model: "kling3.0-omni".to_string(),
|
|
aspect_ratio: "16:9".to_string(),
|
|
duration_seconds: 5,
|
|
resolution: "480p".to_string(),
|
|
mode: "std".to_string(),
|
|
sound: "off".to_string(),
|
|
web_search_enabled: true,
|
|
reference_image_srcs: Vec::new(),
|
|
reference_video_srcs: Vec::new(),
|
|
reference_audio_srcs: Vec::new(),
|
|
project_id: None,
|
|
canvas_completion: None,
|
|
generation_inputs: None,
|
|
source_resource_id: None,
|
|
asset_kind: None,
|
|
asset_folder_id: None,
|
|
asset_label: None,
|
|
})
|
|
.expect("request should serialize");
|
|
|
|
assert_eq!(payload["aspectRatio"], json!("16:9"));
|
|
assert_eq!(payload["durationSeconds"], json!(5));
|
|
assert!(payload.get("priceMudPoints").is_none());
|
|
assert_eq!(payload["model"], json!("kling3.0-omni"));
|
|
assert_eq!(payload["webSearchEnabled"], json!(true));
|
|
}
|
|
|
|
#[test]
|
|
fn editor_video_request_supports_stable_seedance_multimodal_references() {
|
|
let payload = serde_json::to_value(EditorVideoGenerateRequest {
|
|
prompt: "参考素材生成视频".to_string(),
|
|
model: "seedance2.0-fast".to_string(),
|
|
aspect_ratio: "16:9".to_string(),
|
|
duration_seconds: 5,
|
|
resolution: "720p".to_string(),
|
|
mode: "std".to_string(),
|
|
sound: "off".to_string(),
|
|
web_search_enabled: true,
|
|
reference_image_srcs: vec![
|
|
"generated-character-drafts/editor/seedance-references/image/demo.png".to_string(),
|
|
],
|
|
reference_video_srcs: vec![
|
|
"generated-character-drafts/editor/seedance-references/video/demo.mp4".to_string(),
|
|
],
|
|
reference_audio_srcs: vec![
|
|
"generated-character-drafts/editor/seedance-references/audio/demo.mp3".to_string(),
|
|
],
|
|
project_id: None,
|
|
canvas_completion: None,
|
|
generation_inputs: None,
|
|
source_resource_id: None,
|
|
asset_kind: None,
|
|
asset_folder_id: None,
|
|
asset_label: None,
|
|
})
|
|
.expect("request should serialize");
|
|
|
|
assert_eq!(
|
|
payload["referenceImageSrcs"],
|
|
json!(["generated-character-drafts/editor/seedance-references/image/demo.png"])
|
|
);
|
|
assert_eq!(
|
|
payload["referenceVideoSrcs"],
|
|
json!(["generated-character-drafts/editor/seedance-references/video/demo.mp4"])
|
|
);
|
|
assert_eq!(
|
|
payload["referenceAudioSrcs"],
|
|
json!(["generated-character-drafts/editor/seedance-references/audio/demo.mp3"])
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn editor_video_response_uses_canvas_video_shape() {
|
|
let payload = serde_json::to_value(EditorVideoGenerateResponse {
|
|
ok: true,
|
|
video_src: "/generated-editor-videos/task-1/preview.mp4".to_string(),
|
|
thumbnail_src: Some("/generated-editor-videos/task-1/cover.png".to_string()),
|
|
object_key: Some(
|
|
"generated-character-drafts/editor-videos/task-1/preview.mp4".to_string(),
|
|
),
|
|
asset_object_id: Some("assetobj_video_1".to_string()),
|
|
width: 1280,
|
|
height: 720,
|
|
source_type: "generated".to_string(),
|
|
prompt: "让角色向镜头挥手".to_string(),
|
|
actual_prompt: Some("让角色向镜头挥手".to_string()),
|
|
model: "kling3.0-omni".to_string(),
|
|
task_id: "task-1".to_string(),
|
|
duration_seconds: 5,
|
|
resolution: "480p".to_string(),
|
|
price_mud_points: 100,
|
|
project: None,
|
|
resource: None,
|
|
asset: None,
|
|
queue_state: None,
|
|
})
|
|
.expect("response should serialize");
|
|
|
|
assert_eq!(
|
|
payload["videoSrc"],
|
|
json!("/generated-editor-videos/task-1/preview.mp4")
|
|
);
|
|
assert_eq!(
|
|
payload["thumbnailSrc"],
|
|
json!("/generated-editor-videos/task-1/cover.png")
|
|
);
|
|
assert_eq!(
|
|
payload["objectKey"],
|
|
json!("generated-character-drafts/editor-videos/task-1/preview.mp4")
|
|
);
|
|
assert_eq!(payload["assetObjectId"], json!("assetobj_video_1"));
|
|
assert_eq!(payload["sourceType"], json!("generated"));
|
|
assert_eq!(payload["durationSeconds"], json!(5));
|
|
assert!(payload.get("provider").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn editor_icon_spritesheet_request_omits_price_and_response_includes_price() {
|
|
let request_payload = serde_json::to_value(EditorIconSpritesheetGenerateRequest {
|
|
reference_image_src: "generated-images/editor/spec.png".to_string(),
|
|
reference_image_srcs: vec!["generated-images/editor/style.png".to_string()],
|
|
icon_descriptions: vec!["返回按钮".to_string(), "设置按钮".to_string()],
|
|
model: Some("gemini-3.1-flash-image-preview".to_string()),
|
|
aspect_ratio: Some("1:1".to_string()),
|
|
image_size: Some("1K".to_string()),
|
|
})
|
|
.expect("request should serialize");
|
|
|
|
assert_eq!(
|
|
request_payload["referenceImageSrc"],
|
|
json!("generated-images/editor/spec.png")
|
|
);
|
|
assert_eq!(
|
|
request_payload["referenceImageSrcs"],
|
|
json!(["generated-images/editor/style.png"])
|
|
);
|
|
assert!(request_payload.get("priceMudPoints").is_none());
|
|
|
|
let response_payload = serde_json::to_value(EditorIconSpritesheetGenerateResponse {
|
|
spritesheet_image_src: "/generated-images/editor/sheet.png".to_string(),
|
|
spritesheet_width: 512,
|
|
spritesheet_height: 512,
|
|
icon_image_srcs: Vec::new(),
|
|
slice_warning: Some(EditorIconSpritesheetSliceWarningPayload {
|
|
code: "insufficient-connected-components".to_string(),
|
|
reason: "连通域数量不足".to_string(),
|
|
}),
|
|
prompt: "图标 prompt".to_string(),
|
|
actual_prompt: Some("图标 prompt".to_string()),
|
|
model: "gemini-3.1-flash-image-preview".to_string(),
|
|
task_id: "task-icons".to_string(),
|
|
price_mud_points: 12,
|
|
})
|
|
.expect("response should serialize");
|
|
|
|
assert_eq!(response_payload["priceMudPoints"], json!(12));
|
|
assert_eq!(response_payload["iconImageSrcs"], json!([]));
|
|
assert!(response_payload.get("provider").is_none());
|
|
assert_eq!(
|
|
response_payload["sliceWarning"],
|
|
json!({
|
|
"code": "insufficient-connected-components",
|
|
"reason": "连通域数量不足",
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn editor_audio_requests_and_response_use_canvas_audio_shape() {
|
|
let sound_payload = serde_json::to_value(EditorSoundEffectGenerateRequest {
|
|
prompt: "金币掉落叮当声".to_string(),
|
|
model: Some("audio1.0".to_string()),
|
|
duration: 7,
|
|
project_id: None,
|
|
canvas_completion: None,
|
|
generation_inputs: None,
|
|
asset_folder_id: Some("project".to_string()),
|
|
asset_label: Some("游戏音效 1".to_string()),
|
|
})
|
|
.expect("sound request should serialize");
|
|
assert_eq!(sound_payload["prompt"], json!("金币掉落叮当声"));
|
|
assert_eq!(sound_payload["model"], json!("audio1.0"));
|
|
assert_eq!(sound_payload["duration"], json!(7));
|
|
assert_eq!(sound_payload["assetFolderId"], json!("project"));
|
|
assert_eq!(sound_payload["assetLabel"], json!("游戏音效 1"));
|
|
assert!(sound_payload.get("priceMudPoints").is_none());
|
|
assert!(sound_payload.get("sound").is_none());
|
|
assert!(sound_payload.get("type").is_none());
|
|
assert!(sound_payload.get("tempo").is_none());
|
|
|
|
let parsed_sound_payload: EditorSoundEffectGenerateRequest =
|
|
serde_json::from_value(sound_payload)
|
|
.expect("sound request with duration should deserialize");
|
|
assert_eq!(parsed_sound_payload.prompt, "金币掉落叮当声");
|
|
assert_eq!(parsed_sound_payload.duration, 7);
|
|
|
|
let unset_model_payload = serde_json::to_value(EditorSoundEffectGenerateRequest {
|
|
prompt: "按钮确认短促音".to_string(),
|
|
model: None,
|
|
duration: 5,
|
|
project_id: None,
|
|
canvas_completion: None,
|
|
generation_inputs: None,
|
|
asset_folder_id: None,
|
|
asset_label: None,
|
|
})
|
|
.expect("sound request with unset model should serialize");
|
|
assert!(unset_model_payload.get("model").is_none());
|
|
assert_eq!(unset_model_payload["duration"], json!(5));
|
|
|
|
let music_payload = serde_json::to_value(EditorBackgroundMusicGenerateRequest {
|
|
gpt_description_prompt: "森林冒险背景音乐".to_string(),
|
|
make_instrumental: true,
|
|
project_id: None,
|
|
canvas_completion: None,
|
|
generation_inputs: None,
|
|
asset_folder_id: None,
|
|
asset_label: None,
|
|
})
|
|
.expect("background music request should serialize");
|
|
assert_eq!(
|
|
music_payload["gptDescriptionPrompt"],
|
|
json!("森林冒险背景音乐")
|
|
);
|
|
assert_eq!(music_payload["makeInstrumental"], json!(true));
|
|
|
|
let response_payload = serde_json::to_value(EditorAudioGenerateResponse {
|
|
ok: true,
|
|
audio_src: "/generated-character-drafts/editor-audios/sfx.mp3".to_string(),
|
|
object_key: Some("generated-character-drafts/editor-audios/sfx.mp3".to_string()),
|
|
asset_object_id: Some("assetobj_audio_1".to_string()),
|
|
width: 420,
|
|
height: 120,
|
|
source_type: "generated".to_string(),
|
|
prompt: "金币掉落叮当声".to_string(),
|
|
actual_prompt: Some("金币掉落叮当声".to_string()),
|
|
model: "audio1.0".to_string(),
|
|
task_id: "sound-task-1".to_string(),
|
|
price_mud_points: 10,
|
|
audio_kind: "sound-effect".to_string(),
|
|
project: None,
|
|
resource: None,
|
|
asset: Some(json!({
|
|
"assetId": "asset-audio-1",
|
|
"folderId": "user-1:asset-folder:project",
|
|
"label": "游戏音效 1",
|
|
"imageSrc": "/generated-character-drafts/editor-audios/sfx.mp3",
|
|
"width": 420,
|
|
"height": 120,
|
|
"sourceType": "generated",
|
|
"assetKind": "sound-effect"
|
|
})),
|
|
queue_state: None,
|
|
})
|
|
.expect("audio response should serialize");
|
|
assert_eq!(
|
|
response_payload["audioSrc"],
|
|
json!("/generated-character-drafts/editor-audios/sfx.mp3")
|
|
);
|
|
assert_eq!(
|
|
response_payload["objectKey"],
|
|
json!("generated-character-drafts/editor-audios/sfx.mp3")
|
|
);
|
|
assert_eq!(response_payload["assetObjectId"], json!("assetobj_audio_1"));
|
|
assert_eq!(response_payload["audioKind"], json!("sound-effect"));
|
|
assert!(response_payload.get("provider").is_none());
|
|
assert_eq!(
|
|
response_payload["asset"]["assetKind"],
|
|
json!("sound-effect")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn character_workflow_cache_response_keeps_legacy_shape() {
|
|
let payload = serde_json::to_value(CharacterWorkflowCacheSaveResponse {
|
|
ok: true,
|
|
cache: CharacterWorkflowCachePayload {
|
|
character_id: "hero".to_string(),
|
|
cache_scope_id: Some("world-01".to_string()),
|
|
visual_prompt_text: "主形象".to_string(),
|
|
animation_prompt_text: "待机".to_string(),
|
|
animation_prompt_text_by_key: BTreeMap::from([(
|
|
"idle".to_string(),
|
|
"待机".to_string(),
|
|
)]),
|
|
visual_drafts: vec![CharacterVisualDraftPayload {
|
|
id: "draft-1".to_string(),
|
|
label: "候选 1".to_string(),
|
|
image_src: "/generated-character-drafts/hero/visual/job/candidate.svg"
|
|
.to_string(),
|
|
width: 1024,
|
|
height: 1536,
|
|
}],
|
|
selected_visual_draft_id: "draft-1".to_string(),
|
|
selected_animation: "idle".to_string(),
|
|
image_src: Some("/generated-characters/hero/master.png".to_string()),
|
|
generated_visual_asset_id: None,
|
|
generated_animation_set_id: None,
|
|
animation_map: Some(json!({ "idle": { "frames": 4 } })),
|
|
updated_at: Some("2026-04-22T12:00:00Z".to_string()),
|
|
},
|
|
save_message: "角色形象生成缓存已更新。".to_string(),
|
|
})
|
|
.expect("response should serialize");
|
|
|
|
assert_eq!(payload["ok"], json!(true));
|
|
assert_eq!(payload["cache"]["characterId"], json!("hero"));
|
|
assert_eq!(payload["cache"]["cacheScopeId"], json!("world-01"));
|
|
assert_eq!(
|
|
payload["cache"]["animationPromptTextByKey"]["idle"],
|
|
json!("待机")
|
|
);
|
|
assert_eq!(
|
|
payload["cache"]["visualDrafts"][0]["imageSrc"],
|
|
json!("/generated-character-drafts/hero/visual/job/candidate.svg")
|
|
);
|
|
assert_eq!(payload["cache"]["animationMap"]["idle"]["frames"], json!(4));
|
|
}
|
|
|
|
#[test]
|
|
fn character_animation_strategy_uses_legacy_kebab_case() {
|
|
let payload = serde_json::to_value(CharacterAnimationStrategy::MotionTransfer)
|
|
.expect("strategy should serialize");
|
|
|
|
assert_eq!(payload, json!("motion-transfer"));
|
|
}
|
|
|
|
#[test]
|
|
fn character_animation_generate_response_keeps_image_sequence_shape() {
|
|
let payload = serde_json::to_value(CharacterAnimationGenerateResponse {
|
|
ok: true,
|
|
task_id: "animation_1".to_string(),
|
|
strategy: CharacterAnimationStrategy::ImageSequence,
|
|
model: "rust-svg-animation-sequence".to_string(),
|
|
prompt: "待机动作".to_string(),
|
|
image_sources: vec![
|
|
"/generated-character-drafts/hero/animation/idle/job/frame-01.svg".to_string(),
|
|
],
|
|
preview_video_path: None,
|
|
})
|
|
.expect("response should serialize");
|
|
|
|
assert_eq!(payload["ok"], json!(true));
|
|
assert_eq!(payload["taskId"], json!("animation_1"));
|
|
assert_eq!(payload["strategy"], json!("image-sequence"));
|
|
assert_eq!(
|
|
payload["imageSources"][0],
|
|
json!("/generated-character-drafts/hero/animation/idle/job/frame-01.svg")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn character_animation_publish_response_keeps_legacy_shape() {
|
|
let payload = serde_json::to_value(CharacterAnimationPublishResponse {
|
|
ok: true,
|
|
animation_set_id: "animation-set-1".to_string(),
|
|
override_map: json!({}),
|
|
animation_map: json!({
|
|
"idle": {
|
|
"folder": "idle",
|
|
"prefix": "frame",
|
|
"frames": 2,
|
|
"startFrame": 1,
|
|
"extension": "svg",
|
|
"basePath": "/generated-animations/hero/animation-set-1/idle",
|
|
"frameWidth": 192,
|
|
"frameHeight": 256,
|
|
"fps": 8,
|
|
"loop": true
|
|
}
|
|
}),
|
|
save_message: "基础动作资源已写入 OSS 并绑定当前角色。".to_string(),
|
|
})
|
|
.expect("response should serialize");
|
|
|
|
assert_eq!(payload["animationSetId"], json!("animation-set-1"));
|
|
assert_eq!(payload["animationMap"]["idle"]["frames"], json!(2));
|
|
}
|
|
|
|
#[test]
|
|
fn character_animation_draft_payload_accepts_backend_extraction_fields() {
|
|
let payload = serde_json::from_value::<CharacterAnimationDraftPayload>(json!({
|
|
"fps": 8,
|
|
"loop": true,
|
|
"frameWidth": 192,
|
|
"frameHeight": 256,
|
|
"frameCount": 8,
|
|
"applyChromaKey": true,
|
|
"sampleStartRatio": 0.12,
|
|
"sampleEndRatio": 0.94,
|
|
"previewVideoPath": "/generated-character-drafts/hero/animation/idle/task/preview.mp4"
|
|
}))
|
|
.expect("draft payload should deserialize without framesDataUrls");
|
|
|
|
assert!(payload.frames_data_urls.is_empty());
|
|
assert_eq!(payload.frame_count, Some(8));
|
|
assert_eq!(payload.apply_chroma_key, Some(true));
|
|
assert_eq!(payload.sample_start_ratio, Some(0.12));
|
|
assert_eq!(payload.sample_end_ratio, Some(0.94));
|
|
}
|
|
}
|