Rework layout engine around exact-glyph collision, add tests and docs sync
Replace the old bbox/heuristic placement (scale search rounds, large-font capping, stratified sampling, fill-retry ladders) with an area-model font sizing pass feeding a C++ exact-glyph collision engine (centroid-biased spiral + random probing, HD clearance refinement, density/hole optimization). Simplify the frontend advanced-params panel and JobParams type to match the surviving config surface, add a layout-constraints test suite and a repeatable benchmark tool, and bring docs/*.md back in sync with current code (plus new TESTING.md and DEPLOYMENT.md). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+113
-70
@@ -4,132 +4,175 @@
|
||||
|
||||
## 总流程
|
||||
|
||||
1. 读取 Excel 名单:`pipeline.main()`
|
||||
2. 计算自动画幅:`mask.calculate_dynamic_dimensions()`
|
||||
3. 生成并归一化掩膜:`mask.prepare_mask()`
|
||||
4. 计算权重:`weights.extract_weights_from_df()` 和 `weights.get_stroke_complexity_batch()`
|
||||
5. 估算字号范围:`weights.calculate_font_by_area_model()`
|
||||
6. 小画布布局:`layout.OptimizedEfficientWordCloud.generate_from_frequencies()`
|
||||
7. C++ 找可放位置:`IntegralGrid.query_direct()`
|
||||
8. C++ 写入字形占用:`IntegralGrid.stamp_and_rebuild()`
|
||||
9. 计算填充率并必要时重试放大
|
||||
10. 将小画布 layout 放大到高清画布并输出 PNG/SVG/DB/metrics
|
||||
```
|
||||
1. 读取 Excel 名单 → pipeline.main()
|
||||
2. 智能画幅计算 → mask.calculate_dynamic_dimensions()
|
||||
3. 生成并归一化掩膜 → mask.prepare_mask()
|
||||
4. 计算权重 → weights.extract_weights_from_df() + weights.get_stroke_complexity_batch()
|
||||
5. 估算字号范围 → weights.calculate_font_by_area_model()
|
||||
6. 小画布布局 → layout.OptimizedEfficientWordCloud.generate_from_frequencies()
|
||||
7. C++ 按真实字形找位置并原子写入 → IntegralGrid.place_glyph_exact()
|
||||
8. 整批未完整放入 → 统一缩放字号或扩大画布后重排
|
||||
9. 密度优化 → 探测更大字号并保留完整率不下降的候选
|
||||
10. 高清精修 → render.refine_layout_with_hd_clearance() 加入隔离带局部微调
|
||||
11. 输出 PNG / SVG / DB / metrics
|
||||
```
|
||||
|
||||
## 名单和重复填充
|
||||
## 名单与重复填充
|
||||
|
||||
`N_REPETITIONS` 决定目标词数:
|
||||
|
||||
```text
|
||||
```
|
||||
total_target = len(names) * N_REPETITIONS
|
||||
```
|
||||
|
||||
布局序列由 `_build_layout_sequence()` 生成。当前行为是按原名单循环追加:
|
||||
布局序列由 `_build_layout_sequence()` 生成。行为:**按原名单循环追加**,不是把同一个名字所有副本先放完。
|
||||
|
||||
```text
|
||||
```
|
||||
[A, B, C], N_REPETITIONS=4
|
||||
=> [A, B, C, A, B, C, A, B, C, A, B, C]
|
||||
→ [A, B, C, A, B, C, A, B, C, A, B, C]
|
||||
```
|
||||
|
||||
所以当前队列顺序是“先填一轮名单,再填下一轮”,不是先放完同一个名字所有副本。
|
||||
|
||||
需要注意:队列顺序公平不等于最终字号完全一致。放置阶段如果某个词以目标字号找不到位置,会单独降字号继续尝试。因此后几轮词语通常比前几轮小。
|
||||
同名副本在同一批布局中使用相同目标字号。放置失败不会触发逐词缩字号。
|
||||
|
||||
## 权重逻辑
|
||||
|
||||
### Excel 权重
|
||||
|
||||
`WEIGHT_COL_NAME` 优先于 `WEIGHT_COL_INDEX`。有效权重必须是可转数字且大于 0。
|
||||
|
||||
`REMOVE_DUPLICATES = True` 时,同名权重取最大值。`REMOVE_DUPLICATES = False` 时,仍会按名字聚合权重映射,所以同名不同权重不会保留为不同权重实例。
|
||||
- `WEIGHT_COL_NAME` 优先于 `WEIGHT_COL_INDEX`
|
||||
- 有效权重必须是可转数字且大于 `0`
|
||||
- `REMOVE_DUPLICATES=True` 时,同名权重取最大值
|
||||
- `REMOVE_DUPLICATES=False` 时,仍按名字聚合成权重映射
|
||||
|
||||
### 笔画权重
|
||||
|
||||
`ENABLE_STROKE_WEIGHTS = True` 时,系统渲染每个字符到 64x64 灰度图,用像素占用量估算复杂度。一个名字的笔画权重取其中最复杂字符的值。
|
||||
|
||||
`ENABLE_STROKE_WEIGHTS = False` 时跳过笔画权重。若没有 Excel 权重,所有名字权重默认为 `10`。
|
||||
- `ENABLE_STROKE_WEIGHTS=True` 时,系统渲染每个字符到 `64×64` 灰度图,用像素占用量估算复杂度
|
||||
- 一个名字的笔画权重取其中最复杂字符的值
|
||||
- 若同时存在 Excel 权重:Excel 值作为基础权重,笔画复杂度除以全体中位数后作为乘数
|
||||
- 这样手动权重比例仍保留,且 Excel 权重全为 `1` 时笔画开关也不会失效
|
||||
- `ENABLE_STROKE_WEIGHTS=False` 且无 Excel 权重时,所有名字权重默认为 `10`
|
||||
|
||||
## 字号范围估算
|
||||
|
||||
`calculate_font_by_area_model()` 使用可填充面积、目标填充率、packing efficiency、重复次数和名字长度估算 `min_font` / `max_font`。
|
||||
`calculate_font_by_area_model()` 使用以下输入估算 `min_font` / `max_font`:
|
||||
|
||||
- 可填充面积(掩膜中 `0` 的像素数)
|
||||
- 目标填充率 `TARGET_FILL_RATIO`
|
||||
- 打包效率 `PACKING_EFFICIENCY`
|
||||
- 重复次数 `N_REPETITIONS`
|
||||
- 名字长度、各名字权重
|
||||
|
||||
公式思想:
|
||||
|
||||
- 可填区域越大,字号越大
|
||||
- 名字越多、重复次数越高,字号越小
|
||||
- 字符越多,总占用质量越高,字号越小
|
||||
- 字符越多,总字符质量越高,字号越小
|
||||
- 权重越高,在 `log1p(weight)` 归一化后获得更高面积质量
|
||||
|
||||
最终 `max_font` 基于 `min_font * SIZE_RATIO` 计算。
|
||||
最终 `max_font = min_font × SIZE_RATIO`。
|
||||
|
||||
## 字号打分
|
||||
|
||||
当前 `build_log_rank_scores(..., per_word=True)` 会按姓名权重计算固定分数,然后映射到展开后的重复序列。
|
||||
`build_log_rank_scores(..., per_word=True)` 按姓名权重计算固定分数,映射到展开后的重复序列。结果是:
|
||||
|
||||
这意味着:
|
||||
|
||||
- 同名副本的目标分数相同
|
||||
- 同名副本的初始目标字号相同
|
||||
- 同名副本目标分数相同
|
||||
- 同名副本初始目标字号相同
|
||||
- 权重相同时,所有姓名初始目标字号相同
|
||||
|
||||
但最终放置字号仍可能变小,因为放置失败时会逐词降字号。
|
||||
`SIZE_RATIO=1` 时,`min_font == max_font`,所有同权重姓名在重试前后都保持完全相同字号。
|
||||
|
||||
## 等字号模式
|
||||
|
||||
`SIZE_RATIO=1` 时,面积估算阶段直接令 `min_font = max_font`。所有字号重试也只改变单一字号值。
|
||||
|
||||
等字号批次的特殊优化:
|
||||
|
||||
1. 视作整批同字号布局,使用整批字号打分(`per_word=False`)
|
||||
2. 密度优化阶段在完整候选中比较字符级最大空洞(`largest_empty_square_size`),并尝试不同布局种子降低空洞
|
||||
3. 高清精修阶段允许优先级回溯(把失败词提前到队列最前)
|
||||
|
||||
## 放置策略
|
||||
|
||||
每个词的放置流程:
|
||||
|
||||
1. 根据目标分数得到目标字号
|
||||
2. 随机决定横排或竖排
|
||||
3. 用 PIL 测量文字包围盒
|
||||
4. 调用 C++ `query_direct(query_h, query_w, seed)` 找位置
|
||||
5. 如果找不到,字号减 2 后重试,最低到目标字号的 40% 或 `min_font_size`
|
||||
6. 放置成功后,取真实字形 bitmap 并调用 `stamp_and_rebuild()`
|
||||
7. 主循环放不下的词进入 gap filling,用更小字号再尝试一次
|
||||
1. 根据目标分数得到目标字号(线性插值于 `min_font` 到 `max_font`)
|
||||
2. 随机决定横排或竖排(`prefer_horizontal` 控制概率)
|
||||
3. 用 PIL 渲染真实字形 bitmap,施加单侧安全边距(margin)生成碰撞 mask
|
||||
4. 调用 C++ `place_glyph_exact()` 搜索合法位置并原子写入
|
||||
5. 当前方向找不到时,只尝试同字号的另一方向
|
||||
6. 任意姓名失败即视为本次整批布局不完整
|
||||
7. **不逐词缩字号**。Python 统一缩放整批字号范围后重新布局
|
||||
8. 触及字号下限仍失败时,按 `CANVAS_RETRY_GROWTH` 扩大画布并整批重排
|
||||
9. 将完整布局映射到高清字号和坐标
|
||||
10. 高清画布进行逐词隔离带精修,局部无合法位置时扩大画布并整批重排
|
||||
|
||||
## C++ 积分图搜索
|
||||
## C++ 真实字形搜索
|
||||
|
||||
C++ `IntegralGrid` 维护两个核心结构:
|
||||
`IntegralGrid` 维护两个核心结构:
|
||||
|
||||
- `canvas`:真实占用像素,`1` 表示已占用或掩膜阻挡
|
||||
- `data`:`canvas` 的积分图,用于 O(1) 判断矩形区域是否为空
|
||||
- `canvas`:真实占用像素(`1` 表示已占用或掩膜阻挡)
|
||||
- `data`:兼容旧矩形查询的积分图;当前主路径不依赖逐词重建
|
||||
|
||||
`query_direct()` 的行为:
|
||||
`place_glyph_exact()` 行为:
|
||||
|
||||
1. 如果画布完全空,随机返回一个位置
|
||||
2. 先随机探测最多 16 个位置
|
||||
3. 如果未命中,扫描所有可能位置
|
||||
4. 对每个候选位置用积分图判断包围盒是否为空
|
||||
5. 从所有可放位置中随机选一个
|
||||
1. 等字号批次前 `70%` 姓名优先选择靠近可填区域质心的合法位置,避免少量姓名接受第一个随机空位而形成大块空洞
|
||||
2. 多字号批次只对前 `25%` 以及高权重(`score ≥ 0.80`)姓名启用中心偏好
|
||||
3. 少于 `100` 人的等字号批次中心候选数提高到 `768`;`100–300` 人提高到 `512`;其余保持 `256`
|
||||
4. 随机探测失败后,从种子决定的偏移开始完整扫描
|
||||
5. 对每个候选位置逐像素比较碰撞 mask 与 C++ `canvas`
|
||||
6. 命中后只写入真实字形,占位查询和写入在同一次 C++ 调用中完成
|
||||
|
||||
`stamp_and_rebuild()` 的行为:
|
||||
真实字形搜索允许透明角落和笔画间空隙安全交错,比外接矩形碰撞更密。安全边距只参与候选检查,不会被双侧累计放大。
|
||||
|
||||
1. 把真实字形像素写入 C++ `canvas`
|
||||
2. 从字形左上角开始局部重建积分图
|
||||
## 高清精修(隔离带)
|
||||
|
||||
当前碰撞检测是“矩形找位置 + 字形像素落图”。找位置阶段要求文字包围盒矩形完全空;实际占用阶段只写入字形像素。
|
||||
正式流水线不在低清工作网格增加边距,因为 `1` 个工作像素会被放大成约 `5–6` 个高清像素并显著损失容量。
|
||||
|
||||
隔离带优先作用在最终高清画布,宽度为 `1px`,不是用户参数。精修流程:
|
||||
|
||||
1. 小画布完整布局放大到高清
|
||||
2. 逐词在高清画布上验证,加入 `1px` 隔离带
|
||||
3. 碰撞时只允许最大 `max_shift=24px` 的局部微位移
|
||||
4. 精修失败时,最多进行 `1` 次确定性整批优先级回溯(把失败词移到队列最前)
|
||||
5. 若轮廓过窄无法容纳额外隔离带,降为精确零间隙碰撞(`clearance=0`)
|
||||
6. 全部候选无解时,由外层扩大画布后整批重新布局,不进行远距离单词搬移
|
||||
|
||||
密度搜索会保留各档完整整批候选。最高密度候选若无法在有限位置修正范围内通过高清隔离验收,则改用上一档完整整批候选;不会对碰撞姓名单独缩字号,也不会接受带重叠的高密度结果。
|
||||
|
||||
## 填充率重试
|
||||
|
||||
一次布局完成后,`compute_fill_ratio_fast()` 重新渲染 layout 并计算填充率。
|
||||
|
||||
如果填充率低于 `MIN_ACCEPT_FILL_RATIO`,管线会尝试二分放大 `size_scale`,并可通过 `FILL_RETRY_RELAX_LARGE_CAP` 放宽大字号限制。
|
||||
- 面积模型首次完整放入但明显低于 `TARGET_FILL_RATIO` 时,只允许一次整批等比例增字号尝试。新布局必须仍然完整且真实填充率更高才会采用
|
||||
- 整批未完整放入时,流水线不会输出半成品:先整批等比例调整字号;触及最小字号仍失败时按 `CANVAS_RETRY_GROWTH` 扩大画布并重新生成
|
||||
|
||||
如果仍无法达到目标,会保留填充率最好的 layout。
|
||||
## 字号硬约束
|
||||
|
||||
## 已知算法限制
|
||||
- 不存在逐词缩字号、gap filling 或大字号自动压缩路径
|
||||
- `USER_MIN_FONT_SIZE` 和 `USER_MAX_FONT_SIZE` 是不可越过的边界;冲突时直接报错
|
||||
- `SIZE_RATIO=1` 在面积估算、整批重试和高清放大阶段始终保持单一字号
|
||||
- 完整名单不可关闭;放不下时只能整批重排、整批等比例调整或扩大画布
|
||||
|
||||
- 当前没有真正的“整轮统一降字号”机制。重复填充虽然按轮展开,但每个词可以独立降字号。
|
||||
- `LIMIT_LARGE_FONTS` 是全局计数,不区分姓名和轮次。
|
||||
- `LAYOUT_ORDER_MODE_INTERLEAVED_RANDOM` 只改变展开序列的顺序,不改变 C++ 的空间采样策略。
|
||||
- `ENABLE_STRATIFIED_SAMPLING` 调用的 `reorder_stratified()` 对 `query_direct()` 主路径无效。
|
||||
- C++ `batch_query()` 会用矩形 `update_rect_add()` 更新,不走真实字形 `stamp_and_rebuild()`;当前 Python 主路径没有使用它。
|
||||
## 空洞优化(等字号模式)
|
||||
|
||||
## 后续公平重复填充建议
|
||||
等字号模式下,字符级最大空洞(`largest_empty_square_size`)用于判断是否存在"可放字符级空洞":
|
||||
|
||||
如果目标是“每一轮名单整体公平变小”,建议新增独立模式,而不是继续微调当前逐词降字号:
|
||||
- 当前 `largest_empty_square ≥ ceil(font_size * 1.25)` 时视为存在字符级空洞
|
||||
- 流水线会在同一字号档位尝试最多 `3` 种不同布局种子(按人数递减预算)
|
||||
- 目标是最小化最大空洞尺寸,不改变任何字号
|
||||
|
||||
- `REPEAT_FILL_MODE = "ROUND_ROBIN_FAIR"`
|
||||
- 以轮为单位生成任务
|
||||
- 同一轮使用统一字号或统一权重映射
|
||||
- 某一轮放不下时,整轮降低字号重试
|
||||
- 失败词统一进入下一档补位队列
|
||||
- 大字号限制按姓名或轮次计数
|
||||
## 自动画幅
|
||||
|
||||
`calculate_dynamic_dimensions()` 先对 `BASE_HD_WIDTH × BASE_HD_HEIGHT` 做一次 probe 掩膜,计算可填比例 `free_ratio`,再用以下公式扩展:
|
||||
|
||||
```
|
||||
area_per_word = (MIN_READABLE_HEIGHT_PX²) × avg_len × 1.05
|
||||
required_area = num_words × area_per_word × N_REPETITIONS / TARGET_FILL_RATIO
|
||||
required_area /= free_ratio
|
||||
scale_factor = √(required_area / current_area)
|
||||
new_w = clamp(base_w × scale_factor, max_edge=6000)
|
||||
```
|
||||
|
||||
- 扩展后宽高向上取整到最近的 `100`
|
||||
- 最大边长限制为 `6000px`,避免 SVG/PNG 过度膨胀
|
||||
- 掩膜只生成一次,扩展后复用
|
||||
|
||||
+55
-22
@@ -7,13 +7,14 @@
|
||||
- 默认后端地址:`http://localhost:8000`
|
||||
- 请求体中上传文件使用 `multipart/form-data`
|
||||
- `params` 字段是 JSON 字符串,顶层必须是对象
|
||||
- 任务状态存在内存中,服务重启后状态会丢失
|
||||
- 任务状态存在内存中(`JobManager`),服务重启后状态会丢失;文件仍保留在 `service_workspace`
|
||||
- CORS 已开启,允许所有来源
|
||||
|
||||
## Jobs
|
||||
|
||||
### GET `/api/health`
|
||||
|
||||
返回:
|
||||
健康检查。
|
||||
|
||||
```json
|
||||
{"ok": true}
|
||||
@@ -30,15 +31,13 @@
|
||||
Form 字段:
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
| --- | --- | --- |
|
||||
|------|------|------|
|
||||
| `name_list` | 是 | `.xlsx` 名单文件 |
|
||||
| `mask_image` | IMAGE 模式必填 | `.png` / `.jpg` / `.jpeg` 掩膜 |
|
||||
| `font_file` | 否 | 临时上传字体,支持后端 `_FONT_EXTENSIONS` 中的格式 |
|
||||
| `font_file` | 否 | 临时上传字体(`.ttf` / `.ttc` / `.otf`) |
|
||||
| `font_id` | 否 | 使用已上传字体 |
|
||||
| `params` | 否 | JSON 字符串,合并到任务配置 |
|
||||
|
||||
字体格式当前支持 `.ttf`、`.ttc`、`.otf`。
|
||||
|
||||
`params` 示例:
|
||||
|
||||
```json
|
||||
@@ -129,10 +128,12 @@ SSE 事件流。事件数据模型:
|
||||
|
||||
### GET `/api/jobs/{job_id}/locations`
|
||||
|
||||
查询词语位置。查询参数:
|
||||
查询词语位置。
|
||||
|
||||
查询参数:
|
||||
|
||||
| 参数 | 说明 |
|
||||
| --- | --- |
|
||||
|------|------|
|
||||
| `name` | 可选;为空返回全部,非空精确匹配 |
|
||||
|
||||
返回:
|
||||
@@ -173,7 +174,7 @@ SSE 事件流。事件数据模型:
|
||||
查询参数:
|
||||
|
||||
| 参数 | 默认 | 说明 |
|
||||
| --- | --- | --- |
|
||||
|------|------|------|
|
||||
| `fill` | `fill` | `fill` / `dot` / `line` / `ring` |
|
||||
| `stroke` | `0` | 是否描边 |
|
||||
| `spacing` | `10` | 点阵间距 |
|
||||
@@ -192,11 +193,11 @@ SSE 事件流。事件数据模型:
|
||||
|
||||
返回后端硬编码模板列表:
|
||||
|
||||
- `poster_1x2`
|
||||
- `poster_4x5`
|
||||
- `poster_1x1`
|
||||
- `poster_3x4`
|
||||
- `poster_16x9`
|
||||
- `poster_1x2`(竖版手机海报,1080×2160)
|
||||
- `poster_4x5`(社交媒体图,1080×1350)
|
||||
- `poster_1x1`(方形封面,1080×1080)
|
||||
- `poster_3x4`(竖版广告,1080×1440)
|
||||
- `poster_16x9`(横版电商,1920×1080)
|
||||
|
||||
## Assets
|
||||
|
||||
@@ -205,7 +206,7 @@ SSE 事件流。事件数据模型:
|
||||
上传素材。Form 字段:
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
| --- | --- | --- |
|
||||
|------|------|------|
|
||||
| `file` | 是 | 素材文件 |
|
||||
| `name` | 否 | 名称 |
|
||||
| `type` | 否 | 默认 `upload` |
|
||||
@@ -215,7 +216,7 @@ SSE 事件流。事件数据模型:
|
||||
从任务产物导入素材。Form 字段:
|
||||
|
||||
| 字段 | 默认 | 说明 |
|
||||
| --- | --- | --- |
|
||||
|------|------|------|
|
||||
| `kind` | `png` | 产物类型 |
|
||||
| `name` | 空 | 素材名称 |
|
||||
| `type` | `wordcloud` | 素材类型 |
|
||||
@@ -243,10 +244,10 @@ SSE 事件流。事件数据模型:
|
||||
|
||||
### POST `/api/projects`
|
||||
|
||||
Form 字段:
|
||||
创建工程。Form 字段:
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
| --- | --- | --- |
|
||||
|------|------|------|
|
||||
| `name` | 是 | 工程名 |
|
||||
| `template_id` | 是 | 模板 ID |
|
||||
| `background_color` | 是 | `#RRGGBB` |
|
||||
@@ -272,29 +273,61 @@ Form 字段:
|
||||
|
||||
### GET `/api/fonts`
|
||||
|
||||
返回字体列表,包含默认字体项。
|
||||
返回字体列表,包含默认字体项(`__default__`)。
|
||||
|
||||
### POST `/api/fonts`
|
||||
|
||||
上传字体。Form 字段:
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `file` | 是 | 字体文件 |
|
||||
|------|------|------|
|
||||
| `file` | 是 | 字体文件(`.ttf` / `.ttc` / `.otf`) |
|
||||
| `name` | 否 | 字体名 |
|
||||
|
||||
### DELETE `/api/fonts/{font_id}`
|
||||
|
||||
删除已上传字体。默认字体不能删除。
|
||||
|
||||
## Line Spacing Analysis
|
||||
|
||||
### POST `/api/jobs/{job_id}/analyze-line-spacing`
|
||||
|
||||
分析 SVG 词云路径的线距,返回 `LineSpacingAnalysisSummary`。
|
||||
|
||||
请求体(JSON):
|
||||
|
||||
```json
|
||||
{
|
||||
"percentile": 3,
|
||||
"elementWidth": 100.0,
|
||||
"elementHeight": 100.0,
|
||||
"sampleStep": 2.0
|
||||
}
|
||||
```
|
||||
|
||||
响应字段:
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `percentile` | 线距百分位 |
|
||||
| `spacingPx` | 采样线距(像素) |
|
||||
| `spacingMm` | 采样线距(毫米) |
|
||||
| `minSpacingPx` | 最小线距(像素) |
|
||||
| `minSpacingMm` | 最小线距(毫米) |
|
||||
| `curveCount` | 曲线数量 |
|
||||
| `segmentCount` | 线段数量 |
|
||||
| `sourceWidth` / `sourceHeight` | SVG 原始尺寸 |
|
||||
| `elementWidth` / `elementHeight` | 目标元素尺寸 |
|
||||
|
||||
## 常见错误
|
||||
|
||||
| 场景 | 状态码 | detail |
|
||||
| --- | --- | --- |
|
||||
|------|--------|--------|
|
||||
| `params` 不是合法 JSON | 400 | `params must be valid JSON` |
|
||||
| `params` 不是对象 | 400 | `params must be JSON object` |
|
||||
| `name_list` 非 `.xlsx` | 400 | `name_list must be xlsx` |
|
||||
| IMAGE 模式缺少掩膜 | 400 | `mask_image is required when MODE=IMAGE` |
|
||||
| 掩膜文件不存在 | 400 | `mask_image must be png/jpg/jpeg` |
|
||||
| job 不存在 | 404 | `job not found` |
|
||||
| 产物未就绪 | 404 | `artifact not ready` |
|
||||
| 文件类型未知 | 404 | `unknown artifact kind` |
|
||||
|
||||
+72
-37
@@ -1,23 +1,30 @@
|
||||
# 画布与贴纸功能
|
||||
|
||||
本文档描述当前前端代码中已经实现的画布设计功能。事实来源是 `frontend/src/App.tsx`、`frontend/src/pages/CanvasStudio.tsx`、`frontend/src/components/ExportPanel.tsx`、`frontend/src/lib/stickerLibrary.ts` 和 `frontend/src/types.ts`。
|
||||
本文档描述当前前端代码中已实现的画布设计功能。事实来源是 `frontend/src/pages/CanvasStudio.tsx`、`frontend/src/App.tsx`、`frontend/src/components/ExportPanel.tsx`、`frontend/src/lib/stickerLibrary.ts`、`frontend/src/lib/canvasDocument.ts` 和 `frontend/src/types.ts`。
|
||||
|
||||
## 页面关系
|
||||
|
||||
- 应用默认进入画布设计页。
|
||||
- 画布页顶部的“添加词云”会切换到原有词云生成页。
|
||||
- 词云生成页顶部的“返回画布”会回到画布设计页。
|
||||
- 词云生成页的导出面板保留下载 SVG/位图功能,并新增“作为贴纸导入贴纸库”。
|
||||
```
|
||||
TemplateHome(首页)
|
||||
├── CanvasStudio(画布设计页)─ 添加词云 ─→ TestWorkbench(词云生成页)
|
||||
├── TestWorkbench(词云生成页)─ 返回画布 ─→ CanvasStudio
|
||||
└── HelpPage(帮助页)
|
||||
```
|
||||
|
||||
- 应用默认进入首页,展示模板列表
|
||||
- 画布页顶部的"添加词云"会切换到词云生成页
|
||||
- 词云生成页顶部的"返回画布"会回到画布设计页
|
||||
- 词云生成页导出面板保留下载 SVG/位图功能,并新增"作为贴纸导入贴纸库"
|
||||
|
||||
## 贴纸库
|
||||
|
||||
贴纸库是前端本地能力,不依赖后端接口:
|
||||
|
||||
- 存储位置:`localStorage` 的 `wordcloud-sticker-library`。
|
||||
- 数据类型:`StickerAsset`,当前支持 `svg` 和 `image` 两类,已实现入口主要使用 `svg`。
|
||||
- 用户导入 SVG:画布页左侧“贴纸”面板读取 `.svg` 文件文本,写入贴纸库,并立即插入画布。
|
||||
- 词云作为贴纸:导出面板按当前 SVG 导出参数请求 `/api/jobs/{job_id}/custom.svg`,读取返回的 SVG 文本后写入贴纸库。
|
||||
- 贴纸删除只删除本地贴纸库记录,不会删除已经导出的总图文件。
|
||||
- 存储位置:`localStorage` 的 `wordcloud-sticker-library`
|
||||
- 数据类型:`StickerAsset`,当前支持 `svg` 和 `image` 两类,已实现入口主要使用 `svg`
|
||||
- 用户导入 SVG:画布页左侧"贴纸"面板读取 `.svg` 文件文本,写入贴纸库,并立即插入画布
|
||||
- 词云作为贴纸:导出面板按当前 SVG 导出参数请求 `/api/jobs/{job_id}/custom.svg`,读取返回的 SVG 文本后写入贴纸库
|
||||
- 贴纸删除只删除本地贴纸库记录,不会删除已经导出的总图文件
|
||||
|
||||
## 画布模型
|
||||
|
||||
@@ -25,43 +32,71 @@
|
||||
|
||||
当前模型字段:
|
||||
|
||||
- `width`:画布宽度,默认 `1600`。
|
||||
- `height`:画布高度,默认 `1000`。
|
||||
- `background`:画布背景色,默认 `#ffffff`。
|
||||
- `elements`:画布元素数组。
|
||||
- `width`:画布宽度,默认 `1600`
|
||||
- `height`:画布高度,默认 `1000`
|
||||
- `background`:画布背景色,默认 `#ffffff`,支持透明 `#00000000`
|
||||
- `elements`:画布元素数组
|
||||
- `layers`:图层数组(可选,支持可见性、锁定、文件夹分组)
|
||||
- `layerFolders`:图层文件夹数组(可选)
|
||||
|
||||
当前元素类型:
|
||||
|
||||
- `sticker`:引用贴纸库中的 SVG 或图片。
|
||||
- `text`:普通文字,支持内容、颜色、字号、字体、字重、位置、尺寸、旋转、透明度。
|
||||
- `rect`:矩形,支持填充、描边、描边宽度、位置、尺寸、旋转、透明度。
|
||||
- `ellipse`:椭圆,支持填充、描边、描边宽度、位置、尺寸、旋转、透明度。
|
||||
- `line`:线条,支持描边、描边宽度、位置、尺寸、旋转、透明度。
|
||||
- `sticker`:引用贴纸库中的 SVG 或图片(`assetId`)
|
||||
- `text`:普通文字(内容、颜色、字号、字体、字重、位置、尺寸、旋转、透明度)
|
||||
- `rect`:矩形(填充、描边、描边宽度、位置、尺寸、旋转、透明度)
|
||||
- `ellipse`:椭圆(同上)
|
||||
- `line`:线条(描边、描边宽度、位置、尺寸、旋转、透明度)
|
||||
|
||||
## 编辑行为
|
||||
|
||||
- 点击贴纸库中的贴纸会把该贴纸插入画布中央区域。
|
||||
- 画布元素可拖拽移动。
|
||||
- 选中元素后可通过右下角手柄调整大小。
|
||||
- 右侧属性面板可以精确编辑位置、尺寸、旋转、透明度和元素特有属性。
|
||||
- 右侧属性面板提供上移、下移和删除。
|
||||
- 画布面板支持修改画布宽高、背景色、导出 SVG、清空画布。
|
||||
- 点击贴纸库中的贴纸会把该贴纸插入画布中央区域
|
||||
- 画布元素支持:
|
||||
- **拖拽移动**:鼠标/触摸拖拽
|
||||
- **大小调整**:选中后通过右下角手柄调整
|
||||
- **精确编辑**:右侧面板可修改位置、尺寸、旋转、透明度、元素特有属性
|
||||
- **层级调整**:上移、下移
|
||||
- **删除**:删除元素
|
||||
- 右侧面板支持修改画布宽高、背景色
|
||||
- 支持导出总图 SVG、清空画布
|
||||
- **图层管理**:支持图层可见性、锁定、文件夹分组
|
||||
- **吸附对齐**:元素拖拽时自动吸附到附近元素边缘
|
||||
- **缩放**:编辑视图可缩放(不影响导出尺寸)
|
||||
|
||||
## SVG 导出
|
||||
|
||||
“导出总图 SVG”由前端序列化当前画布模型完成:
|
||||
"导出总图 SVG"由前端序列化当前画布模型完成:
|
||||
|
||||
- 导出文件名:`canvas-design.svg`。
|
||||
- 背景输出为一个覆盖全画布的 `<rect>`。
|
||||
- 贴纸输出为 `<image>`,SVG 贴纸会以内联 `data:image/svg+xml` 的形式嵌入。
|
||||
- 文字输出为 `<text>`。
|
||||
- 基础形状输出为原生 SVG 的 `<rect>`、`<ellipse>`、`<line>`。
|
||||
- 元素的位移和旋转写入 SVG `transform`,透明度写入 `opacity`。
|
||||
- 导出文件名:`canvas-design.svg`
|
||||
- 背景输出为一个覆盖全画布的 `<rect>`(透明背景时 fill="none")
|
||||
- 贴纸输出为 `<image>`,SVG 贴纸会以内联 `data:image/svg+xml` 的形式嵌入
|
||||
- 文字输出为 `<text>`
|
||||
- 基础形状输出为原生 SVG 的 `<rect>`、`<ellipse>`、`<line>`
|
||||
- 元素的位移和旋转写入 SVG `transform`,透明度写入 `opacity`
|
||||
|
||||
## ZIP 导出
|
||||
|
||||
支持导出含以下内容的 ZIP 包:
|
||||
|
||||
- `canvas.svg`:总图 SVG
|
||||
- `sticker_{id}.svg`:画布中所有独立 SVG 贴纸
|
||||
- `manifest.json`:元素元数据清单
|
||||
|
||||
## 同底图换名单(Replace Session)
|
||||
|
||||
画布页支持"替换词云名单"功能:
|
||||
|
||||
1. 用户右键点击画布上的词云贴纸,选择"同底图换名单"
|
||||
2. 生成一个 `WordcloudReplaceSession`,包含:
|
||||
- `mask`:原词云的底图遮罩(SVG/Image)
|
||||
- `target`:目标元素信息(含宽度、高度、原遮罩元素 ID)
|
||||
3. 会话传递到词云生成页
|
||||
4. 词云生成页锁定底图,只替换名单内容,生成后自动按原尺寸贴回画布
|
||||
|
||||
## 当前边界
|
||||
|
||||
- 贴纸库和画布文档只保存在当前浏览器本地,不会跨浏览器或跨设备同步。
|
||||
- 当前没有服务端素材库、项目文件格式或协作编辑接口。
|
||||
- SVG 导入按用户信任文件处理;编辑器预览使用图片方式加载,不在页面中直接执行 SVG 内容。
|
||||
- 当前缩放只影响编辑视图,不改变导出尺寸。
|
||||
- 当前导出目标是 SVG;没有在画布页实现 PNG/JPG 总图导出。
|
||||
- 贴纸库和画布文档只保存在当前浏览器 `localStorage`,不会跨浏览器或跨设备同步
|
||||
- 当前没有服务端素材库、项目文件格式或协作编辑接口(Projects 接口存在但服务层级较浅)
|
||||
- SVG 导入按用户信任文件处理;编辑器预览使用图片方式加载,不在页面中直接执行 SVG 内容
|
||||
- 当前缩放只影响编辑视图,不改变导出尺寸
|
||||
- 当前导出目标是 SVG;没有在画布页实现 PNG/JPG 总图导出
|
||||
- 线距分析结果显示在元素属性面板中,辅助激光加工参数设定
|
||||
|
||||
+94
-32
@@ -12,9 +12,9 @@ CLI 入口 `backend/wordcloud_generate_hybrid.py` 的顺序:
|
||||
4. 调用 `finalize_runtime_config()` 派生路径、字体、输出路径
|
||||
5. 调用 `set_random_seed()`
|
||||
|
||||
服务模式下,`POST /api/jobs` 会生成任务配置并写入:
|
||||
服务模式下,`POST /api/jobs` 收到的 `params` JSON 会由服务层合并到任务配置,写入:
|
||||
|
||||
```text
|
||||
```
|
||||
backend/service_workspace/{job_id}/config.json
|
||||
```
|
||||
|
||||
@@ -25,48 +25,110 @@ backend/service_workspace/{job_id}/config.json
|
||||
当前前端 `TestWorkbench.tsx` 提交的关键字段:
|
||||
|
||||
| 前端字段 | 后端配置 |
|
||||
| --- | --- |
|
||||
| `dataColIndex` | `DATA_COL_INDEX` |
|
||||
|----------|----------|
|
||||
| `seed` | `SEED` |
|
||||
| `dataColIndex` | `DATA_COL_INDEX` |
|
||||
| `weightColIndex` | `WEIGHT_COL_INDEX` |
|
||||
| `fontColor` | `FONT_COLOR` |
|
||||
| `nRepetitions` | `N_REPETITIONS` |
|
||||
| `strokeWeights=false` | `ENABLE_STROKE_WEIGHTS=false` |
|
||||
| `strokeWeights` | `ENABLE_STROKE_WEIGHTS` |
|
||||
| `sizeRatio` | `SIZE_RATIO` |
|
||||
| `packingEfficiency` | `PACKING_EFFICIENCY` |
|
||||
| `targetFillRatio` | `TARGET_FILL_RATIO` |
|
||||
| `userMinFontSize` | `USER_MIN_FONT_SIZE` |
|
||||
| `userMaxFontSize` | `USER_MAX_FONT_SIZE` |
|
||||
| `minReadableHeightPx` | `MIN_READABLE_HEIGHT_PX` |
|
||||
| `workScale` | `WORK_SCALE` |
|
||||
| `fillOn` | `FILL_ON` |
|
||||
| `canvasRetryMaxRounds` | `CANVAS_RETRY_MAX_ROUNDS` |
|
||||
| `canvasRetryGrowth` | `CANVAS_RETRY_GROWTH` |
|
||||
| `layoutSeed` | `LAYOUT_SEED` |
|
||||
|
||||
前端只在重复次数大于 1 时传 `N_REPETITIONS`,只在关闭笔画权重时传 `ENABLE_STROKE_WEIGHTS=false`。
|
||||
"名单完整性"不是可关闭参数,始终是硬约束。
|
||||
|
||||
## 常用配置
|
||||
## 完整配置清单
|
||||
|
||||
### 掩膜与画布
|
||||
|
||||
| 键 | 默认值 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `MODE` | `IMAGE` | 掩膜模式,`IMAGE` 或 `TEXT` |
|
||||
| `MASK_IMAGE_PATH` | `7887.png` | IMAGE 模式掩膜路径,服务模式会覆盖为上传文件路径 |
|
||||
| `IMAGE_CANVAS_MODE` | `WIDTH` | 图片掩膜缩放模式 |
|
||||
| `FILL_ON` | `BLACK` | `BLACK` 表示黑色可填,`WHITE` 表示白色可填 |
|
||||
| `EXCEL_PATH` | `四个方向汇总录取名单.xlsx` | Excel 路径,服务模式会覆盖为上传文件路径 |
|
||||
|----|--------|------|
|
||||
| `MODE` | `IMAGE` | `IMAGE` 或 `TEXT` |
|
||||
| `MASK_IMAGE_PATH` | `7887.png` | IMAGE 模式掩膜路径;服务模式会被上传文件路径覆盖 |
|
||||
| `IMAGE_CANVAS_MODE` | `WIDTH` | 图片掩膜缩放模式:`WIDTH` / `HEIGHT` / `AUTO` |
|
||||
| `FILL_ON` | `BLACK` | `BLACK` = 黑色可填,`WHITE` = 白色可填 |
|
||||
| `FILL_CORNERS` | `False` | 是否自动填充四角区域 |
|
||||
| `CORNER_FILL_RATIO` | `0.15` | 四角填充面积占画布比例 |
|
||||
| `BASE_HD_WIDTH` | `4000` | 默认高清画布宽 |
|
||||
| `BASE_HD_HEIGHT` | `4000` | 默认高清画布高 |
|
||||
| `MIN_READABLE_HEIGHT_PX` | `22` | 最小可读高度(像素) |
|
||||
| `WORK_SCALE` | `0.18` | 高清画布到运算网格的缩放比例 |
|
||||
| `CANVAS_RETRY_MAX_ROUNDS` | `1` | 画布扩大重试轮数 |
|
||||
| `CANVAS_RETRY_GROWTH` | `1.12` | 完整名单放不下时的整画布边长增长比例 |
|
||||
|
||||
### 文本掩膜(TEXT 模式)
|
||||
|
||||
| 键 | 默认值 | 说明 |
|
||||
|----|--------|------|
|
||||
| `MASK_TEXT` | `A` | 用作掩膜的文本 |
|
||||
| `MASK_FONT_PATH` | 项目默认字体 | 掩膜字体 |
|
||||
| `MASK_FONT_SIZE` | `3000` | 掩膜文字字号 |
|
||||
|
||||
### 数据与权重
|
||||
|
||||
| 键 | 默认值 | 说明 |
|
||||
|----|--------|------|
|
||||
| `EXCEL_PATH` | `四个方向汇总录取名单.xlsx` | Excel 路径;服务模式被上传文件覆盖 |
|
||||
| `DATA_COL_INDEX` | `1` | 名单列,0-based |
|
||||
| `WEIGHT_COL_INDEX` | `None` | 权重列,0-based |
|
||||
| `WEIGHT_COL_NAME` | `None` | 权重列名,优先于列索引 |
|
||||
| `REMOVE_DUPLICATES` | `False` | 是否对名单去重 |
|
||||
| `ENABLE_STROKE_WEIGHTS` | `True` | 是否在无 Excel 权重时使用笔画复杂度权重 |
|
||||
| `ENABLE_STROKE_WEIGHTS` | `True` | 是否使用笔画复杂度权重 |
|
||||
|
||||
### 填充策略
|
||||
|
||||
| 键 | 默认值 | 说明 |
|
||||
|----|--------|------|
|
||||
| `N_REPETITIONS` | `1` | 名单重复倍率 |
|
||||
| `SIZE_RATIO` | `2.0` | `max_font` 相对 `min_font` 的比例 |
|
||||
| `PACKING_EFFICIENCY` | `0.85` | 面积模型中的打包效率 |
|
||||
| `MIN_ACCEPT_FILL_RATIO` | `0.75` | 填充率重试阈值 |
|
||||
| `REQUIRE_ALL_WORDS` | `True` | 搜索阶段是否优先要求达到目标词数 |
|
||||
| `TARGET_FILL_RATIO` | `0.45` | 面积模型目标笔画填充率 |
|
||||
| `SIZE_RATIO` | `2.0` | `max_font` 相对 `min_font` 的比例;`1.0` 为等字号模式 |
|
||||
| `PACKING_EFFICIENCY` | `0.9` | 面积模型中的打包效率 |
|
||||
|
||||
### 字号硬约束
|
||||
|
||||
| 键 | 默认值 | 说明 |
|
||||
|----|--------|------|
|
||||
| `USER_MIN_FONT_SIZE` | `None` | 用户覆盖最小字号 |
|
||||
| `USER_MAX_FONT_SIZE` | `None` | 用户覆盖最大字号 |
|
||||
| `FONT_SCALE_MIN` | `0.5` | 二分搜索缩放下限 |
|
||||
| `FONT_SCALE_MAX` | `1.2` | 二分搜索缩放上限 |
|
||||
| `LIMIT_LARGE_FONTS` | `True` | 是否限制大字号数量 |
|
||||
| `LARGE_FONT_LIMIT_RATIO` | `0.2` | 大字号数量上限占比 |
|
||||
| `LARGE_FONT_THRESHOLD_RATIO` | `0.8` | 超过有效最大字号该比例视为大字号 |
|
||||
| `LARGE_FONT_CAP_RATIO` | `0.6` | 超过大字号限制后的降级比例 |
|
||||
| `ENABLE_DOT_MATRIX` | `False` | 是否用点阵补偿空白区域 |
|
||||
| `CANVAS_RETRY_MAX_ROUNDS` | `1` | 画布扩大重试轮数 |
|
||||
| `MIN_FONT_FLOOR` | `2` | 绝对字号下限 |
|
||||
|
||||
### 字体与配色
|
||||
|
||||
| 键 | 默认值 | 说明 |
|
||||
|----|--------|------|
|
||||
| `WC_FONT_PATH` | 项目默认字体 | 布局字体 |
|
||||
| `FONT_FALLBACK_PATHS` | 系统字体列表 | 字体回退路径 |
|
||||
| `FONT_COLOR` | `#000000` | 固定字体颜色;为空时使用调色板 |
|
||||
| `DARK_COLOR_PALETTE` | 5 色深色 | `FILL_ON=WHITE` 时使用 |
|
||||
| `LIGHT_COLOR_PALETTE` | 5 色浅色 | `FILL_ON=BLACK` 时使用 |
|
||||
|
||||
### 输出
|
||||
|
||||
| 键 | 默认值 | 说明 |
|
||||
|----|--------|------|
|
||||
| `OUTPUT_DIR` | `.` | 输出目录 |
|
||||
| `OUTPUT_PREFIX` | `""` | 输出文件前缀 |
|
||||
| `OUTPUT_PNG` | `Efficient_Result_HD_AutoResize.png` | PNG 文件名 |
|
||||
| `OUTPUT_SVG` | `Efficient_Result_HD_AutoResize.svg` | SVG 文件名 |
|
||||
| `DB_PATH` | `wordcloud_hd.db` | SQLite 数据库文件名 |
|
||||
| `METRICS_FILE` | `metrics.json` | 指标文件名 |
|
||||
| `SAVE_DEBUG_IMAGES` | `False` | 是否保存调试图 |
|
||||
| `DEBUG_OUTPUT_DIR` | `output` | 调试文件目录 |
|
||||
|
||||
### 可复现性
|
||||
|
||||
| 键 | 默认值 | 说明 |
|
||||
|----|--------|------|
|
||||
| `SEED` | `None` | 随机种子 |
|
||||
| `LAYOUT_ORDER_MODE` | `SORTED` | 展开序列排序模式 |
|
||||
| `LAYOUT_SEED` | `None` | 布局顺序种子,默认继承 `SEED` |
|
||||
|
||||
## JSON 别名
|
||||
@@ -74,9 +136,8 @@ backend/service_workspace/{job_id}/config.json
|
||||
`apply_json_config()` 支持部分小写别名:
|
||||
|
||||
| 别名 | 正式配置 |
|
||||
| --- | --- |
|
||||
|------|----------|
|
||||
| `seed` | `SEED` |
|
||||
| `layout_order_mode` | `LAYOUT_ORDER_MODE` |
|
||||
| `layout_seed` | `LAYOUT_SEED` |
|
||||
| `excel_path` | `EXCEL_PATH` |
|
||||
| `mask_image_path` | `MASK_IMAGE_PATH` |
|
||||
@@ -99,6 +160,7 @@ CLI 覆盖只支持 `parse_args()` 中定义的参数,不支持 `font_color`
|
||||
|
||||
## 路径规则
|
||||
|
||||
相对路径会以 `backend` 目录作为基准解析。输出路径会在 `finalize_runtime_config()` 中创建。
|
||||
|
||||
字体会先尝试项目字体,再尝试 `FONT_FALLBACK_PATHS`。字体不可用会直接失败。
|
||||
- 相对路径以 `backend` 目录作为基准解析
|
||||
- 输出路径会在 `finalize_runtime_config()` 中自动创建目录
|
||||
- 字体先尝试项目字体,再尝试 `FONT_FALLBACK_PATHS`
|
||||
- 字体不可用会直接失败并退出
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
# 部署说明
|
||||
|
||||
本文档覆盖本地开发、Docker 和 Ubuntu 服务器部署。
|
||||
|
||||
## 本地开发
|
||||
|
||||
### 系统要求
|
||||
|
||||
- Python 3.9+
|
||||
- Node.js 16+ + npm
|
||||
- C++ 编译器(macOS: Xcode CLI Tools;Linux: `build-essential`)
|
||||
|
||||
### 一键前后端联调
|
||||
|
||||
```bash
|
||||
./start-all.sh
|
||||
```
|
||||
|
||||
- 后端:http://localhost:8000
|
||||
- 前端:http://localhost:3000
|
||||
- 后端端口可在环境变量 `BACKEND_PORT` 中覆盖(默认 `8000`)
|
||||
|
||||
### 单独启动后端
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
./start-dev.sh
|
||||
```
|
||||
|
||||
脚本行为:
|
||||
|
||||
1. 探测 Python 3.9+(优先系统 Python,否则创建 `.venv`)
|
||||
2. 检查依赖:`fastapi uvicorn python-multipart pydantic pandas openpyxl pillow numpy matplotlib`
|
||||
3. 在外部 Python 中复用 `scipy`(ABI 匹配时),避免网络安装
|
||||
4. C++ 扩展 `ewc_core` 源码有更新时自动重新编译
|
||||
5. 确保运行时目录:`service_workspace`、`service_assets`、`service_projects`
|
||||
6. 自动释放被占用的端口
|
||||
7. 以 `uvicorn --reload` 启动 FastAPI 服务
|
||||
|
||||
### 单独启动前端
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
前端通过 Vite 代理访问后端,配置见 `frontend/vite.config.ts`。
|
||||
|
||||
## Docker 部署
|
||||
|
||||
项目根目录包含 `Dockerfile` 和 `docker-compose.yml`。
|
||||
|
||||
### 构建并启动
|
||||
|
||||
```bash
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
- 前端:http://localhost:3000
|
||||
- 后端:http://localhost:8000
|
||||
- 后端文档:http://localhost:8000/docs
|
||||
|
||||
### 常用命令
|
||||
|
||||
```bash
|
||||
docker compose logs -f # 查看日志
|
||||
docker compose down # 停止服务
|
||||
docker compose restart # 重启服务
|
||||
```
|
||||
|
||||
## Ubuntu 服务器部署
|
||||
|
||||
使用 `install-ubuntu.sh` 一键部署:
|
||||
|
||||
```bash
|
||||
chmod +x install-ubuntu.sh
|
||||
./install-ubuntu.sh
|
||||
```
|
||||
|
||||
脚本行为:
|
||||
|
||||
1. 检查并安装 Docker(Ubuntu/Debian 自动安装)
|
||||
2. 确保 `docker compose` 可用
|
||||
3. 调用 `docker compose up -d --build`
|
||||
4. 打印访问地址
|
||||
|
||||
可选环境变量:
|
||||
|
||||
| 变量 | 说明 |
|
||||
|------|------|
|
||||
| `SKIP_DOCKER_INSTALL=1` | 跳过 Docker 安装检测 |
|
||||
| `NO_BUILD=1` | 不强制 `--build`(沿用已有镜像) |
|
||||
|
||||
首次部署会编译 C++ 扩展和前端,可能需要几分钟。
|
||||
|
||||
### release 包
|
||||
|
||||
`scripts/pack-release.sh` 用于打包 release 包,包含:
|
||||
|
||||
- 预编译前端(无 Node 环境也能运行)
|
||||
- Docker 部署脚本
|
||||
- 简化版 `install-ubuntu.sh`
|
||||
|
||||
## 产物目录
|
||||
|
||||
运行时产生的数据和文件保存在以下目录(已加入 `.gitignore`):
|
||||
|
||||
| 目录 | 用途 |
|
||||
|------|------|
|
||||
| `backend/service_workspace/` | 任务产物(输入/输出/配置) |
|
||||
| `backend/service_assets/` | 后端素材库 |
|
||||
| `backend/service_projects/` | 后端工程项目 |
|
||||
| `backend/service_design_templates/` | 设计模板 |
|
||||
| `backend/service_fonts/` | 上传字体 |
|
||||
| `backend/.runtime/` | 运行时日志 |
|
||||
| `backend/benchmark_outputs/` | 基准测试结果 |
|
||||
|
||||
## 环境变量
|
||||
|
||||
| 变量 | 影响范围 | 说明 |
|
||||
|------|----------|------|
|
||||
| `BACKEND_PORT` | 本地开发 | 后端监听端口,默认 `8000` |
|
||||
| `SKIP_DOCKER_INSTALL` | Ubuntu 部署 | 跳过 Docker 自动安装 |
|
||||
| `NO_BUILD` | Ubuntu 部署 | 不强制 Docker 重建 |
|
||||
| `WORDCLOUD_SCIPY_SITE` | 本地开发 | 外部 SciPy 路径加速启动 |
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 重启后端服务会导致内存中的任务状态丢失,但 `service_workspace` 中的文件产物不受影响
|
||||
- 首次启动时 C++ 扩展编译需要系统编译器;如果失败请检查 `build-essential` 或 Xcode CLI Tools
|
||||
- 前端 `localStorage` 中的贴纸库和画布文档不会自动同步到服务器
|
||||
+143
-79
@@ -1,45 +1,115 @@
|
||||
# 项目标准说明
|
||||
|
||||
本文档按当前代码整理,覆盖项目边界、运行方式、输入输出和维护约定。最后核对代码时间:2026-06-09。
|
||||
本文档覆盖项目边界、目录结构、运行方式、输入输出和维护约定。核对代码时间:2026-07-25。
|
||||
|
||||
## 项目目标
|
||||
|
||||
本项目生成基于名单和掩膜的词云图。后端负责读取 Excel 名单、处理掩膜、计算权重、布局、渲染和导出;前端提供参数面板、任务提交、结果查看、查找和导出入口。
|
||||
本项目是一套中文词云生成+画布设计系统,核心能力:
|
||||
|
||||
当前项目不是通用设计平台。`Projects`、`Assets`、`Templates` 接口存在,但主要服务于当前工作台原型和素材管理,不代表完整生产级工程系统。
|
||||
1. **词云生成**:读取 Excel 名单,按掩膜轮廓填充人名,输出 PNG/SVG/DB/metrics。
|
||||
2. **画布设计**:前端提供图层化画布、贴纸库、基础形状编辑、SVG 总图导出。
|
||||
3. **同底图换名单**:画布中已有的词云底图,支持只换名单不换位置和字号(保留视觉结构)。
|
||||
4. **线距分析**:对 SVG 路径进行线距采样,辅助激光加工参数设定。
|
||||
|
||||
## 目录结构
|
||||
|
||||
| 路径 | 职责 |
|
||||
| --- | --- |
|
||||
| `backend/wordcloud_generate_hybrid.py` | CLI 入口,加载配置后调用生成管线 |
|
||||
| `backend/core/config.py` | 默认配置、JSON 配置合并、CLI 覆盖、路径和字体解析 |
|
||||
| `backend/core/pipeline.py` | 生成主流程:读数据、画布、掩膜、权重、布局、渲染、DB、metrics |
|
||||
| `backend/core/layout.py` | Python 布局调度、字号打分、逐词放置、SVG 导出 |
|
||||
| `backend/core/weights.py` | 笔画复杂度权重、Excel 权重、面积字号模型 |
|
||||
| `backend/core/mask.py` | 掩膜归一化、自动画布、边界安全 padding |
|
||||
| `backend/core/render.py` | 填充率计算和点阵补偿 |
|
||||
| `backend/EfficientWordCloud/` | C++ 扩展及其 Python 包装 |
|
||||
| `backend/service/` | FastAPI 服务、任务管理、文件存储 |
|
||||
| `frontend/src/` | React 工作台 |
|
||||
| `docs/` | 标准文档入口 |
|
||||
```
|
||||
wordcloud/
|
||||
├── docs/ # 标准文档
|
||||
│ ├── README.md
|
||||
│ ├── PROJECT_STANDARD.md
|
||||
│ ├── ALGORITHM.md
|
||||
│ ├── CONFIG.md
|
||||
│ ├── API.md
|
||||
│ ├── CANVAS_STUDIO.md
|
||||
│ ├── TESTING.md
|
||||
│ └── DEPLOYMENT.md
|
||||
├── backend/
|
||||
│ ├── core/ # 词云核心引擎
|
||||
│ │ ├── config.py # 配置默认值、别名、类型校验
|
||||
│ │ ├── pipeline.py # 主流程:数据→掩膜→权重→布局→渲染→导出
|
||||
│ │ ├── layout.py # Python 布局调度、SVG 路径导出
|
||||
│ │ ├── weights.py # 笔画权重、Excel 权重、面积字号模型
|
||||
│ │ ├── mask.py # 掩膜生成、归一化、自动画幅
|
||||
│ │ ├── render.py # 高清精修、填充率计算、重叠检测
|
||||
│ │ ├── fonts.py # 字体缓存
|
||||
│ │ └── ewc.py # 兼容层(基类+Python 稀疏网格)
|
||||
│ ├── EfficientWordCloud/ # C++ 扩展(integral grid + 精确字形碰撞)
|
||||
│ │ ├── efficient_wordcloud/
|
||||
│ │ │ ├── wordcloud.py # Python 包装
|
||||
│ │ │ └── src/ewc_core.cpp # Cython 扩展
|
||||
│ │ └── setup.py # C++ 编译入口
|
||||
│ ├── service/ # FastAPI 服务
|
||||
│ │ ├── app.py # HTTP 路由
|
||||
│ │ ├── schemas.py # Pydantic 模型
|
||||
│ │ ├── runner.py # 子进程任务执行
|
||||
│ │ ├── job_manager.py # 内存任务状态管理
|
||||
│ │ ├── storage.py # 任务文件目录管理
|
||||
│ │ ├── line_spacing.py # SVG 线距分析
|
||||
│ │ └── log_config.py # 服务日志配置
|
||||
│ ├── tests/ # 单元测试
|
||||
│ │ └── test_layout_constraints.py
|
||||
│ ├── tools/ # 基准工具
|
||||
│ │ └── benchmark_layout.py
|
||||
│ ├── service_workspace/ # 运行时任务产物(.gitignore)
|
||||
│ ├── service_assets/ # 后端素材库(.gitignore)
|
||||
│ ├── service_projects/ # 后端工程项目(.gitignore)
|
||||
│ ├── service_design_templates/ # 设计模板(.gitignore)
|
||||
│ └── start-dev.sh # 本地后端启动脚本
|
||||
├── frontend/
|
||||
│ ├── src/
|
||||
│ │ ├── App.tsx # 页面路由:home → canvas / wordcloud / help
|
||||
│ │ ├── main.tsx
|
||||
│ │ ├── types.ts # 全项目 TypeScript 类型
|
||||
│ │ ├── styles.css
|
||||
│ │ ├── pages/ # 页面级组件
|
||||
│ │ │ ├── TemplateHome.tsx # 首页:模板选择
|
||||
│ │ │ ├── CanvasStudio.tsx # 画布设计页
|
||||
│ │ │ ├── TestWorkbench.tsx # 词云生成页
|
||||
│ │ │ └── HelpPage.tsx # 帮助页
|
||||
│ │ ├── components/ # 可复用组件
|
||||
│ │ │ ├── AdvancedPanel.tsx
|
||||
│ │ │ ├── CanvasArea.tsx
|
||||
│ │ │ ├── DockTabBar.tsx
|
||||
│ │ │ ├── EditPanel.tsx
|
||||
│ │ │ ├── ExportPanel.tsx
|
||||
│ │ │ ├── FindPanel.tsx
|
||||
│ │ │ ├── FloatingPanel.tsx
|
||||
│ │ │ ├── ImportPanel.tsx
|
||||
│ │ │ ├── ProgressPanel.tsx
|
||||
│ │ │ ├── ViewControls.tsx
|
||||
│ │ │ └── AppSettingsWindow.tsx
|
||||
│ │ ├── hooks/
|
||||
│ │ ├── lib/
|
||||
│ │ │ ├── api.ts # API 工具函数
|
||||
│ │ │ ├── canvasDocument.ts # 画布模型操作
|
||||
│ │ │ ├── stickerLibrary.ts # 贴纸库存取
|
||||
│ │ │ ├── svgExport.ts # SVG/Zip 序列化
|
||||
│ │ │ └── templateLibrary.ts # 模板库
|
||||
│ │ └── vite-env.d.ts
|
||||
│ ├── vite.config.ts
|
||||
│ ├── package.json
|
||||
│ └── tsconfig.json
|
||||
├── start-all.sh # 一键前后端联调
|
||||
├── install-ubuntu.sh # Ubuntu Docker 一键部署
|
||||
├── release/
|
||||
│ └── install-ubuntu.sh
|
||||
├── scripts/
|
||||
│ └── pack-release.sh
|
||||
└── README.md / README_zh.md # 顶层面向用户说明(非标准)
|
||||
```
|
||||
|
||||
## 运行方式
|
||||
|
||||
### 一键前后端联调
|
||||
|
||||
在项目根目录运行:
|
||||
### 一键前后端联调(推荐开发用)
|
||||
|
||||
```bash
|
||||
./start-all.sh
|
||||
```
|
||||
|
||||
它会:
|
||||
|
||||
- 启动 `backend/start-dev.sh`
|
||||
- 启动前端 `npm run dev`
|
||||
- 默认后端端口为 `8000`
|
||||
- 前端 Vite 端口为 `3000`
|
||||
- 后端:http://localhost:8000
|
||||
- 前端:http://localhost:3000
|
||||
- 两者通过前端 Vite 代理通信(配置见 `frontend/vite.config.ts`)
|
||||
|
||||
### 单独启动后端
|
||||
|
||||
@@ -48,7 +118,11 @@ cd backend
|
||||
./start-dev.sh
|
||||
```
|
||||
|
||||
`start-dev.sh` 会检查 Python 依赖、必要时创建 `.venv`,并在 C++ 源码更新后重新编译 `ewc_core`。
|
||||
脚本会自动:
|
||||
- 探测 Python 3.9+(优先系统 Python,否则创建 `.venv`)
|
||||
- 检查并安装 `fastapi uvicorn python-multipart pydantic pandas openpyxl pillow numpy matplotlib`
|
||||
- 在外部 Python 中复用 `scipy`(ABI 匹配时),避免网络安装
|
||||
- C++ 扩展 `ewc_core` 源码有更新时自动重新编译
|
||||
|
||||
### 单独启动前端
|
||||
|
||||
@@ -57,8 +131,6 @@ cd frontend
|
||||
npm run dev
|
||||
```
|
||||
|
||||
前端通过 Vite 代理访问后端。代理配置见 `frontend/vite.config.ts`。
|
||||
|
||||
### CLI 生成
|
||||
|
||||
```bash
|
||||
@@ -66,81 +138,73 @@ cd backend
|
||||
python wordcloud_generate_hybrid.py --config /path/to/config.json
|
||||
```
|
||||
|
||||
CLI 配置优先级:
|
||||
|
||||
1. `backend/core/config.py` 默认值
|
||||
2. JSON 配置文件
|
||||
3. CLI 参数覆盖
|
||||
CLI 配置优先级见 [CONFIG.md](CONFIG.md)。
|
||||
|
||||
## 输入要求
|
||||
|
||||
### Excel 名单
|
||||
|
||||
服务接口只接受 `.xlsx`。默认名单列为 `DATA_COL_INDEX = 1`,也就是第 2 列,索引从 0 开始。
|
||||
- 服务接口仅接受 `.xlsx`
|
||||
- `DATA_COL_INDEX` 默认 `1`(第2列,0-based)
|
||||
- `REMOVE_DUPLICATES` 默认 `False`,前端也默认保留重复
|
||||
- 权重列:优先 `WEIGHT_COL_NAME`,次选 `WEIGHT_COL_INDEX`
|
||||
- 有效权重必须为正数
|
||||
|
||||
当 `REMOVE_DUPLICATES = False` 时,Excel 中重复姓名会保留。当前前端默认保留重复。
|
||||
### 权重来源
|
||||
|
||||
### 权重
|
||||
|
||||
权重来源按优先级合并:
|
||||
|
||||
1. Excel 权重列:`WEIGHT_COL_NAME` 优先于 `WEIGHT_COL_INDEX`
|
||||
2. 笔画复杂度权重:受 `ENABLE_STROKE_WEIGHTS` 控制
|
||||
3. 默认权重:没有权重时使用 `10`
|
||||
|
||||
关闭 `ENABLE_STROKE_WEIGHTS` 且不传 Excel 权重列时,所有姓名进入均等权重。
|
||||
| 场景 | 行为 |
|
||||
|------|------|
|
||||
| 有 Excel 权重,笔画权重开启 | Excel 值作为基础 × 笔画复杂度归一化乘数 |
|
||||
| 有 Excel 权重,笔画权重关闭 | 仅 Excel 权重 |
|
||||
| 无 Excel 权重,笔画权重开启 | 仅笔画复杂度权重 |
|
||||
| 两者均无 | 全部默认权重 `10` |
|
||||
|
||||
### 掩膜
|
||||
|
||||
`MODE = IMAGE` 时必须提供 PNG/JPG/JPEG 掩膜。后端会将图片转灰度并以阈值 `200` 二值化。
|
||||
|
||||
`FILL_ON = BLACK` 时,黑色区域可填充;`FILL_ON = WHITE` 时,白色区域可填充。
|
||||
- `MODE=IMAGE` 时必须上传 PNG/JPG/JPEG 掩膜文件
|
||||
- 后端转灰度后按阈值 `200` 二值化
|
||||
- `FILL_ON=BLACK`:黑色区域可填充;`FILL_ON=WHITE`:白色区域可填充
|
||||
- `MODE=TEXT`:用指定文字生成文本掩膜
|
||||
|
||||
## 输出产物
|
||||
|
||||
每次服务任务会创建:
|
||||
服务任务产物位置:
|
||||
|
||||
```text
|
||||
```
|
||||
backend/service_workspace/{job_id}/
|
||||
input/
|
||||
mask.png
|
||||
names.xlsx
|
||||
output/
|
||||
Efficient_Result_HD_AutoResize.png
|
||||
Efficient_Result_HD_AutoResize.svg
|
||||
Efficient_Result_HD_AutoResize_stroke.svg
|
||||
wordcloud_hd.db
|
||||
metrics.json
|
||||
debug/
|
||||
mask_src.png
|
||||
mask_hd.png
|
||||
mask_small.png
|
||||
occ_fast.png
|
||||
Efficient_Result_HD_AutoResize.png # 最终位图
|
||||
Efficient_Result_HD_AutoResize.svg # 填充 SVG
|
||||
Efficient_Result_HD_AutoResize_stroke.svg # 描边 SVG(激光雕刻适用)
|
||||
wordcloud_hd.db # SQLite:word_locations 表
|
||||
metrics.json # 运行指标
|
||||
debug/ # 调试图(受 SAVE_DEBUG_IMAGES 控制)
|
||||
config.json
|
||||
```
|
||||
|
||||
产物说明:
|
||||
|
||||
| 文件 | 含义 |
|
||||
| --- | --- |
|
||||
| PNG | 最终位图结果 |
|
||||
| SVG | 填充路径 SVG |
|
||||
| `_stroke.svg` | 描边 SVG,适合继续加工 |
|
||||
| SQLite DB | `word_locations` 表,记录词语位置、字号、颜色、方向、包围盒 |
|
||||
| metrics | 运行指标、画布尺寸、填充率、配置快照 |
|
||||
| debug | 调试图,受 `SAVE_DEBUG_IMAGES` 控制 |
|
||||
|
||||
## 当前限制
|
||||
|
||||
- 重复填充当前按名单轮次展开,但单个词在放置失败时会独立降字号;这会导致后几轮整体字号小于前几轮。
|
||||
- `reorder_stratified()` 当前对主路径 `query_direct()` 没有实际影响,因为 `query_direct()` 不使用 `valid_coords`。
|
||||
- C++ `Grid_query_direct` 的 GIL 释放包装没有包住实际扫描调用,性能并发上还有优化空间。
|
||||
- API 中的 Jobs 存储在进程内存,服务重启后历史任务状态会丢失;文件仍保留在 `service_workspace`。
|
||||
- 旧文档中的市场分析、路线图和性能宣传不作为当前能力承诺。
|
||||
- **名单完整性是不可关闭的硬约束。** 放不下的情况下只会整批缩放字号或扩大画布,不会漏词。
|
||||
- 不存在逐词缩字号、gap filling 或大字号自动压缩路径。
|
||||
- `USER_MIN_FONT_SIZE` 和 `USER_MAX_FONT_SIZE` 是不可越过的边界;冲突时直接报错。
|
||||
- `SIZE_RATIO=1` 时所有同权重词语保持完全相同字号,面积估算和重试阶段都维持单一字号。
|
||||
- C++ 中保留旧矩形查询 API 供底层兼容性,正式流水线使用 `place_glyph_exact()` 真实字形主路径。
|
||||
- 前端贴纸库和画布文档只保存在当前浏览器 `localStorage`,不跨设备同步。
|
||||
- Jobs 状态存在内存中,服务重启后历史任务状态丢失;文件仍保留在 `service_workspace`。
|
||||
- 当前 Projects、Assets、Templates 接口主要服务当前工作台原型和素材管理,不代表完整生产级工程系统。
|
||||
|
||||
## 维护约定
|
||||
|
||||
- 修改算法行为时,同步更新 [ALGORITHM.md](ALGORITHM.md)。
|
||||
- 新增或删除配置项时,同步更新 [CONFIG.md](CONFIG.md)。
|
||||
- 改 HTTP 接口或响应模型时,同步更新 [API.md](API.md)。
|
||||
- 不再新增单次变更记录文档;短期变更应合并进标准文档。
|
||||
| 变更范围 | 同步更新 |
|
||||
|----------|----------|
|
||||
| 算法行为 | ALGORITHM.md |
|
||||
| 配置项增删 | CONFIG.md |
|
||||
| HTTP 接口/响应模型 | API.md |
|
||||
| 前端画布功能 | CANVAS_STUDIO.md |
|
||||
| 测试/基准 | TESTING.md |
|
||||
| 部署方式 | DEPLOYMENT.md |
|
||||
|
||||
不再新增单次变更记录文档;短期变更合并进标准文档。
|
||||
|
||||
+17
-16
@@ -1,27 +1,22 @@
|
||||
# WordCloud 项目文档入口
|
||||
# WordCloud 项目文档
|
||||
|
||||
本文档目录是当前项目的标准文档入口。除非某个历史文档被明确标注为“标准文档”,否则以这里列出的文档为准。
|
||||
|
||||
## 文档准则
|
||||
|
||||
- 以代码为准。文档只描述当前代码实际行为,不提前承诺未实现能力。
|
||||
- 以 `backend/core/config.py`、`backend/core/pipeline.py`、`backend/core/layout.py`、`backend/EfficientWordCloud/efficient_wordcloud/src/ewc_core.cpp` 为算法事实来源。
|
||||
- 以 `backend/service/app.py`、`backend/service/schemas.py` 为 HTTP API 事实来源。
|
||||
- 变更记录只记录历史,不作为使用说明。
|
||||
本文档目录是标准文档入口。标准文档直接对应代码实现;历史/规划文档不作为行为依据。
|
||||
|
||||
## 标准文档
|
||||
|
||||
| 文档 | 用途 |
|
||||
| --- | --- |
|
||||
| [PROJECT_STANDARD.md](PROJECT_STANDARD.md) | 项目结构、运行方式、输入输出、工程约定 |
|
||||
| [ALGORITHM.md](ALGORITHM.md) | 词云生成算法、重复填充、权重、C++ 碰撞搜索 |
|
||||
| [CONFIG.md](CONFIG.md) | 配置项、优先级、前端参数到后端配置的映射 |
|
||||
| [API.md](API.md) | FastAPI 接口、请求格式、响应结构、产物下载 |
|
||||
|------|------|
|
||||
| [PROJECT_STANDARD.md](PROJECT_STANDARD.md) | 项目目标、目录结构、运行方式、输入输出、工程约定 |
|
||||
| [ALGORITHM.md](ALGORITHM.md) | 词云生成算法:面积模型、权重、布局、C++ 碰撞搜索、高清精修 |
|
||||
| [CONFIG.md](CONFIG.md) | 配置项清单、来源优先级、前端参数映射、类型校验 |
|
||||
| [API.md](API.md) | FastAPI HTTP 接口、请求格式、响应结构、产物下载 |
|
||||
| [CANVAS_STUDIO.md](CANVAS_STUDIO.md) | 画布设计、贴纸库、词云作为贴纸、总图 SVG 导出 |
|
||||
| [TESTING.md](TESTING.md) | 单元测试、基准测试、质量门禁 |
|
||||
| [DEPLOYMENT.md](DEPLOYMENT.md) | 本地开发、Docker、Ubuntu 服务器部署 |
|
||||
|
||||
## 非标准/历史文档
|
||||
|
||||
以下文档可能包含历史规划、阶段性设想或已经过期的实现描述,不再作为行为依据:
|
||||
以下文档可能包含过期规划或阶段性描述,不再作为行为依据:
|
||||
|
||||
- `docs/PPT-EfficientWordCloud-详细大纲-v1.0.md`
|
||||
- `docs/stroke-weights-optional.md`
|
||||
@@ -29,4 +24,10 @@
|
||||
- `backend/README.md`
|
||||
- `backend/README_zh.md`
|
||||
|
||||
需要确认行为时,优先查标准文档;标准文档仍不清楚时,直接查代码。
|
||||
## 工程约定
|
||||
|
||||
- **以代码为准。** 文档描述当前代码的实际行为,不提前承诺未实现能力。
|
||||
- 算法事实来源:`backend/core/pipeline.py`、`backend/core/layout.py`、`backend/EfficientWordCloud/efficient_wordcloud/src/ewc_core.cpp`
|
||||
- HTTP API 事实来源:`backend/service/app.py`、`backend/service/schemas.py`
|
||||
- 前端事实来源:`frontend/src/App.tsx`、`frontend/src/pages/TestWorkbench.tsx`、`frontend/src/components/AdvancedPanel.tsx`、`frontend/src/types.ts`
|
||||
- 变更不单独记文档;短期变更直接合并进标准文档。
|
||||
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
# 测试与基准
|
||||
|
||||
本文档覆盖当前代码中的测试工具和基准脚本。
|
||||
|
||||
## 单元测试
|
||||
|
||||
位置:`backend/tests/test_layout_constraints.py`
|
||||
|
||||
运行方式:
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
python -m pytest tests/test_layout_constraints.py -v
|
||||
```
|
||||
|
||||
测试矩阵(基于 `unittest`,不依赖外部服务):
|
||||
|
||||
| 测试 | 目的 |
|
||||
|------|------|
|
||||
| `test_size_ratio_one_keeps_every_equal_weight_size_identical` | `SIZE_RATIO=1` 时所有同权重词字号完全相同 |
|
||||
| `test_explicit_equal_min_max_is_exact` | `USER_MIN=USER_MAX=12` 时所有词精确为 `12px` |
|
||||
| `test_same_weight_groups_receive_the_same_size` | 相同权重组内字号一致;权重组间字号递增 |
|
||||
| `test_stroke_weight_is_applied_when_excel_weights_are_flat` | Excel 权重全为 `1` 时,笔画权重仍能产生区分度 |
|
||||
| `test_largest_empty_square_ignores_space_outside_mask` | 最大空洞算法只计算掩膜内区域 |
|
||||
| `test_conflicting_explicit_font_bounds_fail` | `MIN > MAX` 时报错而非静默回退 |
|
||||
| `test_explicit_max_overrides_automatic_readability_floor` | 用户覆盖最大字号时,自动可读性下限让位于用户输入 |
|
||||
| `test_base_class_never_uses_a_private_fallback_size` | 基类布局不使用隐藏回退字号 |
|
||||
| `test_rendered_ink_stays_inside_mask_and_does_not_overlap` | 工作网格上:墨迹不超出掩膜、不重叠 |
|
||||
| `test_hd_rendered_ink_does_not_overlap_after_scaling` | 高清放大后:零重叠像素、零碰撞边距 |
|
||||
|
||||
固定种子(`SEED=LAYOUT_SEED=20260718`)确保可复现。
|
||||
|
||||
## 基准测试
|
||||
|
||||
位置:`backend/tools/benchmark_layout.py`
|
||||
|
||||
运行方式:
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
python tools/benchmark_layout.py --counts 80 800 --canvas 2000 --assert-targets
|
||||
```
|
||||
|
||||
参数:
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `--counts COUNT [COUNT ...]` | 测试名单数量,默认 `80 800` |
|
||||
| `--canvas CANVAS` | 画布尺寸,默认 `2000` |
|
||||
| `--max-growth-rounds N` | 最大画布扩展轮数,默认 `1` |
|
||||
| `--assert-targets` | 启用门禁检查 |
|
||||
| `--output-dir PATH` | 输出目录,默认 `backend/benchmark_outputs/` |
|
||||
|
||||
### 门禁检查项(`--assert-targets`)
|
||||
|
||||
| 指标 | 阈值 | 含义 |
|
||||
|------|------|------|
|
||||
| `completeness` | `= 1.0` | 名单必须全部放入 |
|
||||
| `equal_weight_font_consistent` | `True` | 等权重时字号一致 |
|
||||
| `hd_overlap_pixels` | `= 0` | 高清渲染后零重叠 |
|
||||
| `contour_grid_coverage` | `≥ 0.80` | 轮廓网格覆盖率(避免大块空洞) |
|
||||
| `hd_true_density` | `≥ 0.10` | 真实笔画密度 |
|
||||
| `total_seconds` | `< 1.0s` (count<100) / `< 5.0s` (count<1000) | 性能门槛 |
|
||||
|
||||
### 输出指标
|
||||
|
||||
| 指标 | 说明 |
|
||||
|------|------|
|
||||
| `count` | 名单数量 |
|
||||
| `canvas` | 最终画布尺寸 |
|
||||
| `canvas_growth_rounds` | 画布扩展轮数 |
|
||||
| `placed` | 实际放置词数 |
|
||||
| `completeness` | 完整率 |
|
||||
| `layout_seconds` / `render_seconds` / `total_seconds` | 各阶段耗时 |
|
||||
| `work_fill_ratio` | 工作网格填充率 |
|
||||
| `font_size_min` / `font_size_max` | 字号范围 |
|
||||
| `equal_weight_font_consistent` | 等权重字号一致性 |
|
||||
| `collision_margin` | 碰撞边距 |
|
||||
| `hd_clearance_shifted_words` | 高清精修时位移词数 |
|
||||
| `hd_clearance_max_shift` | 最大位移像素 |
|
||||
| `hd_clearance_px` | 隔离带宽度 |
|
||||
| `hd_clearance_priority_restarts` | 优先级回溯次数 |
|
||||
| `hd_overlap_pixels` | 高清重叠像素(双重检查) |
|
||||
| `largest_empty_square_work_px` | 工作网格最大空洞(像素) |
|
||||
| `largest_empty_square_font_ratio` | 空洞相对字号比例 |
|
||||
| `hd_true_density` | 高清真实笔画密度 |
|
||||
| `ink_bbox_coverage` | 墨迹包围盒覆盖率 |
|
||||
| `contour_grid_coverage` | 轮廓网格覆盖率 |
|
||||
|
||||
基准结果保存为 JSON:`backend/benchmark_outputs/benchmark.json`
|
||||
|
||||
## 扩展基准
|
||||
|
||||
基准脚本使用 `run_generation_pass()` 直接调用核心管线,绕过 HTTP 服务和文件 IO。
|
||||
|
||||
- 生成数据:`make_names()` 使用中文姓氏库和双字名库组合出不重复姓名
|
||||
- 掩膜:`make_round_mask()` 生成圆形掩膜
|
||||
- 固定种子:`SEED=LAYOUT_SEED=20260718`
|
||||
- 固定配置:`SIZE_RATIO=1.0, N_REPETITIONS=1, WORK_SCALE=0.18, TARGET_FILL_RATIO=0.45`
|
||||
|
||||
## CI 建议
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
python -m pytest tests/test_layout_constraints.py -v
|
||||
python tools/benchmark_layout.py --counts 80 800 --canvas 2000 --assert-targets
|
||||
```
|
||||
|
||||
两次运行均应在数秒内完成。
|
||||
Reference in New Issue
Block a user