将自动分离图像处理移出异步执行器

在 spawn_blocking 中构建标记图并编码写入处理图

在 spawn_blocking 中完成处理图裁切与文件输出

保留 image-edit 和视觉绑定网络请求的 async 调度边界
This commit is contained in:
2026-09-09 10:45:11 +08:00
parent 884c5ec5ee
commit 56fff8325b
@@ -16,7 +16,7 @@ use platform_llm::{
};
use serde::Deserialize;
use std::fs;
use std::path::Path;
use std::path::{Path, PathBuf};
pub fn apply_batch_patch(
state: &mut SeparationState,
tree_index: usize,
@@ -167,9 +167,32 @@ async fn raw_image_edit(
.ok_or_else(|| "图片分离响应没有图像".to_string())
}
fn build_marked_image(
async fn build_marked_image(
source_url: String,
nodes: Vec<SeparationNode>,
target: PathBuf,
) -> Result<String, String> {
tokio::task::spawn_blocking(move || {
let areas = nodes
.iter()
.map(|node| {
(
node.global_pos_x_px,
node.global_pos_y_px,
node.width_px,
node.height_px,
)
})
.collect::<Vec<_>>();
build_marked_image_blocking(&source_url, &areas, &target)
})
.await
.map_err(|error| format!("构建标记图任务失败:{error}"))?
}
fn build_marked_image_blocking(
source_url: &str,
nodes: &[&SeparationNode],
nodes: &[(u32, u32, u32, u32)],
target: &Path,
) -> Result<String, String> {
let encoded = source_url
@@ -184,17 +207,15 @@ fn build_marked_image(
.to_rgba8();
let width = image.width();
let height = image.height();
for node in nodes {
let x0 = node.global_pos_x_px.min(width.saturating_sub(1));
let y0 = node.global_pos_y_px.min(height.saturating_sub(1));
let x1 = node
.global_pos_x_px
.saturating_add(node.width_px)
for &(global_pos_x_px, global_pos_y_px, node_width_px, node_height_px) in nodes {
let x0 = global_pos_x_px.min(width.saturating_sub(1));
let y0 = global_pos_y_px.min(height.saturating_sub(1));
let x1 = global_pos_x_px
.saturating_add(node_width_px)
.min(width)
.saturating_sub(1);
let y1 = node
.global_pos_y_px
.saturating_add(node.height_px)
let y1 = global_pos_y_px
.saturating_add(node_height_px)
.min(height)
.saturating_sub(1);
if x0 >= x1 || y0 >= y1 {
@@ -229,6 +250,23 @@ fn build_marked_image(
))
}
async fn write_processed_image(processed_url: String, target: PathBuf) -> Result<(), String> {
tokio::task::spawn_blocking(move || {
let processed_bytes = base64::engine::general_purpose::STANDARD
.decode(
processed_url
.split_once(',')
.map(|(_, data)| data)
.unwrap_or_default(),
)
.map_err(|error| format!("解析处理图失败:{error}"))?;
fs::write(&target, processed_bytes)
.map_err(|error| format!("写入处理图失败:{}: {error}", target.display()))
})
.await
.map_err(|error| format!("写入处理图任务失败:{error}"))?
}
async fn visual_binding(
source_url: String,
processed_url: String,
@@ -337,7 +375,13 @@ pub(crate) async fn separate_ui_impl(
let prompt =
gen_extract_prompt(batch.iter().map(|n| n.note.clone()).collect::<Vec<_>>());
let marker_path = sidecar.join(format!("marked-{}.png", separation.bound.len()));
let marked_url = match build_marked_image(&source_url, &batch, &marker_path) {
let marked_url = match build_marked_image(
source_url.clone(),
batch_nodes.clone(),
marker_path,
)
.await
{
Ok(value) => value,
Err(error) => {
mark_batch_problematic(&mut separation, tree_index, &batch, error);
@@ -362,15 +406,7 @@ pub(crate) async fn separate_ui_impl(
}
};
let processed_path = sidecar.join(format!("processed-{}.png", separation.bound.len()));
let processed_bytes = base64::engine::general_purpose::STANDARD
.decode(
processed_url
.split_once(',')
.map(|(_, d)| d)
.unwrap_or_default(),
)
.map_err(|e| e.to_string())?;
fs::write(&processed_path, processed_bytes).map_err(|e| e.to_string())?;
write_processed_image(processed_url.clone(), processed_path.clone()).await?;
let binding = match visual_binding(source_url.clone(), processed_url, &batch).await {
Ok(value) => value,
Err(error) => {
@@ -388,7 +424,13 @@ pub(crate) async fn separate_ui_impl(
} = decision
{
let cut_path = sidecar.join(format!("cut-{}.png", to_node.as_str()));
match cut_processed_image(&processed_path, separated_image_area, &cut_path) {
match cut_processed_image(
processed_path.clone(),
*separated_image_area,
cut_path.clone(),
)
.await
{
Ok(()) => {
cut_paths
.insert(to_node.clone(), cut_path.to_string_lossy().to_string());
@@ -414,7 +456,21 @@ pub(crate) async fn separate_ui_impl(
Ok(separation_dto(&separation))
}
fn cut_processed_image(source: &Path, area: &BindingArea, target: &Path) -> Result<(), String> {
async fn cut_processed_image(
source: PathBuf,
area: BindingArea,
target: PathBuf,
) -> Result<(), String> {
tokio::task::spawn_blocking(move || cut_processed_image_blocking(&source, &area, &target))
.await
.map_err(|error| format!("裁切处理图任务失败:{error}"))?
}
fn cut_processed_image_blocking(
source: &Path,
area: &BindingArea,
target: &Path,
) -> Result<(), String> {
let image = image::open(source).map_err(|e| format!("读取处理图失败:{e}"))?;
area.validate_in(image.width(), image.height())?;
let cropped = image.crop_imm(