5c9b5ef69e
主要改动 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>
160 lines
5.4 KiB
Rust
160 lines
5.4 KiB
Rust
//! 通用抠图冒烟验证:本地图片 → 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()
|
||
}
|