合并画布 Agent 工具计价实现
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
use crate::editor_agent::utils::{ImageId, ImageMetadata};
|
||||
use crate::editor_generation_config::EditorGenerationPricingConfig;
|
||||
use crate::openai_image_generation::GPT_IMAGE_2_MODEL;
|
||||
use module_editor_agent::agent::tool::Tool;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -7,7 +10,6 @@ pub struct EditorToolContext {
|
||||
pub images: HashMap<ImageId, ImageMetadata>,
|
||||
}
|
||||
|
||||
|
||||
impl EditorToolContext {
|
||||
/// Check if an image with the given ID exists in the context.
|
||||
pub fn contains_image(&self, image_id: &ImageId) -> bool {
|
||||
@@ -20,3 +22,27 @@ impl EditorToolContext {
|
||||
.map(|metadata| metadata.data_key.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// api-server 侧的画布 Agent 工具计价扩展。
|
||||
///
|
||||
/// 通用 `Tool` 仍只负责参数校验;价格依赖 api-server 的运行时配置,不能下沉到
|
||||
/// `module-editor-agent`。实际执行和扣费仍由既有生成 BFF 负责。
|
||||
#[allow(dead_code)]
|
||||
pub(crate) trait EditorAgentPricedTool: Tool {
|
||||
fn pricing(&self, pricing: &EditorGenerationPricingConfig, args: &<Self as Tool>::Args) -> u32;
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn editor_agent_image_mud_points(
|
||||
pricing: &EditorGenerationPricingConfig,
|
||||
kind: Option<&str>,
|
||||
image_size: Option<&str>,
|
||||
) -> u32 {
|
||||
// 这些 Agent 工具当前向既有 BFF 传 model=None;BFF 会先归一为 gpt-image-2。
|
||||
// 尺寸同样只把精确的 2K 识别为 2K,其余值回落到 1K。
|
||||
let normalized_image_size = match image_size.map(str::trim) {
|
||||
Some("2K") => "2K",
|
||||
_ => "1K",
|
||||
};
|
||||
pricing.image_generation_mud_points(kind, Some(GPT_IMAGE_2_MODEL), Some(normalized_image_size))
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use crate::editor_agent::editor_tools::common::EditorToolContext;
|
||||
use crate::editor_agent::editor_tools::common::{
|
||||
EditorAgentPricedTool, EditorToolContext, editor_agent_image_mud_points,
|
||||
};
|
||||
use crate::editor_agent::utils::ImageId;
|
||||
use crate::editor_generation_config::EditorGenerationPricingConfig;
|
||||
use crate::editor_project::{
|
||||
EditorGenerationCaller, EditorImageEditRequest, edit_editor_image_for_owner,
|
||||
};
|
||||
@@ -122,6 +125,12 @@ impl Tool for EditImageTool {
|
||||
}
|
||||
}
|
||||
|
||||
impl EditorAgentPricedTool for EditImageTool {
|
||||
fn pricing(&self, pricing: &EditorGenerationPricingConfig, _args: &EditImageToolArgs) -> u32 {
|
||||
editor_agent_image_mud_points(pricing, Some("quick-edit"), Some("1K"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Typed result deserialized from `edit_editor_image_for_owner` response.
|
||||
///
|
||||
/// Mirrors `EditorImageGenerationResponse` but uses `String` instead of `&'static str`
|
||||
|
||||
+14
@@ -1,6 +1,10 @@
|
||||
use crate::editor_agent::editor_tools::common::EditorAgentPricedTool;
|
||||
use crate::editor_agent::editor_tools::generate_sound_effect::{
|
||||
map_media_response_error, parse_media_response, pending_message,
|
||||
};
|
||||
use crate::editor_generation_config::{
|
||||
EDITOR_BACKGROUND_MUSIC_MODEL_SUNO, EditorGenerationPricingConfig,
|
||||
};
|
||||
use crate::http_error::AppError;
|
||||
use crate::request_context::RequestContext;
|
||||
use crate::state::AppState;
|
||||
@@ -79,6 +83,16 @@ impl Tool for GenerateBackgroundMusicTool {
|
||||
}
|
||||
}
|
||||
|
||||
impl EditorAgentPricedTool for GenerateBackgroundMusicTool {
|
||||
fn pricing(
|
||||
&self,
|
||||
pricing: &EditorGenerationPricingConfig,
|
||||
_args: &GenerateBackgroundMusicToolArgs,
|
||||
) -> u32 {
|
||||
pricing.background_music_model_mud_points(Some(EDITOR_BACKGROUND_MUSIC_MODEL_SUNO))
|
||||
}
|
||||
}
|
||||
|
||||
impl GenerateBackgroundMusicTool {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn execute(
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
use crate::editor_agent::editor_tools::common::EditorToolContext;
|
||||
use crate::editor_agent::editor_tools::common::{
|
||||
EditorAgentPricedTool, EditorToolContext, editor_agent_image_mud_points,
|
||||
};
|
||||
use crate::editor_agent::editor_tools::generate_image::{
|
||||
EditorImageGenerationResult, GenerateImageError, GenerateImageTool, GenerateImageToolArgs,
|
||||
GenerateImageToolOutput,
|
||||
};
|
||||
use crate::editor_generation_config::EditorGenerationPricingConfig;
|
||||
use crate::editor_project::EditorGenerationCaller;
|
||||
use crate::http_error::AppError;
|
||||
use crate::request_context::RequestContext;
|
||||
@@ -61,6 +64,16 @@ impl Tool for GenerateCharacterTool {
|
||||
}
|
||||
}
|
||||
|
||||
impl EditorAgentPricedTool for GenerateCharacterTool {
|
||||
fn pricing(
|
||||
&self,
|
||||
pricing: &EditorGenerationPricingConfig,
|
||||
args: &GenerateImageToolArgs,
|
||||
) -> u32 {
|
||||
editor_agent_image_mud_points(pricing, Some("character"), args.image_size.as_deref())
|
||||
}
|
||||
}
|
||||
|
||||
impl GenerateCharacterTool {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn execute(
|
||||
|
||||
+14
-1
@@ -1,5 +1,8 @@
|
||||
use crate::editor_agent::editor_tools::common::EditorToolContext;
|
||||
use crate::editor_agent::editor_tools::common::{
|
||||
EditorAgentPricedTool, EditorToolContext, editor_agent_image_mud_points,
|
||||
};
|
||||
use crate::editor_agent::utils::ImageId;
|
||||
use crate::editor_generation_config::EditorGenerationPricingConfig;
|
||||
use crate::editor_project::{
|
||||
EditorGenerationCaller, EditorIconSpritesheetGenerationRequest,
|
||||
generate_editor_icon_spritesheet_for_owner,
|
||||
@@ -118,6 +121,16 @@ impl Tool for GenerateIconSpritesheetTool {
|
||||
}
|
||||
}
|
||||
|
||||
impl EditorAgentPricedTool for GenerateIconSpritesheetTool {
|
||||
fn pricing(
|
||||
&self,
|
||||
pricing: &EditorGenerationPricingConfig,
|
||||
args: &GenerateIconSpritesheetToolArgs,
|
||||
) -> u32 {
|
||||
editor_agent_image_mud_points(pricing, Some("icon"), args.image_size.as_deref())
|
||||
}
|
||||
}
|
||||
|
||||
impl GenerateIconSpritesheetTool {
|
||||
fn validate_args(
|
||||
&self,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use crate::editor_agent::editor_tools::common::EditorToolContext;
|
||||
use crate::editor_agent::editor_tools::common::{
|
||||
EditorAgentPricedTool, EditorToolContext, editor_agent_image_mud_points,
|
||||
};
|
||||
use crate::editor_agent::utils::ImageId;
|
||||
use crate::editor_generation_config::EditorGenerationPricingConfig;
|
||||
use crate::editor_project::{
|
||||
EditorGenerationCaller, EditorImageGenerationRequest, generate_editor_image_for_owner,
|
||||
};
|
||||
@@ -113,6 +116,16 @@ impl Tool for GenerateImageTool {
|
||||
}
|
||||
}
|
||||
|
||||
impl EditorAgentPricedTool for GenerateImageTool {
|
||||
fn pricing(
|
||||
&self,
|
||||
pricing: &EditorGenerationPricingConfig,
|
||||
args: &GenerateImageToolArgs,
|
||||
) -> u32 {
|
||||
editor_agent_image_mud_points(pricing, None, args.image_size.as_deref())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EditorImageGenerationResult {
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
use crate::editor_agent::editor_tools::common::EditorAgentPricedTool;
|
||||
use crate::editor_generation_config::{
|
||||
EDITOR_SOUND_EFFECT_MODEL_VIDU, EditorGenerationPricingConfig,
|
||||
};
|
||||
use crate::http_error::AppError;
|
||||
use crate::request_context::RequestContext;
|
||||
use crate::state::AppState;
|
||||
@@ -87,6 +91,22 @@ impl Tool for GenerateSoundEffectTool {
|
||||
}
|
||||
}
|
||||
|
||||
impl EditorAgentPricedTool for GenerateSoundEffectTool {
|
||||
fn pricing(
|
||||
&self,
|
||||
pricing: &EditorGenerationPricingConfig,
|
||||
args: &GenerateSoundEffectToolArgs,
|
||||
) -> u32 {
|
||||
let model = args
|
||||
.model
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or(EDITOR_SOUND_EFFECT_MODEL_VIDU);
|
||||
pricing.sound_effect_model_mud_points(Some(model))
|
||||
}
|
||||
}
|
||||
|
||||
impl GenerateSoundEffectTool {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn execute(
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use crate::editor_agent::editor_tools::common::EditorToolContext;
|
||||
use crate::editor_agent::editor_tools::common::{
|
||||
EditorAgentPricedTool, EditorToolContext, editor_agent_image_mud_points,
|
||||
};
|
||||
use crate::editor_agent::editor_tools::generate_character::format_tool_message;
|
||||
use crate::editor_agent::editor_tools::generate_image::{
|
||||
EditorImageGenerationResult, GenerateImageError, GenerateImageTool, GenerateImageToolArgs,
|
||||
GenerateImageToolOutput,
|
||||
};
|
||||
use crate::editor_generation_config::EditorGenerationPricingConfig;
|
||||
use crate::editor_project::EditorGenerationCaller;
|
||||
use crate::http_error::AppError;
|
||||
use crate::request_context::RequestContext;
|
||||
@@ -61,6 +64,16 @@ impl Tool for GenerateUiDesignTool {
|
||||
}
|
||||
}
|
||||
|
||||
impl EditorAgentPricedTool for GenerateUiDesignTool {
|
||||
fn pricing(
|
||||
&self,
|
||||
pricing: &EditorGenerationPricingConfig,
|
||||
args: &GenerateImageToolArgs,
|
||||
) -> u32 {
|
||||
editor_agent_image_mud_points(pricing, Some("ui-design"), args.image_size.as_deref())
|
||||
}
|
||||
}
|
||||
|
||||
impl GenerateUiDesignTool {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn execute(
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use crate::character_animation_assets::generate_editor_video_for_owner;
|
||||
use crate::editor_agent::editor_tools::common::EditorAgentPricedTool;
|
||||
use crate::editor_agent::editor_tools::generate_sound_effect::{
|
||||
map_media_response_error, parse_media_response, pending_message,
|
||||
};
|
||||
use crate::editor_agent::utils::ImageId;
|
||||
use crate::editor_generation_config::EditorGenerationPricingConfig;
|
||||
use crate::http_error::AppError;
|
||||
use crate::request_context::RequestContext;
|
||||
use crate::state::AppState;
|
||||
@@ -22,6 +24,13 @@ pub struct GenerateVideoTool {
|
||||
pub context: crate::editor_agent::editor_tools::common::EditorToolContext,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
const DEFAULT_VIDEO_MODEL: &str = "seedance2.0-fast";
|
||||
#[allow(dead_code)]
|
||||
const DEFAULT_VIDEO_RESOLUTION: &str = "720p";
|
||||
#[allow(dead_code)]
|
||||
const DEFAULT_VIDEO_DURATION_SECONDS: u32 = 4;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum GenerateVideoError {
|
||||
PromptNotProvided,
|
||||
@@ -107,6 +116,31 @@ impl Tool for GenerateVideoTool {
|
||||
}
|
||||
}
|
||||
|
||||
impl EditorAgentPricedTool for GenerateVideoTool {
|
||||
fn pricing(
|
||||
&self,
|
||||
pricing: &EditorGenerationPricingConfig,
|
||||
args: &GenerateVideoToolArgs,
|
||||
) -> u32 {
|
||||
let model = args
|
||||
.model
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or(DEFAULT_VIDEO_MODEL);
|
||||
let resolution = args
|
||||
.resolution
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or(DEFAULT_VIDEO_RESOLUTION);
|
||||
let duration_seconds = args
|
||||
.duration_seconds
|
||||
.unwrap_or(DEFAULT_VIDEO_DURATION_SECONDS);
|
||||
pricing.video_model_mud_points(Some(model), resolution, duration_seconds)
|
||||
}
|
||||
}
|
||||
|
||||
impl GenerateVideoTool {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn execute(
|
||||
|
||||
@@ -7,3 +7,6 @@ pub mod generate_image;
|
||||
pub mod generate_sound_effect;
|
||||
pub mod generate_ui_design;
|
||||
pub mod generate_video;
|
||||
|
||||
#[cfg(test)]
|
||||
mod pricing_tests;
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::editor_agent::editor_tools::common::{EditorAgentPricedTool, EditorToolContext};
|
||||
use crate::editor_agent::editor_tools::edit_image::{EditImageTool, EditImageToolArgs};
|
||||
use crate::editor_agent::editor_tools::generate_background_music::{
|
||||
GenerateBackgroundMusicTool, GenerateBackgroundMusicToolArgs,
|
||||
};
|
||||
use crate::editor_agent::editor_tools::generate_character::GenerateCharacterTool;
|
||||
use crate::editor_agent::editor_tools::generate_icon_spritesheet::{
|
||||
GenerateIconSpritesheetTool, GenerateIconSpritesheetToolArgs,
|
||||
};
|
||||
use crate::editor_agent::editor_tools::generate_image::{GenerateImageTool, GenerateImageToolArgs};
|
||||
use crate::editor_agent::editor_tools::generate_sound_effect::{
|
||||
GenerateSoundEffectTool, GenerateSoundEffectToolArgs,
|
||||
};
|
||||
use crate::editor_agent::editor_tools::generate_ui_design::GenerateUiDesignTool;
|
||||
use crate::editor_agent::editor_tools::generate_video::{GenerateVideoTool, GenerateVideoToolArgs};
|
||||
use crate::editor_agent::utils::ImageId;
|
||||
use crate::editor_generation_config::load_editor_generation_pricing_from_paths;
|
||||
|
||||
fn context() -> EditorToolContext {
|
||||
EditorToolContext {
|
||||
images: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn image_args(image_size: Option<&str>) -> GenerateImageToolArgs {
|
||||
GenerateImageToolArgs {
|
||||
prompt: "生成图片".to_string(),
|
||||
reference_image_ids: Vec::new(),
|
||||
aspect_ratio: Some("1:1".to_string()),
|
||||
image_size: image_size.map(ToOwned::to_owned),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_editor_agent_tool_exposes_argument_based_pricing() {
|
||||
let pricing = load_editor_generation_pricing_from_paths(None)
|
||||
.expect("default editor pricing should load");
|
||||
let context = context();
|
||||
|
||||
assert_eq!(
|
||||
EditImageTool {
|
||||
context: context.clone(),
|
||||
}
|
||||
.pricing(
|
||||
&pricing,
|
||||
&EditImageToolArgs {
|
||||
object_image_id: ImageId {
|
||||
id: "image-1".to_string(),
|
||||
},
|
||||
reference_image_ids: Vec::new(),
|
||||
prompt: "改成蓝色".to_string(),
|
||||
},
|
||||
),
|
||||
3
|
||||
);
|
||||
assert_eq!(
|
||||
GenerateImageTool {
|
||||
context: context.clone(),
|
||||
}
|
||||
.pricing(&pricing, &image_args(Some("2K"))),
|
||||
5
|
||||
);
|
||||
assert_eq!(
|
||||
GenerateCharacterTool {
|
||||
context: context.clone(),
|
||||
}
|
||||
.pricing(&pricing, &image_args(None)),
|
||||
3
|
||||
);
|
||||
assert_eq!(
|
||||
GenerateUiDesignTool {
|
||||
context: context.clone(),
|
||||
}
|
||||
.pricing(&pricing, &image_args(Some("2K"))),
|
||||
5
|
||||
);
|
||||
assert_eq!(
|
||||
GenerateIconSpritesheetTool {
|
||||
context: context.clone(),
|
||||
}
|
||||
.pricing(
|
||||
&pricing,
|
||||
&GenerateIconSpritesheetToolArgs {
|
||||
reference_image_id: ImageId {
|
||||
id: "image-1".to_string(),
|
||||
},
|
||||
reference_image_ids: Vec::new(),
|
||||
icon_descriptions: vec!["背包".to_string(), "地图".to_string()],
|
||||
aspect_ratio: Some("1:1".to_string()),
|
||||
image_size: Some("2K".to_string()),
|
||||
},
|
||||
),
|
||||
5
|
||||
);
|
||||
assert_eq!(
|
||||
GenerateVideoTool {
|
||||
context: context.clone(),
|
||||
}
|
||||
.pricing(
|
||||
&pricing,
|
||||
&GenerateVideoToolArgs {
|
||||
prompt: "镜头缓慢推进".to_string(),
|
||||
reference_image_ids: Vec::new(),
|
||||
aspect_ratio: None,
|
||||
duration_seconds: Some(6),
|
||||
model: Some("seedance2.0".to_string()),
|
||||
resolution: Some("720p".to_string()),
|
||||
sound: None,
|
||||
},
|
||||
),
|
||||
144
|
||||
);
|
||||
assert_eq!(
|
||||
GenerateSoundEffectTool.pricing(
|
||||
&pricing,
|
||||
&GenerateSoundEffectToolArgs {
|
||||
prompt: "按钮点击声".to_string(),
|
||||
duration: None,
|
||||
model: None,
|
||||
},
|
||||
),
|
||||
5
|
||||
);
|
||||
assert_eq!(
|
||||
GenerateBackgroundMusicTool.pricing(
|
||||
&pricing,
|
||||
&GenerateBackgroundMusicToolArgs {
|
||||
prompt: "轻松背景音乐".to_string(),
|
||||
make_instrumental: true,
|
||||
},
|
||||
),
|
||||
12
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pricing_uses_the_supplied_runtime_snapshot() {
|
||||
let mut pricing = load_editor_generation_pricing_from_paths(None)
|
||||
.expect("default editor pricing should load");
|
||||
pricing
|
||||
.models
|
||||
.get_mut("gpt-image-2")
|
||||
.expect("gpt image pricing should exist")
|
||||
.prices
|
||||
.insert("2K".to_string(), 37);
|
||||
pricing
|
||||
.models
|
||||
.get_mut("seedance2.0-fast")
|
||||
.expect("video pricing should exist")
|
||||
.prices
|
||||
.insert("720p".to_string(), 7);
|
||||
pricing
|
||||
.models
|
||||
.get_mut("audio1.0")
|
||||
.expect("sound pricing should exist")
|
||||
.price = Some(19);
|
||||
|
||||
assert_eq!(
|
||||
GenerateImageTool { context: context() }.pricing(&pricing, &image_args(Some("2K"))),
|
||||
37
|
||||
);
|
||||
assert_eq!(
|
||||
GenerateVideoTool { context: context() }.pricing(
|
||||
&pricing,
|
||||
&GenerateVideoToolArgs {
|
||||
prompt: "four seconds".to_string(),
|
||||
reference_image_ids: Vec::new(),
|
||||
aspect_ratio: None,
|
||||
duration_seconds: None,
|
||||
model: None,
|
||||
resolution: None,
|
||||
sound: None,
|
||||
},
|
||||
),
|
||||
28
|
||||
);
|
||||
assert_eq!(
|
||||
GenerateSoundEffectTool.pricing(
|
||||
&pricing,
|
||||
&GenerateSoundEffectToolArgs {
|
||||
prompt: "sound".to_string(),
|
||||
duration: None,
|
||||
model: None,
|
||||
},
|
||||
),
|
||||
19
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user