视觉绑定增加透明标记预处理
新增视觉绑定 RGBA 预处理与阈值复用 在请求前生成不透明洋红标记图并记录耗时 更新英文提示词说明标记色语义
This commit is contained in:
@@ -7,6 +7,14 @@ use std::time::Instant;
|
||||
/// intentional workflow decision rather than a scattered numeric literal.
|
||||
pub(crate) const MAX_BINDING_AREA_EDGE_ADJUSTMENT_PX: u32 = 32;
|
||||
|
||||
/// Alpha values below this threshold are treated as transparent for boundary
|
||||
/// detection. The cropped pixels themselves are preserved unchanged.
|
||||
pub(crate) const MIN_VISIBLE_ALPHA: u8 = 16;
|
||||
|
||||
/// An edge needs this many consecutive visible pixels to count as supported.
|
||||
/// The requirement is reduced to the edge length for one-pixel-wide elements.
|
||||
pub(crate) const MIN_CONSECUTIVE_VISIBLE_EDGE_PIXELS: usize = 2;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub(crate) struct NormalizedBindingArea {
|
||||
pub(crate) area: BindingArea,
|
||||
@@ -61,7 +69,35 @@ impl Edge {
|
||||
const ALL: [Self; 4] = [Self::Left, Self::Right, Self::Top, Self::Bottom];
|
||||
}
|
||||
|
||||
fn pixel_is_visible(alpha: u8) -> bool {
|
||||
alpha >= MIN_VISIBLE_ALPHA
|
||||
}
|
||||
|
||||
fn has_consecutive_visible_pixels<I>(alphas: I, required: usize) -> bool
|
||||
where
|
||||
I: IntoIterator<Item = u8>,
|
||||
{
|
||||
let required = required.max(1);
|
||||
let mut consecutive = 0usize;
|
||||
for alpha in alphas {
|
||||
if pixel_is_visible(alpha) {
|
||||
consecutive = consecutive.saturating_add(1);
|
||||
if consecutive >= required {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
consecutive = 0;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn edge_has_visible_pixel(image: &RgbaImage, rect: Rect, edge: Edge) -> bool {
|
||||
let edge_length = match edge {
|
||||
Edge::Left | Edge::Right => rect.bottom - rect.top,
|
||||
Edge::Top | Edge::Bottom => rect.right - rect.left,
|
||||
} as usize;
|
||||
let required = MIN_CONSECUTIVE_VISIBLE_EDGE_PIXELS.max(1).min(edge_length);
|
||||
match edge {
|
||||
Edge::Left | Edge::Right => {
|
||||
let x = if edge == Edge::Left {
|
||||
@@ -69,7 +105,10 @@ fn edge_has_visible_pixel(image: &RgbaImage, rect: Rect, edge: Edge) -> bool {
|
||||
} else {
|
||||
rect.right - 1
|
||||
};
|
||||
(rect.top..rect.bottom).any(|y| image.get_pixel(x, y).0[3] > 0)
|
||||
has_consecutive_visible_pixels(
|
||||
(rect.top..rect.bottom).map(|y| image.get_pixel(x, y).0[3]),
|
||||
required,
|
||||
)
|
||||
}
|
||||
Edge::Top | Edge::Bottom => {
|
||||
let y = if edge == Edge::Top {
|
||||
@@ -77,13 +116,17 @@ fn edge_has_visible_pixel(image: &RgbaImage, rect: Rect, edge: Edge) -> bool {
|
||||
} else {
|
||||
rect.bottom - 1
|
||||
};
|
||||
(rect.left..rect.right).any(|x| image.get_pixel(x, y).0[3] > 0)
|
||||
has_consecutive_visible_pixels(
|
||||
(rect.left..rect.right).map(|x| image.get_pixel(x, y).0[3]),
|
||||
required,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rect_has_visible_pixel(image: &RgbaImage, rect: Rect) -> bool {
|
||||
(rect.top..rect.bottom).any(|y| (rect.left..rect.right).any(|x| image.get_pixel(x, y).0[3] > 0))
|
||||
(rect.top..rect.bottom)
|
||||
.any(|y| (rect.left..rect.right).any(|x| pixel_is_visible(image.get_pixel(x, y).0[3])))
|
||||
}
|
||||
|
||||
fn edge_direction(image: &RgbaImage, rect: Rect, edge: Edge) -> EdgeDirection {
|
||||
@@ -366,12 +409,25 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_nonzero_alpha_antialias_pixels() {
|
||||
fn ignores_low_alpha_halo_while_preserving_visible_bounds() {
|
||||
let mut image = RgbaImage::from_pixel(16, 16, Rgba([0, 0, 0, 0]));
|
||||
image.put_pixel(5, 6, Rgba([255, 255, 255, 1]));
|
||||
image.put_pixel(7, 8, Rgba([255, 255, 255, 255]));
|
||||
let result = normalize_binding_area(&image, area(4, 5, 5, 5)).unwrap();
|
||||
assert_eq!(result.area, area(5, 6, 3, 3));
|
||||
for y in 6..10 {
|
||||
for x in 5..9 {
|
||||
image.put_pixel(x, y, Rgba([255, 255, 255, 255]));
|
||||
}
|
||||
}
|
||||
image.put_pixel(4, 7, Rgba([255, 255, 255, 1]));
|
||||
image.put_pixel(9, 8, Rgba([255, 255, 255, 8]));
|
||||
let result = normalize_binding_area(&image, area(4, 5, 6, 6)).unwrap();
|
||||
assert_eq!(result.area, area(5, 6, 4, 4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_isolated_visible_edge_pixel() {
|
||||
let mut image = image_with_rect(16, 16, 4, 4, 6, 8);
|
||||
image.put_pixel(6, 4, Rgba([255, 255, 255, 255]));
|
||||
let result = normalize_binding_area(&image, area(4, 4, 2, 4)).unwrap();
|
||||
assert_eq!(result.area, area(4, 4, 2, 4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
use super::area::MIN_VISIBLE_ALPHA;
|
||||
use base64::Engine as _;
|
||||
use image::{ImageFormat, Rgba, RgbaImage};
|
||||
use std::fs;
|
||||
use std::io::Cursor;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub(crate) const VISUAL_BINDING_TRANSPARENT_MARKER_RGBA: [u8; 4] = [255, 0, 255, 255];
|
||||
|
||||
pub(crate) async fn preprocess_for_visual_binding(
|
||||
processed_url: String,
|
||||
sidecar: PathBuf,
|
||||
) -> Result<String, String> {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
preprocess_for_visual_binding_blocking(&processed_url, &sidecar)
|
||||
})
|
||||
.await
|
||||
.map_err(|error| format!("视觉绑定预处理任务失败:{error}"))?
|
||||
}
|
||||
|
||||
fn preprocess_for_visual_binding_blocking(
|
||||
processed_url: &str,
|
||||
sidecar: &Path,
|
||||
) -> Result<String, String> {
|
||||
let encoded = processed_url
|
||||
.split_once(',')
|
||||
.map(|(_, data)| data)
|
||||
.ok_or_else(|| "处理图 data URL 无效".to_string())?;
|
||||
let bytes = base64::engine::general_purpose::STANDARD
|
||||
.decode(encoded.trim())
|
||||
.map_err(|error| format!("解析处理图失败:{error}"))?;
|
||||
let mut image = image::load_from_memory(&bytes)
|
||||
.map_err(|error| format!("解码处理图失败:{error}"))?
|
||||
.to_rgba8();
|
||||
for pixel in image.pixels_mut() {
|
||||
if pixel.0[3] < MIN_VISIBLE_ALPHA {
|
||||
*pixel = Rgba(VISUAL_BINDING_TRANSPARENT_MARKER_RGBA);
|
||||
} else {
|
||||
pixel.0[3] = 255;
|
||||
}
|
||||
}
|
||||
let mut png = Vec::new();
|
||||
image::DynamicImage::ImageRgba8(image)
|
||||
.write_to(&mut Cursor::new(&mut png), ImageFormat::Png)
|
||||
.map_err(|error| format!("编码视觉绑定预览失败:{error}"))?;
|
||||
let debug_name = format!("binding-{}.png", uuid::Uuid::new_v4().simple());
|
||||
fs::write(sidecar.join(&debug_name), &png)
|
||||
.map_err(|error| format!("写入视觉绑定预览失败:{error}"))?;
|
||||
Ok(format!(
|
||||
"data:image/png;base64,{}",
|
||||
base64::engine::general_purpose::STANDARD.encode(png)
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use image::Rgba;
|
||||
use tempfile::tempdir;
|
||||
|
||||
fn data_url(image: RgbaImage) -> String {
|
||||
let mut bytes = Vec::new();
|
||||
image::DynamicImage::ImageRgba8(image)
|
||||
.write_to(&mut Cursor::new(&mut bytes), ImageFormat::Png)
|
||||
.expect("encode fixture");
|
||||
format!(
|
||||
"data:image/png;base64,{}",
|
||||
base64::engine::general_purpose::STANDARD.encode(bytes)
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preprocesses_alpha_using_existing_visibility_threshold() {
|
||||
let mut image = RgbaImage::from_pixel(4, 1, Rgba([10, 20, 30, 255]));
|
||||
image.put_pixel(0, 0, Rgba([1, 2, 3, 0]));
|
||||
image.put_pixel(1, 0, Rgba([4, 5, 6, MIN_VISIBLE_ALPHA - 1]));
|
||||
image.put_pixel(2, 0, Rgba([7, 8, 9, MIN_VISIBLE_ALPHA]));
|
||||
image.put_pixel(3, 0, Rgba([11, 12, 13, 254]));
|
||||
let directory = tempdir().expect("create sidecar fixture");
|
||||
|
||||
let url = preprocess_for_visual_binding_blocking(&data_url(image), directory.path())
|
||||
.expect("preprocess fixture");
|
||||
let encoded = url.split_once(',').expect("data URL").1;
|
||||
let bytes = base64::engine::general_purpose::STANDARD
|
||||
.decode(encoded)
|
||||
.expect("decode output");
|
||||
let output = image::load_from_memory(&bytes)
|
||||
.expect("decode output png")
|
||||
.to_rgba8();
|
||||
|
||||
assert_eq!(
|
||||
output.get_pixel(0, 0).0,
|
||||
VISUAL_BINDING_TRANSPARENT_MARKER_RGBA
|
||||
);
|
||||
assert_eq!(
|
||||
output.get_pixel(1, 0).0,
|
||||
VISUAL_BINDING_TRANSPARENT_MARKER_RGBA
|
||||
);
|
||||
assert_eq!(output.get_pixel(2, 0).0, [7, 8, 9, 255]);
|
||||
assert_eq!(output.get_pixel(3, 0).0, [11, 12, 13, 255]);
|
||||
}
|
||||
}
|
||||
+11
-3
@@ -1,7 +1,12 @@
|
||||
use crate::ui_editor::commands::separation::image_preprocess::VISUAL_BINDING_TRANSPARENT_MARKER_RGBA;
|
||||
use crate::ui_editor::commands::separation::SeparationNode;
|
||||
|
||||
pub(crate) fn gen_binding_prompt(nodes: Vec<&SeparationNode>) -> String {
|
||||
let binding_system_prompt = r#"
|
||||
let [marker_red, marker_green, marker_blue, marker_alpha] =
|
||||
VISUAL_BINDING_TRANSPARENT_MARKER_RGBA;
|
||||
let marker_color = format!("rgba({marker_red}, {marker_green}, {marker_blue}, {marker_alpha})");
|
||||
let binding_system_prompt = format!(
|
||||
r#"
|
||||
You will be given a src UI design image and a processed image, where some ui elements are separated.
|
||||
You need to recognize and review the separation using the given tool.
|
||||
field notes:
|
||||
@@ -13,7 +18,10 @@ pub(crate) fn gen_binding_prompt(nodes: Vec<&SeparationNode>) -> String {
|
||||
|
||||
Here were the separation requirements:
|
||||
Preserve hard edges and the exact visible shape.
|
||||
The processed image is a transparent atlas containing the requested image layers.
|
||||
The processed image is an opaque visual-binding preview containing the requested image layers.
|
||||
The solid color {marker_color} is an intentional transparency marker added by this workflow before this request.
|
||||
It is not an image-edit defect and is not part of any UI element.
|
||||
Do not include this marker color in the extracted area.
|
||||
Do not use the source node rectangle as the extracted area.
|
||||
And you should also review if the extracted's successfully meet the src image:
|
||||
* shape
|
||||
@@ -27,7 +35,7 @@ pub(crate) fn gen_binding_prompt(nodes: Vec<&SeparationNode>) -> String {
|
||||
|
||||
these node need handle:
|
||||
"#
|
||||
.to_string();
|
||||
);
|
||||
let mut result = binding_system_prompt;
|
||||
result.reserve(512);
|
||||
for elem in nodes {
|
||||
|
||||
+25
-2
@@ -1,4 +1,5 @@
|
||||
use crate::config::{build_game_creator_llm_client_from_llm_config, load_game_creator_app_config};
|
||||
use crate::ui_editor::commands::separation::image_preprocess;
|
||||
use crate::ui_editor::commands::separation::prompt::gen_binding_prompt;
|
||||
use crate::ui_editor::commands::separation::{
|
||||
validate_binding_response, BindingResp, SeparationNode,
|
||||
@@ -10,15 +11,17 @@ use crate::ui_editor::commands::utils::{
|
||||
use platform_llm::{
|
||||
LlmFunctionTool, LlmMessage, LlmMessageContentPart, LlmRunRequest, LlmToolChoice,
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
use std::time::Instant;
|
||||
|
||||
pub(super) async fn visual_binding(
|
||||
source_url: String,
|
||||
processed_url: String,
|
||||
sidecar: PathBuf,
|
||||
nodes: &[&SeparationNode],
|
||||
) -> Result<BindingResp, String> {
|
||||
let started = Instant::now();
|
||||
let result = visual_binding_inner(source_url, processed_url, nodes).await;
|
||||
let result = visual_binding_inner(source_url, processed_url, sidecar, nodes).await;
|
||||
app_log!(
|
||||
"ui_separation.visual_binding.timing outcome={} elapsed_ms={} nodes={}",
|
||||
if result.is_ok() { "ok" } else { "error" },
|
||||
@@ -31,6 +34,7 @@ pub(super) async fn visual_binding(
|
||||
async fn visual_binding_inner(
|
||||
source_url: String,
|
||||
processed_url: String,
|
||||
sidecar: PathBuf,
|
||||
nodes: &[&SeparationNode],
|
||||
) -> Result<BindingResp, String> {
|
||||
app_log!(
|
||||
@@ -39,6 +43,25 @@ async fn visual_binding_inner(
|
||||
source_url.chars().count(),
|
||||
processed_url.chars().count()
|
||||
);
|
||||
let preprocess_started = Instant::now();
|
||||
let binding_processed_url =
|
||||
match image_preprocess::preprocess_for_visual_binding(processed_url, sidecar).await {
|
||||
Ok(value) => {
|
||||
app_log!(
|
||||
"ui_separation.visual_binding.preprocess.timing outcome=ok elapsed_ms={}",
|
||||
preprocess_started.elapsed().as_millis()
|
||||
);
|
||||
value
|
||||
}
|
||||
Err(error) => {
|
||||
app_log!(
|
||||
"ui_separation.visual_binding.preprocess.timing outcome=error elapsed_ms={}",
|
||||
preprocess_started.elapsed().as_millis()
|
||||
);
|
||||
app_log!("ui_separation.error stage=visual_binding_preprocess error={error}");
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
let llm_config = load_game_creator_app_config()
|
||||
.map_err(|e| {
|
||||
app_log!("ui_separation.error stage=visual_binding reason=load_config error={e}");
|
||||
@@ -67,7 +90,7 @@ async fn visual_binding_inner(
|
||||
text: "processed image:".to_string(),
|
||||
},
|
||||
LlmMessageContentPart::InputImage {
|
||||
image_url: processed_url.clone(),
|
||||
image_url: binding_processed_url,
|
||||
},
|
||||
LlmMessageContentPart::InputText {
|
||||
text: "src image:".to_string(),
|
||||
|
||||
@@ -44,14 +44,17 @@ Rust 在接收并校验工具结果后,才把这两个枚举变体映射为正
|
||||
## 图片编辑与视觉绑定
|
||||
|
||||
- image-edit 直接使用原始 UI design PNG,不再生成或发送绿色框、紫色填充等额外辅助输入图。提取 prompt 直接描述完整页面分层清单和当前 batch 状态。
|
||||
- 原因:部分视觉模型不会可靠读取 PNG alpha;image-edit 返回的处理图还可能出现只包含 `1..254`、缺少 `0` 和 `255` 的异常 alpha,导致 visual binding 无法稳定区分透明区域与素材内容。该问题只影响视觉模型的观察输入,不改变正式 cut 使用的 RGBA 真相。
|
||||
- 追加清单区分本轮 Image 输出目标、已完成 Image、仅作父子/遮挡上下文的 Image,以及只需从父图片移除的 Text。清单使用人类可读的编号、name/description、位置和层级,不向 image-edit 暴露 opaque NodeId。
|
||||
- 提取 prompt 先把 separation tree 投影为小型 YAML 视图,再注入固定规则文本:每个节点只包含展示编号、状态、`x/y/width/height` 矩形、描述、可选返工意见和递归 children;YAML 不携带 opaque NodeId,树的嵌套关系替代 `depth/role` 字段。
|
||||
- 由于 raw endpoint 每次只返回一张 PNG,prompt 要求 image-edit 输出透明 atlas:本轮图片层可以移动和缩放,放置在不会互相遮挡的位置;视觉模型返回每层在 processed 图中的实际区域。源节点矩形只用于语义定位,不用于裁切区域推断。
|
||||
- 请求尺寸始终使用源 UI design 尺寸;Raw GPT Image 2 API 保证返回相同尺寸,客户端不额外做尺寸拒绝检查。
|
||||
- 处理图解码/写入和 cut 裁切属于本地 CPU/文件操作,放入独立的 `spawn_blocking` 任务;image-edit 与 visual binding 网络请求仍运行在 async future 中。
|
||||
- 视觉 binding 输入源图与处理图,只接收当前 batch 的 Image targets,必须为每个 Image target 恰好返回一次 `Ok` 或 `NeedRework`。每个决定继续携带 `to_node: NodeId`;Text 不出现在请求或 schema 中。
|
||||
- visual binding 请求前新增本地预处理:复用 `MIN_VISIBLE_ALPHA`,将 alpha 小于该阈值的像素替换为不透明洋红标记色 `[255, 0, 255, 255]`,其余像素保留 RGB 并将 alpha 设为 `255`。预处理图只用于 visual binding,原始 processed RGBA 继续用于 cut;不新增质量门禁、alpha 统计判定或重试。
|
||||
- 视觉 binding 输入源图与预处理后的不透明处理图,只接收当前 batch 的 Image targets,必须为每个 Image target 恰好返回一次 `Ok` 或 `NeedRework`。每个决定继续携带 `to_node: NodeId`;Text 不出现在请求或 schema 中。
|
||||
- binding prompt 使用英文明确说明:洋红色是本工作流在请求前注入的透明区域标记,不是 image-edit 缺陷,也不是 UI 素材;模型不得将该颜色计入 extracted area。颜色文本由 `VISUAL_BINDING_TRANSPARENT_MARKER_RGBA` 常量生成,避免提示词与实现漂移。
|
||||
- `Ok` 返回 `NodeId + BindingArea`;Rust 仅校验 NodeId、区域边界和非零尺寸,不检查与原节点框的偏差,也不要求区域不重叠。
|
||||
- cut 前会对视觉模型返回的 `BindingArea` 做本地像素边界归一化。处理图是透明 PNG,有效像素定义为 `alpha > 0`。四条边以模型 area 为起点,每条边根据首次扫描结果固定方向:边上无有效像素则只向内收缩,边上有有效像素则只向外扩展;四边每轮从同一矩形快照同时逐像素推进,直到达到“内侧有像素、外侧无像素”的分界、图像边界或每条边相对原始 area 的位移上限。每条边最多相对原始 area 移动 `32px`,由模块级常量 `MAX_BINDING_AREA_EDGE_ADJUSTMENT_PX` 定义,与原始 area 尺寸无关。方向固定用于避免稀疏像素造成边界来回振荡;没有理想分界时使用受限范围内的最终 area,不重新请求视觉模型,也不转 problematic。全透明处理图不走特殊错误分支,仍沿同一规则得到最终 area 后裁切。归一化只影响本地 cut,不改写原始 `BindingDecision`、sidecar 或 DTO;日志记录原始 area、最终 area、是否变更,以及仍需移动时是否受到该常量上限、图像边界或非零尺寸约束。性能优化列 TODO。
|
||||
- cut 前会对视觉模型返回的 `BindingArea` 做本地像素边界归一化。处理图是透明 PNG,有效像素定义为 alpha 不低于模块级常量 `MIN_VISIBLE_ALPHA`(当前为 `16`);边缘扫描还要求至少连续 `MIN_CONSECUTIVE_VISIBLE_EDGE_PIXELS` 个有效像素(当前为 `2`),避免半透明光晕和孤立噪点驱动边界移动。四条边以模型 area 为起点,每条边根据首次扫描结果固定方向:边上无有效像素则只向内收缩,边上有有效像素则只向外扩展;四边每轮从同一矩形快照同时逐像素推进,直到达到“内侧有像素、外侧无像素”的分界、图像边界或每条边相对原始 area 的位移上限。每条边最多相对原始 area 移动 `32px`,由模块级常量 `MAX_BINDING_AREA_EDGE_ADJUSTMENT_PX` 定义,与原始 area 尺寸无关。方向固定用于避免稀疏像素造成边界来回振荡;没有理想分界时使用受限范围内的最终 area,不重新请求视觉模型,也不转 problematic。全透明处理图不走特殊错误分支,仍沿同一规则得到最终 area 后裁切。归一化只影响本地 cut,不改写原始 `BindingDecision`、sidecar 或 DTO;日志记录原始 area、最终 area、是否变更,以及仍需移动时是否受到该常量上限、图像边界或非零尺寸约束。性能优化列 TODO。
|
||||
- `NeedRework` 携带短问题描述(最多 512 个 Unicode 字符);通过校验后按产生顺序追加到目标 `SeparationNode.note.rework_notes`,下一次该节点进入 image-edit 时全部意见会注入提取 prompt。结构化工具调用失败时使用可复用 repair harness,把错误反馈给模型并额外请求一次;image-edit 不使用该 harness。
|
||||
- 达到返工上限时仍先保留最后一条视觉模型意见,再把节点追加到 problematic;网络、IO、裁切等基础设施错误不写入节点意见。
|
||||
- 达到模块级重做常量后,节点移入 problematic;不中断整条工作流,最终统一通知用户。image-edit、图像写入或裁切失败保留当前 state 并返回错误,不自动把整批标记为 problematic。
|
||||
@@ -67,7 +70,7 @@ Rust 在接收并校验工具结果后,才把这两个枚举变体映射为正
|
||||
- `SeparationNode.kind` 与 tree children 一起持久化;当前数据结构变更提升 separation state schema 版本,不提供旧 sidecar 迁移或回退。
|
||||
- sidecar 只在 separation 未完成期间存在;完成后删除 state JSON。
|
||||
- 当前只持久化已经完成的 batch;正在执行 batch 的恢复语义列 TODO。
|
||||
- 临时图片可跨重启保留。image-edit 返回的 processed 图和 cut 图片当前都保留用于 debug;理论上 processed 中间图只应在内存中,清理/归档策略列 TODO。
|
||||
- 临时图片可跨重启保留。image-edit 返回的 processed 图、visual binding 预处理图和 cut 图片当前都保留用于 debug;预处理图位于同一 sidecar,命名为 `binding-<随机 UUID>.png`,不写入 separation state。清理/归档策略列 TODO。
|
||||
- 并发边界:当前由前端 `isSeparating` 与 `runWithStateLocked` 保证同一 UI 编辑会话
|
||||
同时只有一次 separation。sidecar 是临时恢复状态,不是正式 UI 资产真相,不参与
|
||||
manifest 或项目 revision,因此当前不额外持有项目写锁;若未来支持多窗口/多进程并发,
|
||||
@@ -106,7 +109,8 @@ Rust 在接收并校验工具结果后,才把这两个枚举变体映射为正
|
||||
## 实现组织
|
||||
|
||||
- separation prompt 按职责拆分为 `prompt/extract.rs` 与 `prompt/binding.rs`,由 `prompt/mod.rs` 统一导出。
|
||||
- separation workflow 按执行边界拆分为 `workflow/image_edit.rs`(Raw image-edit 与处理图写入)、`workflow/binding.rs`(视觉绑定)、`workflow/cut.rs`(像素归一化与裁切)、`workflow/patch.rs`(批次状态 patch);`workflow/mod.rs` 仅负责批次编排与 sidecar 检查点。
|
||||
- separation workflow 按执行边界拆分为 `workflow/image_edit.rs`(Raw image-edit 与处理图写入)、`workflow/binding.rs`(视觉绑定)、`workflow/cut.rs`(像素归一化与裁切)、`workflow/patch.rs`(批次状态 patch);新增 `image_preprocess.rs`(visual binding 请求前的 RGBA 标记图转换、PNG 写入和 data URL 生成);`workflow/mod.rs` 仅负责批次编排与 sidecar 检查点。
|
||||
- `image_preprocess.rs` 的像素转换和 debug 文件写入运行在独立 `spawn_blocking` 任务;`visual_binding` 记录预处理阶段的 `outcome` 与 `elapsed_ms`,不记录图片内容或绝对路径。预处理单元测试只验证像素转换和 PNG 可解码,不把 debug 文件是否存在作为测试契约。
|
||||
- 上述拆分只调整 Rust 模块边界,不改变批次选择、重试、sidecar 持久化、绑定校验或错误恢复语义。
|
||||
|
||||
## TODO
|
||||
|
||||
Reference in New Issue
Block a user