diff --git a/docs/11-ai-outfit.md b/docs/11-ai-outfit.md index eb77daa..ff5051d 100644 --- a/docs/11-ai-outfit.md +++ b/docs/11-ai-outfit.md @@ -464,6 +464,7 @@ Excel 行 → `OutfitTask` 列表的转换由 `excel_service` 完成;核心只 - 一次 POST → `extract_titles_from_response` 返回**多条**清洗后标题。 - `generate_text(prompt, image_path=None) -> str`:保留(返回第一条,= `generate_texts` 的 `[0]`),供单条场景与既有单测;与 `generate_texts` 共用同一段 POST。 - `extract_titles_from_response(data) -> List[str]`:取 `choices[0].message.content` / gemini `candidates[0].content.parts[].text` 的原始文本,**按逗号(`,`/`,`,兼容换行)拆分 + 逐段清洗**(去首尾空白/序号/符号/引号、丢空),返回标题列表(§19.23)。`extract_text_from_response` = 取其首条(兼容保留)。 +- **读取超时:标题生成走 4K 的 600 秒(§17.8)**。`generate_texts`/`generate_text` 的 `resolution` 形参本是图像分辨率借来的超时档(`resolution_timeout`:512→180 / 1K→240 / 2K→360 / 4K→600 秒);纯文本请求没有"分辨率",只借那张超时表。`generate_titles` 显式传 `resolution="4K"`(`ai_title._TITLE_TIMEOUT_RESOLUTION`)→ 读取超时 **600 秒**,而非默认档 1K 的 240 秒。理由:一次请求要中转站排队、大模型生成多条标题再整段返回,240 秒对慢模型/排队偏紧。连接超时仍 30 秒不变;模型条目若显式设 `timeout_seconds>0` 仍**优先覆盖**这 600 秒(`_post`:`config.timeout_seconds if >0 else resolution_timeout(resolution)`)。 ### 17.3 模型与提示词 @@ -514,3 +515,12 @@ Excel 行 → `OutfitTask` 列表的转换由 `excel_service` 完成;核心只 - **条数对不上不强求**:`N>行数` 多的丢、`N<行数` 后面行留空 + 日志,不报错中止(用户可改提示词数量重跑)。 - **代价**:标题不再与具体某件衣服一一对应(无图);若将来要"每件看图各出标题",那是另一种模式,按 §17.1 旧版思路另做。 - 文本服务保留 `generate_text`(单条)+ 新增 `generate_texts`(多条),共用同一段 POST;标题流程走 `generate_texts(prompt, image_path=None)`。 + +### 17.8 决策:标题生成读取超时用 4K 档(600 秒) + +标题生成默认读取超时从 **1K 的 240 秒改为 4K 的 600 秒**(§19.30)。 + +- **背景**:标题是"一次请求、纯提示词、生成多条"(§17.7)。这一次请求要经中转站转发、大模型排队、生成 N 条标题后**整段**返回;比"一张图"耗时更不可控(推理/排队型模型尤甚)。原先借用 1K 档 240 秒,偏紧,慢模型会在拿到结果前就 `ReadTimeout`。 +- **改法**:`generate_titles` 显式传 `resolution="4K"`(常量 `ai_title._TITLE_TIMEOUT_RESOLUTION`),把读取超时抬到 600 秒。**只改标题这一条链路**——`generate_texts`/`generate_text` 的默认 `resolution="1K"` 不动,其它/未来调用方不受影响。 +- **为什么复用分辨率档而非新增字段**:纯文本请求本无分辨率,`resolution` 只是 `resolution_timeout` 那张 `{512/1K/2K/4K → 180/240/360/600}` 超时表的键;复用它零新增配置、与图片侧同一套超时语义,最省。若将来要独立可调,再在模型条目上用 `timeout_seconds>0` 覆盖(已支持,优先级高于分辨率档)。 +- **代价**:慢/挂死的请求现在最长等 600 秒才失败(而非 240 秒),但标题生成是单次、手动触发、非批量循环,多等的是最坏情况,正常返回不受影响。 diff --git a/src/core/ai_title.py b/src/core/ai_title.py index cd7b8a6..13de2fa 100644 --- a/src/core/ai_title.py +++ b/src/core/ai_title.py @@ -9,6 +9,13 @@ from services.ai_text_service import AiTextClient logger = logging.getLogger(__name__) +# 标题是"一次请求、纯提示词、生成多条"(docs/11 §17.7):要经中转站转发、大模型 +# 排队、生成 N 条再整段返回,比一张图更耗时。纯文本请求本无"分辨率",这里只借 +# resolution_timeout 那张 {512/1K/2K/4K → 180/240/360/600 秒} 超时表的键,用 4K 档把 +# 读取超时抬到 600 秒(默认 1K 的 240 秒偏紧,慢/排队型模型会先 ReadTimeout)。见 +# docs/11 §17.8。模型条目若显式设 timeout_seconds>0 仍优先覆盖(ai_text_service._post)。 +_TITLE_TIMEOUT_RESOLUTION = "4K" + def generate_titles(prompt, model_config, api_client=None): """Generate a list of titles in ONE prompt-only request (docs/11 §17.1). @@ -18,6 +25,7 @@ def generate_titles(prompt, model_config, api_client=None): (AiTextClient raises in that case, so a normal return is always non-empty). """ client = api_client or AiTextClient(model_config) - titles = client.generate_texts(prompt) # image_path=None -> text-only + # image_path=None -> 纯文本;resolution="4K" 只为借 600s 读取超时(§17.8) + titles = client.generate_texts(prompt, resolution=_TITLE_TIMEOUT_RESOLUTION) logger.info("Generated %d title(s) in one request", len(titles)) return titles diff --git a/tasks.md b/tasks.md index fac7c6f..167b521 100644 --- a/tasks.md +++ b/tasks.md @@ -1542,87 +1542,24 @@ - [x] 测试:标题模型不出现在图片 AI 模型下拉;图片模型仍显示;标题生成仍能按 `title_model` 找到同名模型;只有标题模型时下拉为空/占位 - [x] 验证:`py_compile`、`test_ai_outfit_panel.py`、全套 `python -m unittest discover -s tests`、离屏启动 AI 穿搭页通过 -### 19.30 独立脚本:合并图匹配印花清单 — `match_stamp.py` - -背景: - -需要一个独立脚本(不引用现有项目代码)按“方案2”生成匹配清单:例如合并图 `output\20260623_094529\TY037\1_TY037.png`,从文件名后缀提取 `TY037`,在印花文件夹 `D:\chengma\印花和底图\已处理印花\卡通71(66大码200斤 KEKE已上)\横1` 中匹配 `TY037.png`,输出 CSV 清单。 - -任务: - -- [x] 新增 `match_stamp.py`,只使用 Python 标准库,不导入 `src/` 或项目服务代码 -- [x] 脚本顶部提供 `OUTPUT_DIR` / `STAMP_DIR` / `REPORT_PATH`,默认生成 `match_stamp_report.csv` -- [x] 递归扫描合并图和印花图,按合并图文件名最后一个 `_` 后的编码匹配印花文件 stem -- [x] CSV 字段:`status` / `code` / `merged_image` / `stamp_image` / `merged_folder` / `message`,支持 `matched` / `missing` / `duplicate` / `bad_name` -- [x] 验证:`python -m py_compile match_stamp.py`;用示例目录生成系统临时 CSV,匹配 96 张、缺失 0、重复 0、坏名 0 - -### 19.31 合并图按图片内容匹配印花 — 需求与方案文档 - -背景: - -§19.30 的 `match_stamp.py` 是按文件名后缀匹配,例如 `1_TY037.png -> TY037.png`。用户澄清真实需求是:给定一张衣服图片(可理解为合并后的图片)和一个印花目录,通过类似 OpenCV template matching 的图像内容匹配,找出衣服图中印花与目录内哪张印花最相似;先不保存 CSV,直接 `print` 结果。 - -任务: - -- [x] 新增 `docs/12-stamp-template-matching.md`,单独记录需求、输入输出、方案比较、推荐方案和依赖 -- [x] 明确推荐第一版:多尺度 `matchTemplate` + alpha mask + 边缘辅助分数,直接打印 Top N -- [x] 明确第三方库:最小 `opencv-python` + `numpy`,建议用 `Pillow` 处理 Windows 中文路径 -- [x] 明确约束:独立脚本、不引用项目代码、不修改图片、第一版不写 CSV/Excel、不接入 GUI -- [x] 验证:文档格式检查通过 - -### 19.32 独立脚本:按图片内容匹配合并图印花 — `match_stamp.py` +### 19.30 标题生成读取超时改用 4K 档(240s→600s) — docs/11 §17.2 / §17.8 前置阅读: -- `docs/12-stamp-template-matching.md` -- `match_stamp.py` +- `docs/11-ai-outfit.md`(§17.2 文本服务、§17.8 决策) +- `src/core/ai_title.py`(`generate_titles` 调 `generate_texts`) +- `src/services/ai_text_service.py`(`generate_texts`/`generate_text` 的 `resolution` 形参、`_post` 的 `read_timeout` 取值) +- `src/services/ai_image_service.py`(`RESOLUTION_TIMEOUTS` / `resolution_timeout`) +- `tests/test_ai_title.py`、`tests/test_ai_text_service.py` 背景: -§19.31 已明确真实需求不是按 `1_TY037.png -> TY037.png` 的文件名规则匹配,而是给定一张衣服 / 合并图和一个印花目录,通过图像内容匹配找出最相似的印花。第一版继续保持为独立脚本,不接入 GUI,不写 CSV,直接打印结果。 +标题是「一次请求、纯提示词、生成多条」(§17.7)。这次请求要经中转站转发 + 大模型排队 + 生成多条再整段返回,比一张图更耗时。原借 1K 档 240 秒读取超时,慢/排队型模型常在返回前 `ReadTimeout`。改为借 4K 档 600 秒(`resolution_timeout`:512/1K/2K/4K → 180/240/360/600 秒),只改标题链路,不动 `generate_texts` 默认 `1K`。 任务: -- [x] 将 `match_stamp.py` 从“递归扫描 output 并按文件名写 CSV”改为“单张合并图 + 印花目录内容匹配” -- [x] 使用 `Pillow` 读取图片以兼容 Windows 中文路径,再转为 `numpy` / OpenCV 数组 -- [x] 遍历印花目录内 PNG/JPG/JPEG/WEBP 图片,对透明 PNG 使用 alpha mask 并裁剪透明边界 -- [x] 使用多尺度模板匹配,主分数使用 RGB 彩色 `matchTemplate`,边缘图分数作为辅助 -- [x] 输出最佳匹配和 Top N,包含综合分、模板分、边缘分、坐标、缩放比例和匹配尺寸 -- [x] 找不到高可信结果时打印低分提示;不保存 CSV,不修改任何图片 -- [x] 支持命令行覆盖 `--image`、`--stamp-dir`、`--top` -- [x] 验证:`python -m py_compile match_stamp.py` 通过;临时构造一张合并图和两个候选印花,Top 1 命中正确印花 - -### 19.33 合并图印花匹配:宽高不等比例变形方案文档 - -背景: - -实际合成衣服和印花时,印花可能不按原始宽高比缩放。例如原始印花 `1000x1000`,合成到衣服上时被设置为 `1000x800`。这种非等比例变形不是单一 `scale` 能覆盖的,当前脚本只做等比例多尺度搜索时匹配稳定性会下降。 - -任务: - -- [x] 更新 `docs/12-stamp-template-matching.md`,明确 `matchTemplate` 对宽高不等比例变形敏感 -- [x] 将推荐方案扩展为“多尺度模板匹配 + 有限宽高比变形 + alpha mask + 边缘辅助分数” -- [x] 明确不做完整 `scale_x × scale_y` 暴力搜索,优先用基础 `scale` 加少量 `aspect_y` 候选控制耗时 -- [x] 明确 `1000x1000 -> 1000x800` 可由 `scale_x=1.00`、`scale_y=0.80`、`aspect_y=0.80` 覆盖 -- [x] 更新输出字段建议:从单一 `scale` 改为 `scale_x`、`scale_y`、`aspect_y` -- [x] 验证:文档路径、标题、示例和验收点检查通过 - -### 19.34 独立脚本:支持宽高不等比例印花匹配 — `match_stamp.py` - -前置阅读: - -- `docs/12-stamp-template-matching.md` -- `match_stamp.py` - -背景: - -§19.33 已明确实际合成时印花可能被非等比例缩放,例如原始 `1000x1000` 合成后变成 `1000x800`。`match_stamp.py` 需要按文档把单一 `scale` 搜索扩展为基础 `scale` 加有限 `aspect_y` 候选,提升这类场景的匹配稳定性。 - -任务: - -- [x] 新增 `ASPECT_Y_FACTORS = 0.60 / 0.70 / 0.80 / 0.90 / 1.00 / 1.10 / 1.20` -- [x] 将缩放搜索从单一 `scale` 改为 `scale_x=scale`、`scale_y=scale*aspect_y` -- [x] RGB 模板、边缘模板和 alpha mask 使用同一组 `scale_x / scale_y` 缩放 -- [x] 最佳结果保存并打印 `scale_x`、`scale_y`、`aspect_y`,不再只打印单一 `scale` -- [x] 保持独立脚本约束:不引用项目代码、不写 CSV、不修改图片 -- [x] 验证:`python -m py_compile match_stamp.py` 通过;临时构造 `100x100 -> 100x80` 非等比例样本,Top 1 命中正确印花且输出 `aspect_y=0.8000` +- [x] 文档更新:`docs/11-ai-outfit.md` §17.2 补读取超时说明、新增 §17.8 决策 +- [x] `ai_title.py`:加常量 `_TITLE_TIMEOUT_RESOLUTION = "4K"`(带注释说明借 4K 档的 600s);`generate_titles` 改调 `client.generate_texts(prompt, resolution=_TITLE_TIMEOUT_RESOLUTION)` +- [x] 不动 `generate_texts`/`generate_text` 默认 `resolution="1K"`;模型条目 `timeout_seconds>0` 仍优先覆盖(`_post` 逻辑不变) +- [x] 测试:`tests/test_ai_title.py` 断言 `generate_titles` 以 `resolution="4K"` 调 `generate_texts`(mock 客户端捕获 kwargs);既有用例保持绿 +- [x] 验证:`test_ai_title.py`、`test_ai_text_service.py`、全套 py37 通过(`test_config_service` 的 packaging 模板失败属并行历史遗留,无关) diff --git a/tests/test_ai_title.py b/tests/test_ai_title.py index cd95ede..d6dd82e 100644 --- a/tests/test_ai_title.py +++ b/tests/test_ai_title.py @@ -17,6 +17,15 @@ class TestAiTitleCore(unittest.TestCase): self.assertEqual(client.calls, 1) # 只请求一次 self.assertEqual(client.image_paths, [None]) # 纯文本,不传图 + def test_generate_titles_uses_4k_read_timeout(self): + # §19.30/§17.8:标题生成借 4K 档(600s 读取超时),不用默认 1K 的 240s + from core.ai_title import _TITLE_TIMEOUT_RESOLUTION, generate_titles + + self.assertEqual(_TITLE_TIMEOUT_RESOLUTION, "4K") + client = _RecordingTextClient(["a", "b"]) + generate_titles("生成 2 条标题", model_config={}, api_client=client) + self.assertEqual(client.resolutions, ["4K"]) + def test_generate_titles_propagates_client_error(self): from core.ai_title import generate_titles @@ -29,10 +38,12 @@ class _RecordingTextClient: self._titles = titles self.calls = 0 self.image_paths = [] + self.resolutions = [] def generate_texts(self, prompt, image_path=None, resolution="1K"): self.calls += 1 self.image_paths.append(image_path) + self.resolutions.append(resolution) return list(self._titles)