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:
2026-07-26 18:32:25 +08:00
co-authored by Claude Sonnet 5
parent bf2b138007
commit 1d17b5e20d
24 changed files with 2600 additions and 1283 deletions
+113 -70
View File
@@ -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``100300` 人提高到 `512`;其余保持 `256`
4. 随机探测失败后,从种子决定的偏移开始完整扫描
5. 对每个候选位置逐像素比较碰撞 mask 与 C++ `canvas`
6. 命中后只写入真实字形,占位查询和写入在同一次 C++ 调用中完成
`stamp_and_rebuild()` 的行为:
真实字形搜索允许透明角落和笔画间空隙安全交错,比外接矩形碰撞更密。安全边距只参与候选检查,不会被双侧累计放大。
1. 把真实字形像素写入 C++ `canvas`
2. 从字形左上角开始局部重建积分图
## 高清精修(隔离带)
当前碰撞检测是“矩形找位置 + 字形像素落图”。找位置阶段要求文字包围盒矩形完全空;实际占用阶段只写入字形像素
正式流水线不在低清工作网格增加边距,因为 `1` 个工作像素会被放大成约 `56` 个高清像素并显著损失容量
隔离带优先作用在最终高清画布,宽度为 `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
View File
@@ -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
View File
@@ -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
View File
@@ -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`
- 字体不可用会直接失败并退出
+132
View File
@@ -0,0 +1,132 @@
# 部署说明
本文档覆盖本地开发、Docker 和 Ubuntu 服务器部署。
## 本地开发
### 系统要求
- Python 3.9+
- Node.js 16+ + npm
- C++ 编译器(macOS: Xcode CLI ToolsLinux: `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. 检查并安装 DockerUbuntu/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
View File
@@ -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 # SQLiteword_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
View File
@@ -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
View File
@@ -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
```
两次运行均应在数秒内完成。