Files
Genarrative/server-rs/crates/platform-matting/examples/segment_smoke.rs
T
lhk229 5c9b5ef69e 动作视频/生图背景色链路升级 + 阿里云抠图接入 (#75)
主要改动
1. 阿里云通用抠图接入(新增 platform-matting crate)

手搓阿里云 VIAPI 通用抠图接口(ACS3-HMAC-SHA256 签名),跨区输入走临时桶通道。
图片抠图升级为三档降级:BgFilter → 阿里云 → 本地键色兜底。
动作视频逐帧抠图改用阿里云,remove_editor_generated_green_screen_background 降为最后兜底。
2. 背景色不再写死绿色,改多背景色自动决策

动作视频背景色和生图一样从候选调色板自动选,选项继承。
3. 视觉背景色决策(修"蓝撞蓝")

让 LLM 看参考图选背景色(gpt-4o-mini 视觉模型,路由到 VectorEngine 视觉客户端),修复原纯文本盲选。
4. 背景色候选硬过滤器(新增 editor_screen_background_filter)

按参考图前景配色算 Lab 危险质量 + 皮肤专属三判据(ΔE 距离 / 色调投影 / RGB 分离)剔除撞色候选,LLM 只在安全集合里审美选。露肤角色自动收敛到冷区。新增"灰竹绿"候选补齐安全弧绿色段。
5. 源图合成到背景色(修视频背景变白)

透明源图先填成选定实色再发给 Ark,视频背景确定性等于抠图键色,不再赌模型服从提示词。
6. screenColorHex 结构化记录 + 死代码清理

四条链路统一把实际背景色写进 generation_inputs,修复生图链路"抠图背景色"字段从未生效的旧逻辑。
清理前端抠绿 + qwenSprite 死代码,移除一次性探针示例。

Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/75
Reviewed-by: kdletters <kdletters@qq.com>
Co-authored-by: Linghong <ink29535@proton.me>
Co-committed-by: Linghong <ink29535@proton.me>
2026-07-10 18:22:17 +08:00

160 lines
5.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 通用抠图冒烟验证:本地图片 → OSS → 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)。
use std::path::{Path, PathBuf};
use platform_matting::{
DEFAULT_IMAGESEG_ENDPOINT, MattingClient, MattingConfig, SegmentCommonImageRequest,
SegmentReturnForm,
};
fn load_env_files() {
// example 的工作目录通常是 server-rs,.env 都在仓库根目录。
for candidate in [
".env",
".env.local",
".env.secrets.local",
"../.env",
"../.env.local",
"../.env.secrets.local",
] {
if Path::new(candidate).exists() {
let _ = dotenvy::from_path_override(candidate);
}
}
}
#[tokio::main]
async fn main() {
load_env_files();
let input_path = std::env::args().nth(1).unwrap_or_else(|| {
eprintln!("用法:cargo run -p platform-matting --example segment_smoke -- <图片路径>");
std::process::exit(1);
});
let input_bytes = std::fs::read(&input_path)
.unwrap_or_else(|error| panic!("读取测试图片失败({input_path}):{error}"));
println!("[1/5] 已读取测试图片:{input_path}({} 字节)", input_bytes.len());
// SegmentCommonImage 要求分辨率低于 2000x2000,超限先等比缩小。
const MAX_EDGE: u32 = 1999;
let decoded = image::load_from_memory(&input_bytes).expect("测试图片应可解码");
let input_bytes = if decoded.width() > MAX_EDGE || decoded.height() > MAX_EDGE {
let resized = decoded.resize(
MAX_EDGE,
MAX_EDGE,
image::imageops::FilterType::CatmullRom,
);
let mut buffer = std::io::Cursor::new(Vec::new());
resized
.write_to(&mut buffer, image::ImageFormat::Png)
.expect("缩放后的图片应可编码为 PNG");
println!(
" 分辨率 {}x{} 超限,已缩放到 {}x{}",
decoded.width(),
decoded.height(),
resized.width(),
resized.height()
);
buffer.into_inner()
} else {
input_bytes
};
let http_client = reqwest::Client::new();
// --- 调用通用抠图 ---
// key 优先级:VIAPI 专用 → 官方 SDK 标准命名(#IMAGE_CALL)→ 短信 key 兜底。
let (matting_key_id, matting_key_secret) = [
("ALIYUN_IMAGESEG_ACCESS_KEY_ID", "ALIYUN_IMAGESEG_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)| {
let id = std::env::var(id_name).ok()?;
let secret = std::env::var(secret_name).ok()?;
if id.trim().is_empty() || secret.trim().is_empty() {
return None;
}
println!(" 使用 {id_name} 调用抠图服务");
Some((id, secret))
})
.expect("未找到可用的抠图 AccessKey 环境变量");
let matting_config = MattingConfig::new(
std::env::var("ALIYUN_IMAGESEG_ENDPOINT")
.unwrap_or_else(|_| DEFAULT_IMAGESEG_ENDPOINT.to_string()),
matting_key_id,
matting_key_secret,
)
.expect("抠图配置应有效");
let matting_client = MattingClient::new(matting_config).expect("抠图客户端应可构建");
// 本地 OSS 在北京地域,抠图服务要求上海地域,走 VIAPI 官方临时桶上传。
let temp_url = matting_client
.upload_temp_image(input_bytes, "segment-input.png", "image/png")
.await
.expect("上传 VIAPI 临时桶应成功");
println!("[2/5] 已上传 VIAPI 临时桶");
println!("[3/5] 输入图 URL host:{}", host_of(&temp_url));
let result = matting_client
.segment_common_image(SegmentCommonImageRequest {
image_url: temp_url,
return_form: Some(SegmentReturnForm::Crop),
})
.await;
let result = match result {
Ok(result) => result,
Err(error) => {
eprintln!("[4/5] 通用抠图调用失败:{error}");
std::process::exit(1);
}
};
println!(
"[4/5] 通用抠图成功,RequestId={}",
result.request_id.as_deref().unwrap_or("unknown")
);
// --- 下载结果 ---
let output_bytes = http_client
.get(&result.image_url)
.send()
.await
.expect("下载抠图结果应成功")
.error_for_status()
.expect("抠图结果 URL 应返回 200")
.bytes()
.await
.expect("读取抠图结果字节应成功");
let output_path = build_output_path(&input_path);
std::fs::write(&output_path, &output_bytes).expect("写出抠图结果应成功");
println!(
"[5/5] 抠图结果已保存:{}({} 字节)",
output_path.display(),
output_bytes.len()
);
}
fn build_output_path(input_path: &str) -> PathBuf {
let input = Path::new(input_path);
let stem = input
.file_stem()
.and_then(|value| value.to_str())
.unwrap_or("segment-output");
input.with_file_name(format!("{stem}-matting.png"))
}
fn host_of(url: &str) -> String {
url.split("//")
.nth(1)
.and_then(|rest| rest.split('/').next())
.unwrap_or("unknown")
.to_string()
}