内存优化 (#91)
BGfilter服务调用改为传url,减少内存占用 Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/91 Reviewed-by: 段舒康 <kdletters@qq.com> Co-authored-by: Linghong <ink29535@proton.me> Co-committed-by: Linghong <ink29535@proton.me>
This commit was merged in pull request #91.
This commit is contained in:
Generated
-3
@@ -4495,18 +4495,15 @@ dependencies = [
|
||||
name = "platform-matting"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"dotenvy",
|
||||
"hex",
|
||||
"hmac",
|
||||
"httpdate",
|
||||
"image",
|
||||
"platform-oss",
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sha1",
|
||||
"sha2",
|
||||
"time",
|
||||
"tokio",
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
//! 阿里云通用抠图在 api-server 侧的适配层。
|
||||
//!
|
||||
//! 输入 URL 策略:抠图服务只认上海地域 OSS URL,而我们没有上海地域自有 OSS,
|
||||
//! 统一由 platform-matting 上传 VIAPI 官方临时桶(1 天自动过期,无需清理)。
|
||||
//! 输入统一由 platform-matting 通过 AuthorizeFileUpload 单对象 Policy 上传动态临时 OSS。
|
||||
|
||||
use axum::http::StatusCode;
|
||||
use platform_matting::MattingError;
|
||||
use serde_json::json;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::{
|
||||
http_error::AppError, openai_image_generation::DownloadedOpenAiImage, state::AppState,
|
||||
};
|
||||
|
||||
/// 图片字节 → 阿里云通用抠图 → 原尺寸透明 PNG。
|
||||
/// 未配置抠图客户端时返回错误,由调用方决定是否降级本地算法。
|
||||
pub(crate) async fn segment_image_with_aliyun_matting(
|
||||
/// 私有 OSS 签名 URL → 延迟下载 → 阿里云临时桶 → 原尺寸透明 PNG。
|
||||
/// 下载和临时上传由 platform-matting 收口,源图缓冲不会跨越整次阿里云推理常驻。
|
||||
pub(crate) async fn segment_image_url_with_aliyun_matting(
|
||||
state: &AppState,
|
||||
image: &DownloadedOpenAiImage,
|
||||
image_url: &str,
|
||||
log_label: &str,
|
||||
) -> Result<DownloadedOpenAiImage, AppError> {
|
||||
let matting_client = state
|
||||
@@ -25,7 +24,7 @@ pub(crate) async fn segment_image_with_aliyun_matting(
|
||||
let file_name = format!("{log_label}.png");
|
||||
let started_at = std::time::Instant::now();
|
||||
let output_bytes = matting_client
|
||||
.segment_image_to_transparent_png(image.bytes.as_slice(), &file_name)
|
||||
.segment_image_url_to_transparent_png(image_url, &file_name)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
aliyun_matting_failure_to_app_error(&error, started_at.elapsed().as_millis() as u64)
|
||||
@@ -34,7 +33,7 @@ pub(crate) async fn segment_image_with_aliyun_matting(
|
||||
provider = "aliyun-matting",
|
||||
log_label,
|
||||
elapsed_ms = started_at.elapsed().as_millis() as u64,
|
||||
"阿里云通用抠图完成"
|
||||
"阿里云通用抠图 URL 输入完成"
|
||||
);
|
||||
|
||||
Ok(DownloadedOpenAiImage {
|
||||
@@ -48,18 +47,36 @@ pub(crate) async fn segment_image_with_aliyun_matting(
|
||||
///
|
||||
/// 分类(是否外部调用、超时、传输层故障、上游 HTTP 状态)由 platform-matting 在错误发生处
|
||||
/// 结构化捕获,这里只做协议中立的读取,不再从中文 message 反推——外部供应商协议归属留在
|
||||
/// platform-* 层。`InvalidConfig` / `InvalidRequest` / `Sign` 是发请求前的本地预检失败,
|
||||
/// `external_call_attempted()` 为 false,据此跳过外部失败审计。
|
||||
/// platform-* 层。`InvalidConfig` / `InvalidRequest` / `Sign` 是尚未开始外部调用的本地预检失败;
|
||||
/// URL 链路在 OSS GET 成功后发生的解码、尺寸或其它本地处理失败由 `LocalProcessing` 表示,
|
||||
/// 仍然需要进入外部失败审计,但不得包装成可重试的上游 5xx。
|
||||
fn aliyun_matting_failure_to_app_error(error: &MattingError, latency_ms: u64) -> AppError {
|
||||
let message = error.message();
|
||||
if !error.external_call_attempted() {
|
||||
// 本地预检失败(未配置 / 图片解码失败 / 尺寸过小 / 签名构造失败),未触达阿里云。
|
||||
// 本地预检失败(未配置 / 尚未下载源图前的参数或签名错误),未触达外部调用链路。
|
||||
return AppError::from_status(StatusCode::UNPROCESSABLE_ENTITY).with_details(json!({
|
||||
"provider": "aliyun-matting",
|
||||
"message": message,
|
||||
"timeout": false,
|
||||
"transport": false,
|
||||
"localProcessing": false,
|
||||
"externalCallAttempted": false,
|
||||
"failureStage": error.failure_stage(),
|
||||
"latencyMs": latency_ms,
|
||||
"rawExcerpt": message.chars().take(500).collect::<String>(),
|
||||
}));
|
||||
}
|
||||
// 外部调用已开始,但失败在本地解码 / 校验 / 归一等阶段:要审计,不能标成上游 5xx / 可重试。
|
||||
if matches!(error, MattingError::LocalProcessing(_)) {
|
||||
return AppError::from_status(StatusCode::UNPROCESSABLE_ENTITY).with_details(json!({
|
||||
"provider": "aliyun-matting",
|
||||
"message": message,
|
||||
"timeout": false,
|
||||
"transport": false,
|
||||
"localProcessing": true,
|
||||
"upstreamStatus": Value::Null,
|
||||
"externalCallAttempted": true,
|
||||
"failureStage": error.failure_stage(),
|
||||
"latencyMs": latency_ms,
|
||||
"rawExcerpt": message.chars().take(500).collect::<String>(),
|
||||
}));
|
||||
@@ -75,7 +92,10 @@ fn aliyun_matting_failure_to_app_error(error: &MattingError, latency_ms: u64) ->
|
||||
"message": message,
|
||||
"timeout": timeout,
|
||||
"transport": error.is_transport(),
|
||||
"localProcessing": false,
|
||||
"upstreamStatus": error.upstream_status(),
|
||||
"externalCallAttempted": true,
|
||||
"failureStage": error.failure_stage(),
|
||||
"latencyMs": latency_ms,
|
||||
"rawExcerpt": message.chars().take(500).collect::<String>(),
|
||||
}))
|
||||
@@ -86,6 +106,7 @@ fn aliyun_matting_unconfigured_error() -> AppError {
|
||||
"provider": "aliyun-matting",
|
||||
"message": "阿里云抠图客户端未配置或未启用。",
|
||||
"externalCallAttempted": false,
|
||||
"failureStage": "preflight",
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -100,6 +121,21 @@ mod tests {
|
||||
assert!(!crate::external_api_audit::matting_failure_external_call_attempted(&error));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_input_path_delegates_download_and_temp_upload_to_platform_matting() {
|
||||
let source = include_str!("aliyun_matting.rs");
|
||||
let start = source
|
||||
.find("pub(crate) async fn segment_image_url_with_aliyun_matting")
|
||||
.expect("URL input adapter should exist");
|
||||
let tail = &source[start..];
|
||||
let end = tail
|
||||
.find("/// 把 platform-matting 的错误映射")
|
||||
.expect("URL input adapter should end before error mapper");
|
||||
let body = &tail[..end];
|
||||
|
||||
assert!(body.contains("segment_image_url_to_transparent_png"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_preflight_failures_do_not_count_as_external_call() {
|
||||
for error in [
|
||||
@@ -108,7 +144,7 @@ mod tests {
|
||||
),
|
||||
MattingError::InvalidRequest("解析待抠图图片失败:invalid png".to_string()),
|
||||
MattingError::InvalidConfig("endpoint 为空".to_string()),
|
||||
MattingError::Sign("初始化 OSS V1 签名器失败".to_string()),
|
||||
MattingError::Sign("构造 AuthorizeFileUpload 签名失败".to_string()),
|
||||
] {
|
||||
let mapped = aliyun_matting_failure_to_app_error(&error, 3);
|
||||
|
||||
@@ -129,6 +165,79 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_processing_after_source_download_is_audited_with_failure_stage() {
|
||||
for (error, expected_stage) in [
|
||||
(
|
||||
MattingError::InvalidRequest("解析待抠图图片失败:invalid png".to_string())
|
||||
.with_failure_stage("source_decode"),
|
||||
"source_decode",
|
||||
),
|
||||
(
|
||||
MattingError::InvalidRequest("待抠图图片尺寸 16x16 过小".to_string())
|
||||
.with_failure_stage("source_validate"),
|
||||
"source_validate",
|
||||
),
|
||||
] {
|
||||
let mapped = aliyun_matting_failure_to_app_error(&error, 3);
|
||||
|
||||
assert!(crate::external_api_audit::matting_failure_external_call_attempted(&mapped));
|
||||
// 本地处理失败要审计,但 HTTP 包装不得落成可重试 5xx。
|
||||
assert_eq!(mapped.status_code(), StatusCode::UNPROCESSABLE_ENTITY);
|
||||
let details = mapped.details().expect("details present");
|
||||
assert_eq!(
|
||||
details
|
||||
.get("externalCallAttempted")
|
||||
.and_then(|v| v.as_bool()),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
details.get("localProcessing").and_then(|v| v.as_bool()),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
details.get("transport").and_then(|v| v.as_bool()),
|
||||
Some(false)
|
||||
);
|
||||
assert!(
|
||||
details
|
||||
.get("upstreamStatus")
|
||||
.is_none_or(|value| value.is_null())
|
||||
);
|
||||
assert_eq!(
|
||||
details.get("failureStage").and_then(|v| v.as_str()),
|
||||
Some(expected_stage)
|
||||
);
|
||||
assert_eq!(
|
||||
crate::external_api_audit::matting_failure_audit_failure_stage(
|
||||
&mapped,
|
||||
"aliyun_segment",
|
||||
),
|
||||
expected_stage
|
||||
);
|
||||
assert_eq!(
|
||||
crate::external_api_audit::matting_failure_audit_status_code(&mapped),
|
||||
None,
|
||||
"本地处理失败没有上游 HTTP 状态,不能回退包装码"
|
||||
);
|
||||
let draft = crate::external_api_audit::build_matting_external_api_failure_draft(
|
||||
"aliyun-matting",
|
||||
"imageseg.example".to_string(),
|
||||
"editor-screen-background-removal",
|
||||
expected_stage,
|
||||
crate::external_api_audit::matting_failure_audit_status_code(&mapped),
|
||||
crate::external_api_audit::matting_failure_audit_timeout(&mapped),
|
||||
crate::external_api_audit::matting_failure_audit_is_transport(&mapped),
|
||||
crate::external_api_audit::matting_failure_audit_latency_ms(&mapped),
|
||||
mapped.message().to_string(),
|
||||
crate::external_api_audit::matting_failure_audit_raw_excerpt(&mapped),
|
||||
&crate::external_api_audit::ExternalApiAuditContext::default(),
|
||||
);
|
||||
assert_eq!(draft.status_class, Some("local"));
|
||||
assert!(!draft.retryable);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upstream_transport_failure_maps_to_retryable_transport() {
|
||||
let error = MattingError::upstream_transport_error(
|
||||
|
||||
@@ -79,7 +79,6 @@ use crate::{
|
||||
resolve_editor_screen_background_color,
|
||||
},
|
||||
http_error::AppError,
|
||||
openai_image_generation::DownloadedOpenAiImage,
|
||||
platform_errors::map_oss_error,
|
||||
prompt::role_asset_studio::{
|
||||
build_role_asset_workflow, normalize_animation_prompt_text_by_key,
|
||||
@@ -2418,7 +2417,7 @@ async fn process_and_persist_editor_character_animation_frame(
|
||||
audit: &crate::external_api_audit::ExternalApiAuditContext,
|
||||
) -> Result<ProcessedEditorCharacterAnimationFrame, AppError> {
|
||||
// 中文注释:每一帧只要求自己的绿幕源图先落 OSS,不再等待整批源图全部上传完成。
|
||||
put_character_animation_object(
|
||||
let source_put = put_character_animation_object(
|
||||
state,
|
||||
LegacyAssetPrefix::Animations,
|
||||
vec![
|
||||
@@ -2432,7 +2431,7 @@ async fn process_and_persist_editor_character_animation_frame(
|
||||
frame.extension
|
||||
),
|
||||
frame.mime_type.clone(),
|
||||
frame.bytes.clone(),
|
||||
frame.bytes,
|
||||
build_asset_metadata(
|
||||
EDITOR_CHARACTER_ANIMATION_ASSET_KIND,
|
||||
owner_user_id,
|
||||
@@ -2444,14 +2443,9 @@ async fn process_and_persist_editor_character_animation_frame(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let image = DownloadedOpenAiImage {
|
||||
bytes: frame.bytes,
|
||||
mime_type: frame.mime_type,
|
||||
extension: frame.extension,
|
||||
};
|
||||
let removed = remove_editor_generated_screen_background_with_bgfilter_with_request_timeout(
|
||||
state,
|
||||
&image,
|
||||
source_put.object_key.as_str(),
|
||||
screen_color,
|
||||
EDITOR_BGFILTER_DEFAULT_SEG_MODEL,
|
||||
EDITOR_BGFILTER_CROSS_CHECK_ENABLED,
|
||||
@@ -6223,6 +6217,7 @@ mod tests {
|
||||
"put_character_animation_object",
|
||||
"green-screen-frame",
|
||||
"remove_editor_generated_screen_background_with_bgfilter_with_request_timeout",
|
||||
"source_put.object_key.as_str()",
|
||||
"bgfilter_request_timeout_ms",
|
||||
"finalize_animation_frame_payload",
|
||||
"put_character_animation_object",
|
||||
@@ -6244,6 +6239,13 @@ mod tests {
|
||||
"finalize_animation_frame_payload",
|
||||
],
|
||||
);
|
||||
let frame_pipeline = source
|
||||
.split_once("async fn process_and_persist_editor_character_animation_frame")
|
||||
.and_then(|(_, tail)| tail.split_once("async fn publish_animation_set"))
|
||||
.map(|(body, _)| body)
|
||||
.expect("frame pipeline function should exist");
|
||||
assert!(!frame_pipeline.contains("frame.bytes.clone()"));
|
||||
assert!(!frame_pipeline.contains("DownloadedOpenAiImage"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -159,6 +159,7 @@ pub(crate) async fn record_matting_external_api_failure(
|
||||
failure_stage: &'static str,
|
||||
status_code: Option<u16>,
|
||||
timeout: bool,
|
||||
transport: bool,
|
||||
latency_ms: Option<u64>,
|
||||
error_message: String,
|
||||
raw_excerpt: Option<String>,
|
||||
@@ -170,6 +171,7 @@ pub(crate) async fn record_matting_external_api_failure(
|
||||
failure_stage,
|
||||
status_code,
|
||||
timeout,
|
||||
transport,
|
||||
latency_ms,
|
||||
error_message,
|
||||
raw_excerpt,
|
||||
@@ -178,31 +180,41 @@ pub(crate) async fn record_matting_external_api_failure(
|
||||
record_external_api_failure(state, draft).await;
|
||||
}
|
||||
|
||||
/// 构建抠图失败审计 draft。`transport` 必须由调用方从错误结构化字段读取,
|
||||
/// 不能用 `status_code.is_none()` 反推——本地处理失败同样没有上游 HTTP 状态。
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn build_matting_external_api_failure_draft(
|
||||
pub(crate) fn build_matting_external_api_failure_draft(
|
||||
provider: &'static str,
|
||||
endpoint: String,
|
||||
operation: &'static str,
|
||||
failure_stage: &'static str,
|
||||
status_code: Option<u16>,
|
||||
timeout: bool,
|
||||
transport: bool,
|
||||
latency_ms: Option<u64>,
|
||||
error_message: String,
|
||||
raw_excerpt: Option<String>,
|
||||
context: &ExternalApiAuditContext,
|
||||
) -> ExternalApiFailureDraft {
|
||||
// status_code=None ⟺ statusClass=transport(见 status_class):DNS / 连接重置 / 读体中断 / 超时
|
||||
// 这类传输层失败没有上游 HTTP 状态。它们必须与 "transport failures actionable" 语义一致,
|
||||
// 记为 retryable=true,否则 statusClass=transport 却 retryable=false 会误导告警 / 重试分析。
|
||||
let has_transport_error = status_code.is_none();
|
||||
// 传输层:无上游 HTTP 状态 + timeout/transport 标记 → statusClass=transport、retryable=true。
|
||||
// 本地处理:无上游 HTTP 状态且非 transport → statusClass=local、retryable=false。
|
||||
// 不得把「status_code=None」一律当成 transport,否则 LocalProcessing 会污染可重试 5xx 分析。
|
||||
let is_transport_failure = timeout || transport;
|
||||
let resolved_status_class = if is_transport_failure && status_code.is_none() {
|
||||
"transport"
|
||||
} else if status_code.is_none() {
|
||||
"local"
|
||||
} else {
|
||||
status_class(status_code)
|
||||
};
|
||||
ExternalApiFailureDraft::new(provider, endpoint, operation, failure_stage, error_message)
|
||||
.with_status_code(status_code)
|
||||
.with_optional_status_class(Some(status_class(status_code)))
|
||||
.with_optional_status_class(Some(resolved_status_class))
|
||||
.with_timeout(timeout)
|
||||
.with_retryable(is_retryable_external_api_failure(
|
||||
status_code,
|
||||
timeout,
|
||||
has_transport_error,
|
||||
is_transport_failure,
|
||||
))
|
||||
.with_latency_ms(latency_ms)
|
||||
.with_raw_excerpt(raw_excerpt)
|
||||
@@ -210,21 +222,31 @@ fn build_matting_external_api_failure_draft(
|
||||
}
|
||||
|
||||
pub(crate) fn matting_failure_audit_status_code(error: &AppError) -> Option<u16> {
|
||||
error
|
||||
// 真实上游 HTTP 状态优先;本地处理 / 传输层都没有可写的上游 status。
|
||||
if let Some(status) = error
|
||||
.details()
|
||||
.and_then(|details| details.get("upstreamStatus"))
|
||||
.and_then(Value::as_u64)
|
||||
.and_then(|value| u16::try_from(value).ok())
|
||||
.or_else(|| {
|
||||
if matting_failure_audit_is_transport(error) {
|
||||
None
|
||||
} else {
|
||||
Some(error.status_code().as_u16())
|
||||
}
|
||||
})
|
||||
{
|
||||
return Some(status);
|
||||
}
|
||||
if matting_failure_audit_is_local_processing(error) || matting_failure_audit_is_transport(error)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(error.status_code().as_u16())
|
||||
}
|
||||
|
||||
fn matting_failure_audit_is_transport(error: &AppError) -> bool {
|
||||
pub(crate) fn matting_failure_audit_is_local_processing(error: &AppError) -> bool {
|
||||
error
|
||||
.details()
|
||||
.and_then(|details| details.get("localProcessing"))
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub(crate) fn matting_failure_audit_is_transport(error: &AppError) -> bool {
|
||||
matting_failure_audit_timeout(error)
|
||||
|| error
|
||||
.details()
|
||||
@@ -256,6 +278,30 @@ pub(crate) fn matting_failure_external_call_attempted(error: &AppError) -> bool
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
pub(crate) fn matting_failure_audit_failure_stage(
|
||||
error: &AppError,
|
||||
fallback: &'static str,
|
||||
) -> &'static str {
|
||||
let stage = error
|
||||
.details()
|
||||
.and_then(|details| details.get("failureStage"))
|
||||
.and_then(Value::as_str);
|
||||
match stage {
|
||||
Some("preflight") => "preflight",
|
||||
Some("source_download") => "source_download",
|
||||
Some("source_decode") => "source_decode",
|
||||
Some("source_validate") => "source_validate",
|
||||
Some("source_normalize") => "source_normalize",
|
||||
Some("temp_upload") => "temp_upload",
|
||||
Some("aliyun_segment") => "aliyun_segment",
|
||||
Some("result_download") => "result_download",
|
||||
Some("result_decode") => "result_decode",
|
||||
Some("result_validate") => "result_validate",
|
||||
Some("result_encode") => "result_encode",
|
||||
_ => fallback,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn matting_failure_audit_raw_excerpt(error: &AppError) -> Option<String> {
|
||||
error
|
||||
.details()
|
||||
@@ -575,7 +621,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn matting_failure_draft_marks_non_timeout_transport_retryable() {
|
||||
// status_code=None、timeout=false 的非超时 transport 失败(DNS / 连接重置 / 读体中断):
|
||||
// status_code=None、timeout=false、transport=true 的非超时传输故障:
|
||||
// statusClass 必须是 transport 且 retryable=true,否则与 "transport failures actionable" 冲突。
|
||||
let draft = build_matting_external_api_failure_draft(
|
||||
"bgfilter",
|
||||
@@ -584,6 +630,7 @@ mod tests {
|
||||
"bgfilter_segment",
|
||||
None,
|
||||
false,
|
||||
true,
|
||||
Some(67),
|
||||
"请求 BgFilter 服务失败:dns error".to_string(),
|
||||
Some("dns error".to_string()),
|
||||
@@ -599,6 +646,29 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matting_failure_draft_marks_local_processing_non_retryable() {
|
||||
// 本地处理失败没有上游 HTTP 状态,也不是 transport;不得记成可重试 5xx/transport。
|
||||
let draft = build_matting_external_api_failure_draft(
|
||||
"aliyun-matting",
|
||||
"imageseg.example".to_string(),
|
||||
"editor-screen-background-removal",
|
||||
"source_decode",
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
Some(3),
|
||||
"解析待抠图图片失败:invalid png".to_string(),
|
||||
Some("invalid png".to_string()),
|
||||
&ExternalApiAuditContext::default(),
|
||||
);
|
||||
|
||||
assert_eq!(draft.status_code, None);
|
||||
assert_eq!(draft.status_class, Some("local"));
|
||||
assert!(!draft.timeout);
|
||||
assert!(!draft.retryable);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matting_failure_draft_keeps_client_error_non_retryable() {
|
||||
// 真实上游 4xx(非 429 / 408)不是传输故障,仍应 retryable=false,不能被误判为可重试。
|
||||
@@ -609,6 +679,7 @@ mod tests {
|
||||
"aliyun_segment",
|
||||
Some(400),
|
||||
false,
|
||||
false,
|
||||
Some(12),
|
||||
"通用抠图接口返回失败(HTTP 400,Code=InvalidImage):bad image".to_string(),
|
||||
None,
|
||||
|
||||
@@ -5,13 +5,10 @@ version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
base64 = { workspace = true }
|
||||
hmac = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
httpdate = { workspace = true }
|
||||
image = { workspace = true, features = ["png", "jpeg", "webp"] }
|
||||
sha1 = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["json", "rustls-tls"] }
|
||||
reqwest = { workspace = true, features = ["json", "multipart", "rustls-tls"] }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_urlencoded = { workspace = true }
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
//! 通用抠图冒烟验证:本地图片 → OSS → SegmentCommonImage → 下载结果。
|
||||
//! 通用抠图冒烟验证:本地图片 → AuthorizeFileUpload 临时对象 → SegmentCommonImage → 下载结果。
|
||||
//!
|
||||
//! 运行(在 server-rs 目录下):
|
||||
//! cargo run -p platform-matting --example segment_smoke -- "C:\path\to\input.png"
|
||||
//!
|
||||
//! 依赖仓库根目录 .env.local(OSS bucket/endpoint)与 .env.secrets.local(AK/SK)。
|
||||
//! 依赖仓库根目录 .env / .env.local / .env.secrets.local 中的 AK/SK
|
||||
//!(`GENARRATIVE_ALIYUN_MATTING_*` 或 `ALIBABA_CLOUD_ACCESS_KEY_*`);
|
||||
//! 可选 `GENARRATIVE_ALIYUN_MATTING_ENDPOINT`。临时 OSS bucket/endpoint 由
|
||||
//! AuthorizeFileUpload 动态下发,不依赖自有 OSS 配置。
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
@@ -67,17 +70,16 @@ async fn main() {
|
||||
let http_client = reqwest::Client::new();
|
||||
|
||||
// --- 调用通用抠图 ---
|
||||
// key 优先级:VIAPI 专用 → 官方 SDK 标准命名(#IMAGE_CALL)→ 短信 key 兜底。
|
||||
// key 优先级与 api-server 配置保持一致:抠图专用 → 官方 SDK 标准命名。
|
||||
let (matting_key_id, matting_key_secret) = [
|
||||
(
|
||||
"ALIYUN_IMAGESEG_ACCESS_KEY_ID",
|
||||
"ALIYUN_IMAGESEG_ACCESS_KEY_SECRET",
|
||||
"GENARRATIVE_ALIYUN_MATTING_ACCESS_KEY_ID",
|
||||
"GENARRATIVE_ALIYUN_MATTING_ACCESS_KEY_SECRET",
|
||||
),
|
||||
(
|
||||
"ALIBABA_CLOUD_ACCESS_KEY_ID",
|
||||
"ALIBABA_CLOUD_ACCESS_KEY_SECRET",
|
||||
),
|
||||
("ALIYUN_SMS_ACCESS_KEY_ID", "ALIYUN_SMS_ACCESS_KEY_SECRET"),
|
||||
]
|
||||
.iter()
|
||||
.find_map(|(id_name, secret_name)| {
|
||||
@@ -91,7 +93,7 @@ async fn main() {
|
||||
})
|
||||
.expect("未找到可用的抠图 AccessKey 环境变量");
|
||||
let matting_config = MattingConfig::new(
|
||||
std::env::var("ALIYUN_IMAGESEG_ENDPOINT")
|
||||
std::env::var("GENARRATIVE_ALIYUN_MATTING_ENDPOINT")
|
||||
.unwrap_or_else(|_| DEFAULT_IMAGESEG_ENDPOINT.to_string()),
|
||||
matting_key_id,
|
||||
matting_key_secret,
|
||||
@@ -99,12 +101,12 @@ async fn main() {
|
||||
.expect("抠图配置应有效");
|
||||
let matting_client = MattingClient::new(matting_config).expect("抠图客户端应可构建");
|
||||
|
||||
// 本地 OSS 在北京地域,抠图服务要求上海地域,走 VIAPI 官方临时桶上传。
|
||||
// 非上海地域输入按新版官方 SDK 的 AdvanceRequest 口径申请单对象 Policy 后上传。
|
||||
let temp_url = matting_client
|
||||
.upload_temp_image(input_bytes, "segment-input.png", "image/png")
|
||||
.await
|
||||
.expect("上传 VIAPI 临时桶应成功");
|
||||
println!("[2/5] 已上传 VIAPI 临时桶");
|
||||
.expect("上传 AuthorizeFileUpload 临时对象应成功");
|
||||
println!("[2/5] 已上传 AuthorizeFileUpload 临时对象");
|
||||
println!("[3/5] 输入图 URL host:{}", host_of(&temp_url));
|
||||
|
||||
let result = matting_client
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user