Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
```text
L1 页面级 标题 / 调号 / 拍号 / 主谱面 ROI
L2 主谱面 谱行(systems;pitch+和弦+歌词绑定为同一行)
L3 谱行内 小节线 → 小节(measures)
L3 谱行内 纵向分割线 splits → 派生小节 measures(#85
L4 小节内 音符 / 和弦 / 歌词候选 ROI(几何为主)
L5 音符节点 音高数字 OCR(+几何兜底)+ 时值线 / 高低音点等几何
→ 组装 Score JSON
Expand All @@ -55,10 +55,12 @@ L5 音符节点 音高数字 OCR(+几何兜底)+ 时值线 / 高低音点
| 原则 | 说明 |
|------|------|
| L1–L4 | **OpenCV / 几何 / 版面**为主,不依赖整页 OCR 文本顺序 |
| L3 | **主存分割线**,小节框由 L2 行边界 + 线推导;桌面拖线编辑 |
| L5 | **音高数字**以 OCR 为主,并与同节点几何特征绑定 |
| 开关 | `ENPU_PIPELINE_MODE=structure` 或 `legacy` |

桌面在结构模式下可叠图查看 L1–L5。完整说明见 [docs/architecture-structure-first.md](./docs/architecture-structure-first.md) · [docs/architecture.md](./docs/architecture.md)。
桌面在结构模式下可叠图查看 L1–L5,L3 以分割线编辑为主。
完整说明:[architecture-structure-first.md](./docs/architecture-structure-first.md) · [l3-split-model.md](./docs/l3-split-model.md) · [architecture.md](./docs/architecture.md)。

---

Expand Down
13 changes: 9 additions & 4 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
> 目标格式:MusicXML / MIDI / 自定义 JSON。

**仓库**:https://github.com/loootte/EnPu
**最后更新**:2026-07-25
**当前阶段**:#34/#35/#38 ✅;**#58 结构优先分层内核**(可选 `ENPU_PIPELINE_MODE=structure`);精度 **#36/#37**;体验 **#45–#52**;目标 **v0.1.0**;云端 #13 可并行
**最后更新**:2026-08-01
**当前阶段**:#34/#35/#38 ✅;**#58 结构优先** + **#85 L3 分割线模型** ✅;#84/#86/#89 分层评测/调优 ✅;精度 **#36/#37**;体验 **#45–#52**;目标 **v0.1.0**;云端 #13 可并行

---

Expand Down Expand Up @@ -336,7 +336,7 @@ Issues 列表:https://github.com/loootte/EnPu/issues
| [#36](https://github.com/loootte/EnPu/issues/36) | 扩充真实敬拜谱评测集(≥15) | P4 | **P0** | ⬜ **下一步(精度)** |
| [#37](https://github.com/loootte/EnPu/issues/37) | 高密度谱与时值线检测 | P4 | P1 | ⬜ |
| [#43](https://github.com/loootte/EnPu/issues/43) | 产品/体验演进清单入 Roadmap | docs | P0 | ✅ [PR #44](https://github.com/loootte/EnPu/pull/44) |
| [#58](https://github.com/loootte/EnPu/issues/58) | 结构优先分层识别内核(L1–L5,先几何后 OCR) | P4 / core | **P0** | 🔄 脚手架 `pipeline_mode=structure` |
| [#58](https://github.com/loootte/EnPu/issues/58) | 结构优先分层识别内核(L1–L5,先几何后 OCR) | P4 / core | **P0** | 🔄 持续迭代(L1–L5 已可跑;L3 见 #85) |
| [#45](https://github.com/loootte/EnPu/issues/45) | 原稿对照双视图校对(P4-F) | P4 | **P0** | ⬜ v0.1 体验加分 |
| [#46](https://github.com/loootte/EnPu/issues/46) | 错误标记与问题导航(P4-G) | P4 | **P0** | ⬜ v0.1 体验加分 |
| [#47](https://github.com/loootte/EnPu/issues/47) | 图像预处理工具箱(P4-H) | P4 | P1 | ⬜ |
Expand All @@ -359,6 +359,10 @@ Issues 列表:https://github.com/loootte/EnPu/issues
| [#33](https://github.com/loootte/EnPu/issues/33) | 基于基线更新产品与技术路线 | docs | ✅ |
| [#34](https://github.com/loootte/EnPu/issues/34) | 版面/谱行分类,过滤非谱行数字 | P4 | ✅ |
| [#35](https://github.com/loootte/EnPu/issues/35) | 小节线检测与切分加固 | P4 | ✅ |
| [#84](https://github.com/loootte/EnPu/issues/84) | 旋律带小节线检测 | P4 / L3 | ✅ |
| [#85](https://github.com/loootte/EnPu/issues/85) | L3 行内纵向分割线模型(主存 splits → 派生 measures) | P4 / L3 | ✅ |
| [#86](https://github.com/loootte/EnPu/issues/86) | 分层准确度量化 + 编辑框作 GT | P4 / eval | ✅ |
| [#89](https://github.com/loootte/EnPu/issues/89) | 单层自动调优循环(L3) | P4 / eval | ✅ |
| [#36](https://github.com/loootte/EnPu/issues/36) | 扩充真实敬拜谱评测集 | P4 | ⬜ OPEN |
| [#37](https://github.com/loootte/EnPu/issues/37) | 高密度谱与时值线检测 | P4 | ⬜ OPEN |
| [#38](https://github.com/loootte/EnPu/issues/38) | print_clear F1 CI 门槛 | P1 | ✅ |
Expand Down Expand Up @@ -398,14 +402,15 @@ Issues 列表:https://github.com/loootte/EnPu/issues
| 2026-07-24 | **#33** 转向结构精度:#34 过滤 → #35 小节 → #36 真实集;v0.1.0 不阻塞于云端 |
| 2026-07-25 | **#43** 产品/体验清单入 Roadmap;子 Issue **#45–#52** 已开;**#34/#35/#38** 已关闭 |
| 2026-07-25 | Issue 索引同步:OPEN = #13/#36/#37/#43/#45–#52;精度下一步 **#36** |
| 2026-08-01 | **#85** L3 主存分割线、派生小节、桌面拖线、双谱高亮对齐;文档 `l3-split-model.md`;**#84/#86/#89** 已合 |

### Phase 摘要

**Phase 0–2**:PoC、识别 MVP、编辑导出、打包 CD 已通。

**Phase 1 收尾**:评测与基线完成(#29)。

**精度**:#34/#35/#38 ✅;收尾 **#36** 真实评测集 + **#37** 高密度。
**精度**:#34/#35/#38 ✅;结构 L3 **#85**(分割线)✅ + #84/#86/#89 评测调优 ✅;收尾 **#36** 真实评测集 + **#37** 高密度。

**产品体验(#43 → #45–#52)**:校对工具链 + 编辑增强已拆 Issue。

Expand Down
12 changes: 9 additions & 3 deletions core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,21 @@ ENPU_RECOGNIZE_ENGINE=mock pytest -q
5. **parse** — OCR → `Score` v0.1(音高/时值/小节;失败回退 hints / ocr_only)
6. **export** — `POST /v1/export` 将 Score 转为 MusicXML / MIDI(music21,#11)

### structure(#58)
### structure(#58 / #85

`ENPU_PIPELINE_MODE=structure` 时:

```text
L1 版面 → L2 谱行 → L3 小节 → L4 音符 ROI → L5 局部 OCR+几何 → Score
L1 版面 → L2 谱行 → L3 纵向分割线 → 派生小节 → L4 音符 ROI → L5 局部 OCR+几何 → Score
```

代码:`core/app/pipeline/structure/`。详见仓库根 [README](../README.md) 与 [docs/architecture-structure-first.md](../docs/architecture-structure-first.md)。
| 层 | 要点 |
|----|------|
| L3(#85) | 主存 **interior splits**(全图像素 x);`splits_to_measures` 派生小节矩形;端点取 L2 行边界 |
| 模块 | `structure/splits.py`(纯函数)、`l3_measures.py`(检测)、`ir.SplitLine`、`assemble`/`rebuild` |

结构调试字段:`structure.barlines[]`(可编辑分割线)、L3 items `kind=measure_derived`。
文档:[l3-split-model.md](../docs/l3-split-model.md) · [architecture-structure-first.md](../docs/architecture-structure-first.md) · 根 [README](../README.md)。

## Sidecar 打包(可选,Issue #8)

Expand Down
6 changes: 6 additions & 0 deletions core/app/evaluation/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,12 @@ def barline_x_metrics(
"n_pred": len(pred_xs),
"max_dist": max_dist,
"mean_abs_dist": (sum(dists) / len(dists)) if dists else None,
# #85 line-level report aliases
"split_count_mae": float(abs(len(pred_xs) - len(gt_xs))),
"split_count_exact": 1.0 if len(pred_xs) == len(gt_xs) else 0.0,
"split_mean_abs_x_error": (sum(dists) / len(dists)) if dists else None,
"split_fp": fp,
"split_fn": fn,
"abs_delta_count": abs(len(pred_xs) - len(gt_xs)),
},
)
47 changes: 36 additions & 11 deletions core/app/pipeline/structure/assemble.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,29 +248,54 @@ def page_layout_to_structure_debug(layout: PageLayout) -> StructureDebug:
by1, by2 = float(mb[0]), float(mb[1])
else:
by1, by2 = sys.rect.y1, sys.rect.y2
for x in sys.barline_xs:
barlines.append(
{
"system": sys.index,
"x": x,
"y1": by1,
"y2": by2,
}
)
# #85: splits are primary L3; barlines carry id/source for UI drag-edit
split_list = list(getattr(sys, "splits", None) or [])
if split_list:
for i, sp in enumerate(split_list):
x = float(getattr(sp, "x", sp) if not isinstance(sp, (int, float)) else sp)
sid = getattr(sp, "split_id", None) or f"l3-split-sys{sys.index}-{i}"
src = getattr(sp, "source", None) or "detect"
conf = getattr(sp, "confidence", None)
barlines.append(
{
"system": sys.index,
"x": x,
"y1": by1,
"y2": by2,
"id": sid,
"source": src,
"confidence": conf,
"editable": True,
}
)
else:
for i, x in enumerate(sys.barline_xs):
barlines.append(
{
"system": sys.index,
"x": x,
"y1": by1,
"y2": by2,
"id": f"l3-split-sys{sys.index}-{i}",
"source": "detect",
"editable": True,
}
)
for meas in sys.measures:
global_m += 1
# global_m follows system/measure iteration after geometry sort (#78)
src = (meas.extra or {}).get("measure_source", "")
label = f"m{global_m}"
if src and src not in ("l3_barline",):
if src and src not in ("l3_barline", "l3_split"):
label = f"m{global_m}/{src}"
items.append(
StructureBox(
layer="L3",
id=f"l3-m{global_m}",
label=label,
box=meas.rect.as_box(),
kind="measure",
# derived from splits — UI treats as read-only when editing L3 lines
kind="measure_derived",
confidence=meas.confidence,
)
)
Expand Down
30 changes: 28 additions & 2 deletions core/app/pipeline/structure/ir.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ class NoteCandidate:

@dataclass
class MeasureLayout:
"""L3: one measure on a staff system."""
"""L3: one measure on a staff system (usually **derived** from splits, #85)."""

index: int # 0-based within system
rect: Rect
Expand All @@ -110,14 +110,40 @@ class MeasureLayout:
extra: dict[str, Any] = field(default_factory=dict)


@dataclass
class SplitLine:
"""L3 interior vertical split on a staff system (#85). Full-image pixel x."""

x: float
split_id: str = ""
source: str = "detect" # detect | user | soft_gap | migrate
confidence: float = 0.7
extra: dict[str, Any] = field(default_factory=dict)

def as_dict(self) -> dict[str, Any]:
return {
"x": self.x,
"split_id": self.split_id,
"source": self.source,
"confidence": self.confidence,
**({"extra": self.extra} if self.extra else {}),
}


@dataclass
class StaffSystem:
"""L2: one horizontal staff / jianpu row."""
"""L2: one horizontal staff / jianpu row.

**#85**: L3 primary state is ``splits`` (interior vertical dividers).
``measures`` and ``barline_xs`` are derived for L4/Score compatibility.
"""

index: int
rect: Rect
measures: list[MeasureLayout] = field(default_factory=list)
barline_xs: list[float] = field(default_factory=list)
# Interior split lines (full-image x); not including L2 left/right bounds
splits: list[SplitLine] = field(default_factory=list)
confidence: float = 0.5
extra: dict[str, Any] = field(default_factory=dict)

Expand Down
Loading
Loading