Commit 38b37e08 38b37e086d340ade289d9b15fd828001e1f346d9 by cnb.bofCdSsphPA

Why the schema docs need one song-centric story, not parallel histories

Constraint: New teammates must understand where slice/model/feature data lands without reading deprecated v2/planner-worker material
Rejected: Keep old docs with disclaimers | still leaves two competing mental models in the default docs path
Confidence: high
Scope-risk: narrow
Directive: Keep future docs anchored on the 4-table song-centric path unless the physical schema default truly changes
Tested: markdown link check on /workspace/docs; staged diff review; verified referenced wrapper script is present
Not-tested: No database or pipeline rerun was needed for this docs-only consolidation
1 parent 020702cc
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PYTHON_BIN="${PYTHON_BIN:-/usr/local/miniconda3/bin/python}"
DSN="${1:-${PG_DSN:-}}"
SCHEMA="${2:-${PG_SCHEMA:-acr_songcentric_test}}"
INPUT_ROOT="${3:-$ROOT_DIR/data/songcentric_builder_smoke}"
OUTPUT_DIR="${4:-$ROOT_DIR/data/pgvector_eval/music20}"
if [[ -z "$DSN" ]]; then
echo "usage: $0 <postgres-dsn> [schema] [input-root] [output-dir]" >&2
echo "or set PG_DSN before running this script" >&2
exit 1
fi
cd "$ROOT_DIR/.."
"$PYTHON_BIN" acr-engine/scripts/run_songcentric_directory_pipeline_live.py \
--dsn "$DSN" \
--schema "$SCHEMA" \
--input-root "${INPUT_ROOT#$ROOT_DIR/..\/}" \
--output-dir "${OUTPUT_DIR#$ROOT_DIR/..\/}"
# Changelog
## 2026-06-04
- 收敛 `docs/` 到当前 song-centric 主线,只保留 `README / start-here / session-handoff / postgresql-data-model / postgres_db_schema_samples / CHANGELOG` 六份核心文档,删除旧的 v2 / planner-worker / registry 扩展文档,避免新同学误入已退居次线的设计。
- 重写 `docs/postgresql-data-model.md`,明确 `保存切片的数据 + 模型 + feature` 的落表方案:`window``audio_object`,模型身份落 `feature_fact.model_name/model_version/feature_set_name`,具体 `fingerprint/embedding` 也统一落 `feature_fact`
- 重写 `docs/postgres_db_schema_samples.md` 与入口文档,补充当前 4 表主链的流程图、典型 SQL 样例、查询回溯路径与写入顺序,统一文档口径到 `media_entity -> audio_object -> feature_fact -> set_membership`
## 2026-06-04
- 新增 `acr-engine/scripts/start_songcentric_shortest_path.sh`,把当前默认主线再收敛成一条可直接复制执行的 shell 入口,并已用 fresh runner 结果再次验证。
-`run_songcentric_directory_pipeline_live.py` 提升为当前默认主线入口,并把 fresh runner 结果同步到 `docs/README.md``docs/start-here.md``docs/session-handoff.md`,降低下次 session 的恢复成本。
......
# ACR Docs Overview
> 当前仅保留与 **song-centric + 融合优先** ACR 设计直接相关的文档。
> 当前 docs 只保留与 **song-centric + 4 表融合 schema** 直接相关的文档。
---
## 0. 新同学先做什么
## 1. 先看什么
如果当前要继续 song-centric 主线,先跑
新同学接手顺序
```bash
cd /workspace
/usr/local/miniconda3/bin/python acr-engine/scripts/run_songcentric_directory_pipeline_live.py \
--dsn 'postgres://d2:d2pass@127.0.0.1:5432/d2' \
--schema acr_songcentric_test \
--input-root acr-engine/data/songcentric_builder_smoke \
--output-dir acr-engine/data/pgvector_eval/music20
```
如果要回归旧的 planner/worker 合同,再跑:
```bash
cd /workspace/acr-engine
/usr/local/miniconda3/bin/python scripts/run_planner_validation_commands_live.py \
--dsn 'postgres://d2:d2pass@127.0.0.1:5432/d2' \
--output data/pgvector_eval/music20/planner_validation_commands_runner_report.json
```
也可以用包装脚本:`acr-engine/scripts/start_phase1_shortest_path.sh 'postgres://d2:d2pass@127.0.0.1:5432/d2'`
当前 fresh evidence:
- `executed_count = 4`
- `all_passed = true`
1. [start-here.md](./start-here.md)
2. [session-handoff.md](./session-handoff.md)
3. [postgresql-data-model.md](./postgresql-data-model.md)
4. [postgres_db_schema_samples.md](./postgres_db_schema_samples.md)
5. [CHANGELOG.md](./CHANGELOG.md)
---
## 1. 当前默认设计口径
## 2. 当前默认设计口径
当前 Phase-1 默认按下面理解
逻辑语义
```text
song -> asset -> window -> fingerprint / embedding
```
对应融合优先物理表:
物理落表:
```text
media_entity -> audio_object -> feature_fact -> set_membership
```
核心目标:
- 最终稳定返回 `song_id`
- 同一个 `song` 下允许多个音频文件
- `window` 是切片/evidence/召回最小单元
- `feature_fact` 同时承载 exact lane 与 semantic lane
- Phase-1 直接复用开源 encoder,不先训练/微调
---
## 2. 必读文档
## 3. 一键验证主链
1. [start-here.md](./start-here.md)
2. [session-handoff.md](./session-handoff.md)
3. [acr-architecture.md](./acr-architecture.md)
4. [postgresql-data-model.md](./postgresql-data-model.md)
5. [phase1-implementation-checklist.md](./phase1-implementation-checklist.md)
```bash
cd /workspace
/usr/local/miniconda3/bin/python acr-engine/scripts/run_songcentric_directory_pipeline_live.py \
--dsn 'postgres://d2:d2pass@127.0.0.1:5432/d2' \
--schema acr_songcentric_test \
--input-root acr-engine/data/songcentric_builder_smoke \
--output-dir acr-engine/data/pgvector_eval/music20
```
---
包装脚本:
## 3. 实施相关文档
```bash
acr-engine/scripts/start_songcentric_shortest_path.sh 'postgres://d2:d2pass@127.0.0.1:5432/d2'
```
- [postgresql-data-model.md](./postgresql-data-model.md) — 当前唯一默认数据模型;含切片/模型/feature 落表说明与流程图
- [postgres_db_schema_samples.md](./postgres_db_schema_samples.md) — PostgreSQL 存储样例
- [model-feature-registry-bootstrap.md](./model-feature-registry-bootstrap.md) — model/feature/reference set 初始化
- [phase1-worker-contract.md](./phase1-worker-contract.md) — worker、job、失败语义合同
- [phase1-implementation-checklist.md](./phase1-implementation-checklist.md) — Phase-1 实施清单
- [production-encoder-freeze-and-embedding-strategy.md](./production-encoder-freeze-and-embedding-strategy.md) — encoder-only 冻结策略
- [sota-evolution-guide.md](./sota-evolution-guide.md) — 当前 SOTA 演进主线
当前 fresh evidence:
- `song_count = 2`
- `asset_count = 2`
- `window_count = 5`
- `matcher_fingerprint_count = 5`
- `fallback_fingerprint_count = 0`
- `semantic_runtime_available = false`
- `import_counts.feature_fact = 24`
---
## 4. 当前稳定结论
## 4. 当前保留文档分别解决什么
- 最终归属对象当前只要求稳定返回 `song_id`
- 同一个 `song` 下允许有多个音频文件
- 当前暂不把 `recording/version` 作为必须返回对象
- `window` 仍然保留,因为它是 evidence / offset / 检索最小单元
- `feature_fact` 统一承载 `fingerprint``embedding`
- [start-here.md](./start-here.md):新同学 10 分钟接手入口
- [session-handoff.md](./session-handoff.md):下次启动从哪里继续
- [postgresql-data-model.md](./postgresql-data-model.md):表设计、字段语义、流程图、设计取舍
- [postgres_db_schema_samples.md](./postgres_db_schema_samples.md):DDL、样例数据、典型 SQL、导入查询链路
- [CHANGELOG.md](./CHANGELOG.md):变更历史
---
## 5. 文档维护命令
```bash
/usr/local/miniconda3/bin/python scripts/check_markdown_links.py --root docs
/usr/local/miniconda3/bin/python /workspace/scripts/check_markdown_links.py --root /workspace/docs
```
默认会跳过 `CHANGELOG.md` 这类历史归档文档。
......
# ACR 系统蓝图 / Architecture Blueprint
> 更新:2026-06-04
> 目标:把当前 ACR 原型、未来 SOTA 演进路径、以及不同角色的关注点统一到一份可读的系统蓝图里。
## 一页结论
当前仓库已经验证了一个可运行的混合识别原型:
- `Chromaprint / fingerprint`:负责 exact / near-duplicate 快速召回
- `ECAPA-style embedding`:负责当前语义向量召回 baseline
- `melody-aware rerank`:负责弱旋律补强
但未来面向 **版权保护 + 100w 音频 / 30w 歌曲** 的目标,系统应演进为:
1. **数据规范稳定**`canonical_song -> work -> recording -> recording_asset -> audio_window`
2. **底座模型可替换**`model_registry -> feature_set_registry -> embedding/index`
3. **检索链分层**:exact lane + semantic lane + version/cover lane + aggregation
4. **服务与运维分离**:离线建库、在线召回、审核归一、监控治理分别有清晰职责
---
## 1. 总体系统图
```mermaid
flowchart TD
A[Audio Sources\n官方母带 / 平台音频 / 抓取音频 / UGC / 录音] --> B[Asset Normalization]
B --> C[Canonical Data Model\nSong / Work / Recording / Asset / Window]
C --> D1[Exact Lane\nChromaprint / Neural AFP]
C --> D2[Semantic Lane\nFoundation Encoder]
C --> D3[Version/Cover Lane\nPhase-2+]
D1 --> E[Candidate Aggregation]
D2 --> E
D3 --> E
E --> F[Canonical Song Decision]
F --> G[Service / Review / Audit]
```
---
## 2. 当前实现 vs 目标实现
| 维度 | 当前实现 | 目标实现 |
|---|---|---|
| 底座向量模型 | ECAPA-style baseline | MERT / MuQ 等 foundation encoder 为主 |
| 检索结构 | chromaprint + embedding + melody | exact + semantic + version/cover + rerank |
| 数据主键 | 以 `song_id` 为核心 | `canonical_song / work / recording / asset / window` 分层 |
| 存储形态 | 原型式 pgvector schema + 文件产物 | PostgreSQL 主数据 + 可替换向量/索引层 |
| 服务目标 | 验证闭环 | 版权保护 / 归属判断 / 工业化运维 |
---
## 2.1 为什么现在会显得“层很多”
因为当前蓝图同时覆盖了 3 个维度:
1. **业务归属**`song/work/recording`
2. **音频实体**`asset/window`
3. **检索计算**`feature/index/candidate/decision`
把这三类问题放在一张总图中,会看起来像一条很长的链。
但在工程上,它们其实是不同职责:
- 业务归属层回答:**最后该归谁**
- 音频实体层回答:**命中的是哪段音频**
- 检索计算层回答:**这段音频是怎么被召回出来的**
---
## 2.2 当前最小可用架构可以收敛到什么程度
如果当前阶段只追求:
> 快速稳定地把 query 命中到正确 `song_id`
那 Phase-1 完全可以按下面这套最小骨架推进:
```text
song -> asset -> window -> fingerprint / embedding
```
保留原因:
- `window` 不能删:它是 offset/evidence/多段投票的最小单元
- `feature_set_registry` / `feature_fact` 不能删:否则未来换 MERT/MuQ 会把 schema 写死
- `asset` 不能删:同一个 `song` 下会有多个真实音频文件
可以延后:
- `recording`
- `work`
- 更重的 `retrieval_index_registry`
- 更细的全链路审计表
因此推荐口径不是“把所有层都砍掉”,而是:
> **Phase-1 先上 song-centric 最小可用层;未来版本归属/cover/work 治理再继续加层。**
---
## 3. 角色视图
## 3.1 产品 / 架构角色
关注:
- 版权保护是否能最终定位到 `canonical_song_id`
- `recording``work` 的区别是否明确
- 当前阶段是否坚持“先冻结规范、后迭代模型”
- 各团队之间接口是否清晰
最该读:
- 本文
- [sota-evolution-guide.md](./sota-evolution-guide.md)
- [postgresql-data-model.md](./postgresql-data-model.md)
---
## 3.2 开发角色(后端 / 检索 / 数据)
关注:
- 如何把音频导入统一实体模型
- 如何切窗、建 feature_set、挂索引
- 如何从 query 走到候选,再归一到 `canonical_song_id`
- 如何支持未来切换 `model_name / model_version / feature_set`
最该读:
- 本文
- [postgresql-data-model.md](./postgresql-data-model.md)
---
## 3.3 运维 / 平台角色
关注:
- 离线任务:抽特征、建索引、重建索引
- 在线服务:召回、聚合、缓存、可观测性
- 存储分层:对象存储、PostgreSQL、索引后端
- 版本化:encoder 变更如何灰度、回滚、双写/双索引
最该读:
- 本文
- [postgresql-data-model.md](./postgresql-data-model.md)
- [phase1-worker-contract.md](./phase1-worker-contract.md)
---
## 3.4 模型底座 / 研究角色
关注:
- Phase-1 先不用微调时,选哪个开源 encoder
- 如何定义 feature_set:窗长、hop、pooling、layer selection
- 未来如何从 encoder-only 升级到 version/cover lane
- 如何让新模型接入而不破坏数据层
最该读:
- [sota-evolution-guide.md](./sota-evolution-guide.md)
- [production-encoder-freeze-and-embedding-strategy.md](./production-encoder-freeze-and-embedding-strategy.md)
- [postgresql-data-model.md](./postgresql-data-model.md)
---
## 4. 离线 / 在线职责拆分
```mermaid
flowchart LR
A[Offline\n数据治理/切窗/特征抽取/建索引] --> B[Registered Artifacts\nfeature_set / index / metadata]
B --> C[Online\nquery encode / retrieve / aggregate / decide]
```
### 离线职责
- 资产标准化
- 元数据归一
- 切窗
- 模型特征抽取
- fingerprint / embedding 建索引
- 回填 PostgreSQL 元数据
### 在线职责
- 接收 query
- query 切块 / 编码
- exact / semantic / version lane 召回
- recording/work/song 聚合
- 输出 `canonical_song_id` + 证据
---
## 5. 为什么必须把角色拆开
因为这个项目已经不是单一模型脚本,而是:
1. **数据治理系统**:谁的音频、属于哪个 recording/work/song
2. **检索系统**:如何从 query 找到候选
3. **判定系统**:最终输出哪一个 `canonical_song_id`
4. **服务系统**:如何对外提供 API 与可观测性
5. **演进系统**:底座模型会变,但数据规范不能跟着乱变
---
## 6. 当前阶段建议
### 当前最重要的不是继续改训练,而是:
1. 先把 PostgreSQL 数据规范稳定下来
2. 先把 `model_registry / feature_set_registry` 结构打稳
3. Phase-1 用开源 encoder 直接做 semantic lane baseline
4. 保留当前 ECAPA 作为历史 baseline / 对照组
### 当前系统中的保留项
- `Chromaprint`:保留
- `ECAPA baseline`:保留为对照组
- `melody rerank`:保留为补充 lane,不再作为主演进方向
### 当前系统中的升级项
- semantic lane 主 encoder -> foundation model
- pgvector 原型 schema -> 可扩展 PostgreSQL 数据模型
- 扁平 song_id -> canonical/work/recording/recording_asset/audio_window
---
## 7. 与代码的映射
| 代码/文档 | 当前角色 |
|---|---|
| `acr-engine/src/engines/chromaprint_matcher.py` | exact lane 原型 |
| `acr-engine/src/engines/ecapa_embedder.py` | current embedding lane baseline |
| `acr-engine/src/engines/hybrid_engine.py` | current aggregation prototype |
| `acr-engine/sql/pgvector_schema.sql` | 早期 pgvector prototype |
| `acr-engine/sql/acr_pg_schema_v2.sql` | 推荐的 PostgreSQL V2 schema |
| [postgresql-data-model.md](./postgresql-data-model.md) | V2 schema 设计说明 |
---
## 8. 阅读建议
如果你是:
- **架构负责人**:下一篇看 [sota-evolution-guide.md](./sota-evolution-guide.md)
- **数据/后端负责人**:下一篇看 [postgresql-data-model.md](./postgresql-data-model.md)
- **模型负责人**:先看 [sota-evolution-guide.md](./sota-evolution-guide.md) 再看 [production-encoder-freeze-and-embedding-strategy.md](./production-encoder-freeze-and-embedding-strategy.md)
# 模型与 Feature Set 初始化手册 / Model & Feature Registry Bootstrap
> 更新:2026-06-04
> 目标:给出 Phase-1 里 `model_registry`、`feature_set_registry`、`reference_set_registry` 的初始化约定,避免每次接入新 encoder 时重新设计。
## 一页结论
Phase-1 不微调底座时,真正需要初始化的不是“训练任务”,而是三类对象:
1. **模型定义**`model_registry`
2. **特征定义**`feature_set_registry`
3. **reference 集定义**`reference_set_registry`
也就是说,先把“你要怎么用模型”写清楚,再开始抽特征。
---
## 1. 推荐命名约定
## 1.1 model_name
推荐固定小写:
- `chromaprint`
- `mert`
- `muq`
- `ecapa`
- `coverhunter_encoder`(Phase-2+)
## 1.2 model_version
推荐表达清楚来源和规模:
- `v1-95m`
- `v1-330m`
- `large-msd-iter`
- `acr-baseline-v1`
## 1.3 feature set 命名
推荐格式:
```text
<model_name>__<feature_name>__<window>x<hop>__<pooling>__<metric>
```
示例:
- `mert__semantic_embedding__5s_2.5s__mean__cosine`
- `mert__semantic_embedding__10s_5s__mean__cosine`
- `muq__semantic_embedding__5s_2.5s__mean__cosine`
---
## 2. Phase-1 推荐初始化对象
## 2.1 模型清单
| model_name | model_version | 角色 |
|---|---|---|
| `chromaprint` | `v1` | exact lane |
| `mert` | `v1-95m` | semantic 主 baseline |
| `muq` | `large-msd-iter` | semantic challenger |
| `ecapa` | `acr-baseline-v1` | 历史 baseline / 对照 |
---
## 2.2 Feature set 清单
| feature_set | 目的 |
|---|---|
| `chromaprint asset-level` | exact 匹配 |
| `mert 5s/2.5s mean` | 主 semantic baseline |
| `mert 10s/5s mean` | 较长上下文验证 |
| `muq 5s/2.5s mean` | challenger baseline |
| `ecapa 5s/2.5s` | 历史对照 |
---
## 3. 推荐初始化 SQL
## 3.1 注册模型
```sql
insert into model_registry (
model_name, model_family, model_version, model_source, model_uri,
license_name, input_modality, input_sample_rate, input_channel_mode,
default_window_sec, default_hop_sec, output_embedding_dim,
pooling_supported, layer_selection_supported, is_trainable
) values
('chromaprint', 'fingerprint', 'v1', 'local', null,
null, 'audio', 16000, 'mono',
5.0, 2.5, null,
array['none'], false, false),
('mert', 'music_ssl', 'v1-95m', 'github', 'https://github.com/yizhilll/MERT',
null, 'audio', 24000, 'mono',
5.0, 2.5, 768,
array['mean','cls'], true, false),
('muq', 'music_ssl', 'large-msd-iter', 'github', 'https://github.com/tencent-ailab/MuQ',
null, 'audio', 24000, 'mono',
5.0, 2.5, 768,
array['mean','cls'], true, false),
('ecapa', 'speech_derived', 'acr-baseline-v1', 'local', null,
null, 'audio', 16000, 'mono',
5.0, 2.5, 192,
array['mean'], false, true);
```
---
## 3.2 注册 feature set
```sql
insert into feature_set_registry (
model_id, feature_name, feature_level, extraction_granularity,
window_sec, hop_sec, embedding_dim, pooling_strategy, layer_selection,
normalize_l2, distance_metric, quantization_type, feature_schema_version
)
select model_id, 'semantic_embedding', 'window', 'sliding_window',
5.0, 2.5, 768, 'mean', 'final',
true, 'cosine', 'none', 'v1'
from model_registry
where model_name = 'mert' and model_version = 'v1-95m';
insert into feature_set_registry (
model_id, feature_name, feature_level, extraction_granularity,
window_sec, hop_sec, embedding_dim, pooling_strategy, layer_selection,
normalize_l2, distance_metric, quantization_type, feature_schema_version
)
select model_id, 'semantic_embedding', 'window', 'sliding_window',
10.0, 5.0, 768, 'mean', 'final',
true, 'cosine', 'none', 'v1'
from model_registry
where model_name = 'mert' and model_version = 'v1-95m';
insert into feature_set_registry (
model_id, feature_name, feature_level, extraction_granularity,
window_sec, hop_sec, embedding_dim, pooling_strategy, layer_selection,
normalize_l2, distance_metric, quantization_type, feature_schema_version
)
select model_id, 'semantic_embedding', 'window', 'sliding_window',
5.0, 2.5, 768, 'mean', 'final',
true, 'cosine', 'none', 'v1'
from model_registry
where model_name = 'muq' and model_version = 'large-msd-iter';
```
---
## 3.3 注册 reference set
```sql
insert into reference_set_registry (
set_name, description, encoder_scope, status
) values (
'phase1_hot_reference_v1',
'Phase-1 主 reference 集,仅包含当前线上热参考 recording',
'mert-v1-95m / muq-large-msd-iter',
'active'
);
```
---
## 4. reference set 的运营原则
### 当前建议
- 一个时间点只允许一个主 `active` hot reference set
- 新 encoder / 新聚合策略上线时,新建 set,不覆盖旧 set
- A/B 或 shadow 期间,允许多个 set 并存,但只有一个主线上标记
### 为什么
这样可以支持:
- 线上回滚
- encoder 升级
- 索引热切换
- 离线重放
---
## 5. 维度扩展规则
当前 DDL 只演示了:
- `audio_embedding_vector_192`
- `audio_embedding_vector_768`
后续如果接入新 encoder 维度,如 `1024`
1. 新增 `audio_embedding_vector_1024`
2. 对应 feature_set 的 `embedding_dim=1024`
3. 独立建索引
4. 通过 `retrieval_index_registry` 切换
### 原则
- 维度变化是 feature set 升级,不是主数据模型升级
- 主数据层不该因 encoder 升级而改表
---
## 6. 当前推荐顺序
```mermaid
flowchart TD
A[注册 model_registry] --> B[注册 feature_set_registry]
B --> C[注册 reference_set_registry]
C --> D[抽取 embeddings/fingerprint]
D --> E[写 audio_embedding/audio_fingerprint]
E --> F[建 retrieval_index_registry]
```
---
## 7. 最后建议
如果你今天就开始做 Phase-1 初始化,建议最少先注册:
1. `chromaprint v1`
2. `mert v1-95m`
3. `muq large-msd-iter`
4. `mert 5s/2.5s mean`
5. `muq 5s/2.5s mean`
6. `phase1_hot_reference_v1`
这样数据、模型、索引三条线就都有了稳定入口。
---
## 10. Phase-1 worker contract(新增执行层)
当前已经不只是 registry/bootstrap 了,还补上了最小真实 worker 执行面:
- `acr-engine/scripts/bootstrap_phase1_reference_members_live.py`
- `acr-engine/workers/mark_job_status.py`
- `acr-engine/workers/run_chromaprint_job.py`
- `acr-engine/workers/run_embedding_job.py`
这层的作用不是立即跑完真实抽特征,而是先把下面这条链打通:
```text
planner -> feature_extraction_job -> worker -> PostgreSQL status update
```
### 当前能力
1. 读取 `feature_extraction_job`
2. 联表解析 `feature_set_registry + model_registry`
3. 解析 `target_scope`
4. 回写 `pending -> running -> completed`
5. 为后续真模型推理保留稳定契约
### 推荐阅读
详细契约与流程图见:
- [docs/phase1-worker-contract.md](./phase1-worker-contract.md)
---
## 8. live PostgreSQL bootstrap 脚本
为了避免每次手工执行 SQL,本仓库现在提供了一个可直接连 PostgreSQL 的 live bootstrap 脚本:
- `acr-engine/scripts/bootstrap_phase1_model_registry_live.py`
用途:
- 向目标 schema 写入 `model_registry`
- 写入 `feature_set_registry`
- 写入 `reference_set_registry`
- 采用 **幂等式 upsert / ensure** 方式,适合重复执行
### 8.1 执行命令
```bash
cd /workspace/acr-engine
/usr/local/miniconda3/bin/python scripts/bootstrap_phase1_model_registry_live.py \
--dsn 'postgres://d2:d2pass@127.0.0.1:5432/d2' \
--schema acr_test \
--output data/pgvector_eval/music20/phase1_registry_bootstrap_report.json
```
### 8.2 当前已验证结果(acr_test)
本轮已在 `acr_test` schema 上真实执行,写入结果如下:
| 对象 | 数量 |
|---|---:|
| `model_registry` | `5` |
| `feature_set_registry` | `6` |
| `reference_set_registry` | `2` |
其中新增的 Phase-1 对象包含:
#### models
- `chromaprint v1`
- `mert v1-95m`
- `muq large-msd-iter`
- `ecapa acr-baseline-v1`
#### feature sets
- `chromaprint fingerprint_asset`
- `mert semantic_embedding 5s/2.5s`
- `mert semantic_embedding 10s/5s`
- `muq semantic_embedding 5s/2.5s`
- `ecapa semantic_embedding 5s/2.5s`
#### reference set
- `phase1_hot_reference_v1`
### 8.3 当前产物
- `acr-engine/data/pgvector_eval/music20/phase1_registry_bootstrap_report.json`
- `acr-engine/data/pgvector_eval/music20/phase1_registry_bootstrap_idempotency_report.json`
这个文件已经记录了:
- model_id
- feature_set_id
- reference_set_id
- 最终表计数
因此,下次 session 不需要再从 SQL 片段手工执行开始,而可以直接从 live bootstrap 脚本接上。
### 8.4 幂等性验证(已做)
同一套命令在 `acr_test` schema 上连续执行两次后,已经拿到真实幂等性证据:
| 项目 | 第 1 次 | 第 2 次 |
|---|---:|---:|
| `model_registry` | `5` | `5` |
| `feature_set_registry` | `6` | `6` |
| `reference_set_registry` | `2` | `2` |
第二次执行时:
- `models` 全部表现为 `updated`
- `feature_sets` 全部表现为 `reused`
- `reference_set` 表现为 `updated`
结论:
> 当前 bootstrap 脚本可重复执行,不会把 Phase-1 registry 数据重复灌爆。
---
## 9. Phase-1 extraction job bootstrap
`model_registry / feature_set_registry / reference_set_registry` 都已经存在后,下一步不是立刻手工跑抽特征,而是先把 **待执行 job** 写到 `feature_extraction_job`
本仓库现在已经提供:
- `acr-engine/scripts/bootstrap_phase1_extraction_jobs_live.py`
用途:
- 根据已存在的 `feature_set_registry`
-`phase1_hot_reference_v1` 生成待执行 extraction jobs
- 把 Phase-1 的 exact / semantic lanes 统一放进 PostgreSQL job 表
### 9.1 执行命令
```bash
cd /workspace/acr-engine
/usr/local/miniconda3/bin/python scripts/bootstrap_phase1_extraction_jobs_live.py \
--dsn 'postgres://d2:d2pass@127.0.0.1:5432/d2' \
--schema acr_test \
--output data/pgvector_eval/music20/phase1_extraction_jobs_report.json
```
### 9.2 当前已验证结果(acr_test)
本轮已真实创建 5 条待执行 job:
| lane | model | feature | target_scope | status |
|---|---|---|---|---|
| exact | `chromaprint` | `fingerprint_asset` | `reference_set:phase1_hot_reference_v1` | `pending` |
| semantic | `mert` | `semantic_embedding` 5s/2.5s | `reference_set:phase1_hot_reference_v1` | `pending` |
| semantic | `mert` | `semantic_embedding` 10s/5s | `reference_set:phase1_hot_reference_v1` | `pending` |
| semantic | `muq` | `semantic_embedding` 5s/2.5s | `reference_set:phase1_hot_reference_v1` | `pending` |
| semantic | `ecapa` | `semantic_embedding` 5s/2.5s | `reference_set:phase1_hot_reference_v1` | `pending` |
对应 live 报告:
- `acr-engine/data/pgvector_eval/music20/phase1_extraction_jobs_report.json`
这意味着:
> 现在 PostgreSQL 里已经不只是“模型定义”和“特征定义”,而是连 **下一步该跑哪些抽特征任务** 都已经具备结构化入口了。
---
## 10. Phase-1 extraction plan(从 pending jobs 生成)
`feature_extraction_job` 已经存在后,下一步通常不是马上手敲命令,而是先从 PostgreSQL 生成一个**统一执行计划**
本仓库现在已经提供:
- `acr-engine/scripts/plan_phase1_extraction_jobs_live.py`
用途:
- 读取 `feature_extraction_job`
- 过滤 `job_status=pending`
- 联表 `feature_set_registry + model_registry`
- 生成按 lane / priority 排序的 execution plan
### 10.1 执行命令
```bash
cd /workspace/acr-engine
/usr/local/miniconda3/bin/python scripts/plan_phase1_extraction_jobs_live.py \
--dsn 'postgres://d2:d2pass@127.0.0.1:5432/d2' \
--schema acr_test \
--job-status pending \
--output data/pgvector_eval/music20/phase1_extraction_plan_report.json
```
### 10.2 当前已验证结果(acr_test)
本轮已真实生成一份 ordered execution plan:
| order | lane | model | feature | physical_target |
|---|---|---|---|---|
| 1 | `exact` | `chromaprint` | `fingerprint_asset` | `audio_fingerprint` |
| 2 | `semantic` | `mert` | `semantic_embedding 5s/2.5s` | `audio_embedding` |
| 3 | `semantic` | `mert` | `semantic_embedding 10s/5s` | `audio_embedding` |
| 4 | `semantic` | `muq` | `semantic_embedding 5s/2.5s` | `audio_embedding` |
| 5 | `semantic` | `ecapa` | `semantic_embedding 5s/2.5s` | `audio_embedding` |
其中 planner 还会自动给出:
- `vector_table`
- `audio_embedding_vector_768`
- `audio_embedding_vector_192`
- `target_scope`
- `execution_notes`
当前产物:
- `acr-engine/data/pgvector_eval/music20/phase1_extraction_plan_report.json`
结论:
> 现在 PostgreSQL 里已经不仅能描述“有哪些 job”,还可以直接生成**按执行顺序排好的抽特征计划**。
### 10.3 ready-to-run command suggestions(已补齐)
本轮又进一步把 planner 升级为:**每条 job 都生成 command suggestion**
示例:
#### exact lane
```bash
cd /workspace/acr-engine && PG_DSN="${PG_DSN:?set PG_DSN}" EXTRACTION_JOB_ID=1 FEATURE_SET_ID=2 TARGET_SCOPE='reference_set:phase1_hot_reference_v1' PG_SCHEMA=acr_test OUTPUT_TARGET=audio_fingerprint \
/usr/local/miniconda3/bin/python workers/run_chromaprint_job.py --complete-dry-run
```
#### semantic lane
```bash
cd /workspace/acr-engine && PG_DSN="${PG_DSN:?set PG_DSN}" EXTRACTION_JOB_ID=2 FEATURE_SET_ID=3 TARGET_SCOPE='reference_set:phase1_hot_reference_v1' PG_SCHEMA=acr_test MODEL_NAME=mert MODEL_VERSION=v1-95m VECTOR_TABLE=audio_embedding_vector_768 OUTPUT_TARGET=audio_embedding \
/usr/local/miniconda3/bin/python workers/run_embedding_job.py --complete-dry-run
```
这意味着下个 session 不需要先手工拼环境变量和 job 绑定关系,而可以直接从 planner 报告里复制命令模板。
### 10.4 planner 现在也会附带 validation commands
除了 per-job command suggestion,当前 planner 还会输出一组全局验证入口:
- `prereq_audit`
- `worker_contract_smoke`
- `semantic_vector_negative_matrix`
- `asset_level_upsert_validation`
也就是:
1. 先审计 host 前置条件
2. 再跑 exact+semantic 的 contract smoke
3. 再检查 semantic vector-table 负例是否稳定
4. 再验证 asset-level upsert contract
这让 planner 从“只会排任务”升级成“同时给出执行前检查入口”的交付物。
### 10.5 planner validation_commands 已做直接消费验证
本轮继续补了一层 fresh evidence:不是只看 planner JSON 里有命令,而是**直接从 `phase1_extraction_plan_report.json` 读取命令后执行**
对应产物:
- `acr-engine/data/pgvector_eval/music20/phase1_validation_commands_execution_report.json`
当前已验证:
- `validation_commands.prereq_audit` -> `returncode = 0`
- `validation_commands.worker_contract_smoke` -> `returncode = 0`
- `validation_commands.semantic_vector_negative_matrix` -> `returncode = 0`
- `validation_commands.asset_level_upsert_validation` -> `returncode = 0`
当前 `phase1_validation_commands_execution_report.json` 已经达到:
- `executed_commands = 4`
- `all_passed = true`
这说明 planner 报告现在不仅能“展示命令”,还可以被脚本化消费为真正的执行入口。
# Phase-1 实施清单 / Encoder-only Implementation Checklist
> 更新:2026-06-04
> 目标:把“先不上微调、先用开源 encoder”的 Phase-1 方案拆成可执行步骤,方便数据、检索、平台、运维团队并行推进。
## 一页结论
Phase-1 的交付目标不是“证明某个新模型绝对最优”,而是:
1.**PostgreSQL 主数据模型** 落稳
2.**reference 资产 / window / feature_set** 跑通
3.**MERT + MuQ** 建立 encoder-only baseline
4.**fingerprint lane + semantic lane** 的聚合链先跑通
5. 给 Phase-2 的 version/cover lane 留好接口
---
## 1. 交付范围
### 本阶段必须完成
- `canonical_song / work / recording / recording_asset / audio_window` 入库
- `model_registry / feature_set_registry` 初始化
- MERT/MuQ encoder-only 特征抽取
- hot reference set 建设
- semantic index 建设
- query -> candidate -> canonical_song 的基础闭环
### 本阶段不强求完成
- 底座微调
- cover 专项训练
- humming 专项 melody tower
- 全量冷数据统一进热索引
---
## 2. 角色分工
| 角色 | 主要交付 |
|---|---|
| 数据工程 | 资产清洗、去重、实体映射、切窗清单 |
| 后端/DBA | PostgreSQL DDL、索引、写入链、校验约束 |
| 检索工程 | fingerprint lane、semantic lane、聚合逻辑 |
| 模型工程 | MERT/MuQ 接入、feature_set 设计、抽特征脚本 |
| 平台/运维 | 离线任务编排、对象存储、热/冷索引治理 |
---
## 3. 分阶段 checklist
## Stage 1:主数据落库
### 目标
把业务事实层稳定下来,不依赖具体 encoder。
### Checklist
- [ ] 建库执行 `acr-engine/sql/acr_pg_schema_v2.sql`
- [ ] 初始化 `canonical_song`
- [ ] 初始化 `work`
- [ ] 初始化 `recording`
- [ ] 初始化 `recording_asset`
- [ ] 校验 lineage trigger 可用
- [ ] 用一小批 reference 数据做插入烟测
### 输出物
- PostgreSQL schema v2
- 初始实体数据
- 可复用的数据导入脚本
---
## Stage 2:reference 资产与切窗
### 目标
把“可被检索”的 reference 集合建出来。
### Checklist
- [ ] 选出 `is_reference=true` 的 recording
- [ ] 创建 `reference_set_registry`
- [ ] 回填 `reference_set_member`
- [ ] 统一标准化音频路径
- [ ] 生成 `audio_window`
- [ ] 标记 `active_for_index`
### 推荐规则
- 先只放主 reference 版本
- 默认先做 `5s / 2.5s hop`
- intro/outro 可先保留,后续再做 quality pruning
---
## Stage 3:模型与 feature_set 初始化
### 目标
把模型注册和特征版本定义稳定下来。
### Checklist
- [ ] 注册 `chromaprint`
- [ ] 注册 `mert v1-95m`
- [ ] 注册 `muq`
- [ ] 注册 `mert 5s/2.5s mean pool`
- [ ] 注册 `mert 10s/5s mean pool`
- [ ] 注册 `muq 5s/2.5s mean pool`
- [ ] 明确每个 feature_set 的 metric / quantization / dim
### 输出物
- `model_registry` 初始化数据
- `feature_set_registry` 初始化数据
- feature set 命名约定
---
## Stage 4:encoder-only 抽特征
### 目标
先不上训练,直接把 reference 集变成可检索 embedding。
### Checklist
- [ ] 抽取 MERT window embeddings
- [ ] 抽取 MuQ window embeddings
- [ ] 写入 `audio_embedding`
- [ ] 热数据写入 `audio_embedding_vector_768` 或对应物理表
- [ ] 冷数据落对象存储/parquet
- [ ] 回填 `is_indexed`
### 验证
- [ ] 随机抽样检查 `window -> embedding -> feature_set` 回链可用
- [ ] 检查向量 norm/缺失率/重复率
---
## Stage 5:索引与召回
### 目标
跑通 semantic lane 与 exact lane 的双路召回。
### Checklist
- [ ] 建 fingerprint index
- [ ] 建 semantic index
- [ ] 回填 `retrieval_index_registry`
- [ ] 做 query encode
- [ ] 返回 `retrieval_candidate`
- [ ] 聚合到 `recording / work / canonical_song`
- [ ]`phase1_prereq_audit`
- [ ]`phase1_worker_contract_smoke`
- [ ]`semantic_vector_negative_matrix`
- [ ]`asset_level_upsert_validation`
### 第一版聚合建议
- max score
- top-k average
- hit windows count
- exact lane / semantic lane agreement bonus
---
## Stage 6:基础评测与上线门禁
### 目标
先证明 Phase-1 结构可用。
### Checklist
- [ ] exact query bucket
- [ ] noisy/BGM bucket
- [ ] version-like bucket(即便暂时不训练 cover lane)
- [ ] Top1/Top3/MRR
- [ ] canonical_song recall
- [ ] work-level recall
- [ ] reference set 版本记录
---
## 4. 推荐时间顺序
```mermaid
flowchart TD
A[Schema v2 落库] --> B[实体导入]
B --> C[reference set 初始化]
C --> D[audio_window 生成]
D --> E[model/feature_set 初始化]
E --> F[MERT/MuQ 抽特征]
F --> G[semantic index]
C --> H[fingerprint index]
G --> I[candidate aggregation]
H --> I
I --> J[Phase-1 benchmark]
```
---
## 5. 第一版验收标准
### 数据层
- 能稳定插入 `canonical_song -> work -> recording -> recording_asset -> audio_window`
- 能支撑至少一套 `reference_set`
### 模型/特征层
- 能并行存在多个 `model_registry / feature_set_registry`
- 能跑通 MERT/MuQ encoder-only 抽特征
### 检索层
- 能同时返回 fingerprint lane 与 semantic lane 候选
- 能聚合输出 `canonical_song_id`
### 运维层
- 能重建 reference set
- 能重建 semantic index
- 能记录 feature_set 与 index version
---
## 6. 本阶段容易踩的坑
1. 先把 embedding 存储设计死到某个模型维度
2. 只保留 song_id,不保留 work/recording
3. reference set 没有版本化
4. query 结果无法回查具体 evidence window
5. exact lane 被过早删除
---
## 7. 当前建议结论
如果你要马上排计划,建议按这个优先级:
1. Schema v2 与主数据导入
2. reference set + audio_window
3. MERT/MuQ feature_set 初始化
4. encoder-only 抽特征
5. 双路召回与聚合
6. benchmark 与门禁
## 6.1 当前 planner 已提供的 validation entrypoints
`acr-engine/scripts/plan_phase1_extraction_jobs_live.py` 现在除了 job 级 `command_suggestions`,还会在 `phase1_extraction_plan_report.json` 里附带:
- `validation_commands.prereq_audit`
- `validation_commands.worker_contract_smoke`
- `validation_commands.semantic_vector_negative_matrix`
- `validation_commands.asset_level_upsert_validation`
这意味着下次启动时可以先跑“全局验证入口”,再决定是否执行具体 job,而不必手工拼测试命令。
## 6.2 当前推荐的一键验证入口
如果只是想先确认当前 host 是否具备继续推进 Phase-1 的条件,推荐优先执行:
```bash
cd /workspace/acr-engine
/usr/local/miniconda3/bin/python scripts/run_planner_validation_commands_live.py --dsn 'postgres://d2:d2pass@127.0.0.1:5432/d2' --output data/pgvector_eval/music20/planner_validation_commands_runner_report.json
```
它会直接读取 `phase1_extraction_plan_report.json``validation_commands`,并批量执行:
- `prereq_audit`
- `worker_contract_smoke`
- `semantic_vector_negative_matrix`
- `asset_level_upsert_validation`
当前 live 结果:
- `executed_count = 4`
- `all_passed = true`
# Phase-1 Worker Contract / 作业执行器契约
> 更新:2026-06-04
> 目标:把 Phase-1 从“只有 registry / plan”推进到“worker 可以真实消费 PostgreSQL 作业并更新状态”。
---
## 一页结论
当前 Phase-1 已经具备一条最小真实执行链:
1. planner 从 `feature_extraction_job` 读 pending jobs
2. worker 读取 `extraction_job_id`
3. worker 联表解析 `feature_set_registry + model_registry`
4. worker 解析 `target_scope`
5. worker 回写 `feature_extraction_job.job_status / input_count / output_count / metadata_json`
也就是说,现在 PostgreSQL 不只是“数据字典”,已经开始承担:
- 作业编排面
- 状态机面
- 执行证据面
---
## 1. 当前落地的 worker
位于:
- `acr-engine/scripts/bootstrap_phase1_reference_members_live.py`
- `acr-engine/workers/mark_job_status.py`
- `acr-engine/workers/run_chromaprint_job.py`
- `acr-engine/workers/run_embedding_job.py`
- `acr-engine/workers/_job_common.py`
### 角色划分
| worker | 作用 |
|---|---|
| `mark_job_status.py` | 通用状态推进器 |
| `run_chromaprint_job.py` | exact lane worker |
| `run_embedding_job.py` | semantic lane worker |
| `_job_common.py` | 共享的 job 读取、scope 解析、状态回写逻辑 |
### 配套 bootstrap
为了让 worker 不再面对空 scope,这轮还补上了:
- `acr-engine/scripts/bootstrap_phase1_reference_members_live.py`
它会把当前 `recording.is_reference = true` 的录音挂到:
- `phase1_hot_reference_v1`
这样 worker 可以真实看到:
- `recording_count`
- `ready_asset_count`
- `active_window_count`
---
## 2. 当前状态机
```mermaid
flowchart LR
A[pending] --> B[running]
B --> C[completed]
B --> D[failed]
```
### 当前已验证的状态流转
- `pending -> running`
- `running -> completed`(dry-run 模式)
### 当前状态保护
- worker 认领 job 时要求前置状态为 `pending`
- worker 完成 job 时要求前置状态为 `running`
- `mark_job_status.py` 只接受:
- `pending`
- `running`
- `completed`
- `failed`
- `finished_at` 只在首次完成时落值,不再被重复覆盖
### 已验证的 guard 行为
当前已真实验证:
1. 同一 chromaprint job 第一次 dry-run:
- 成功 `pending -> running -> completed`
2. 不做 reset,直接第二次执行同一 job:
- 被前置状态保护拒绝
对应证据:
- `acr-engine/data/pgvector_eval/music20/phase1_worker_double_claim_guard_report.json`
### 设计意图
先把 **作业契约与状态流转** 固定住,再把真正的模型推理塞进去。
这样后续不管换成:
- `Chromaprint`
- `MERT`
- `MuQ`
- `CoverHunter encoder`
都不需要重做 orchestration 数据结构。
---
## 3. worker 输入契约
### 环境变量
| 变量 | 说明 |
|---|---|
| `PG_DSN` | PostgreSQL 连接串 |
| `PG_SCHEMA` | 目标 schema |
| `EXTRACTION_JOB_ID` | 要执行的作业 id |
| `FEATURE_SET_ID` | 规划时附带,worker 可用于一致性检查 |
| `TARGET_SCOPE` | 规划时附带,worker 当前以 DB 中 job 记录为准 |
| `MODEL_NAME` | embedding worker 用于防错 |
| `MODEL_VERSION` | embedding worker 用于防错 |
| `VECTOR_TABLE` | embedding worker 目标向量表 |
| `OUTPUT_TARGET` | `audio_fingerprint``audio_embedding` |
### CLI 参数
三个 worker 都支持显式 CLI 参数覆盖 env。
### planner 命令模板的当前约定
`plan_phase1_extraction_jobs_live.py` 现在会显式生成:
```bash
cd /workspace/acr-engine && PG_DSN="${PG_DSN:?set PG_DSN}" ...
```
这样复制命令时,如果调用方忘了提供数据库连接串,会立刻失败,而不是静默跑空。
当前 planner 还会显式使用:
```bash
/usr/local/miniconda3/bin/python
```
原因是当前环境里 `python` 不在 PATH 上,但这个解释器路径已被验证可用。
对于当前 dry-run worker,planner 的主命令模板也会显式带上:
```bash
--complete-dry-run
```
这样 `primary_command` 就能直接复现:
```text
pending -> running -> completed
```
---
## 4. PostgreSQL 读取契约
worker 当前真实读取:
1. `feature_extraction_job`
2. `feature_set_registry`
3. `model_registry`
4. `reference_set_registry` / `reference_set_member`
5. `recording_asset`
6. `audio_window`
### 为什么要读 scope summary
因为 Phase-1 第一阶段的核心不是“立刻抽出 embedding”,而是先确定:
- 这次 job 面向哪个 reference set
- 涉及多少 recording
- 涉及多少 ready asset
- 涉及多少 active window
这样后续做:
- 分片
- 并行
- 重试
- SLA 估算
才有稳定基线。
---
## 5. 当前 dry-run 的真实意义
当前 worker 还没有真正调用模型做特征提取;它做的是:
1. 验证 planner 命令模板可被真实消费
2. 验证 job -> feature_set -> model 的 join 契约
3. 验证 target scope 解析
4. 验证 PostgreSQL 作业状态回写
5. 为下一步真推理保留稳定入口
所以它不是假文档,而是:
> **先把工业执行面的骨架打通,再把模型推理填进去。**
---
## 6. 推荐执行顺序
```mermaid
flowchart TD
A[bootstrap model/feature/reference registry] --> B[bootstrap feature_extraction_job]
B --> C[plan pending jobs]
C --> D[run worker dry-run]
D --> E[validate status transitions]
E --> F[replace dry-run with real extractor]
```
---
## 7. exact lane 与 semantic lane 的后续替换点
### 7.1 Chromaprint worker
后续把下面逻辑塞进 `run_chromaprint_job.py`
1. 读取 `recording_asset`
2. 读取可用音频并提取 exact-lane hash
3. 写 artifact JSON
4.`audio_fingerprint`
5. 更新 `output_count`
6. 标记 `completed`
### 当前 exact lane 的真实状态
这轮已经把 `run_chromaprint_job.py` 从“只有 dry-run”推进到:
- 如果 source audio 可读:
- 生成 repo-local chromaprint-style hash artifact
- 写入 `audio_fingerprint`
- 如果 source audio 不可读:
- 明确把 job 标记为 `failed`
-`failure_reason``missing_asset_count``missing_asset_samples` 写回 PostgreSQL
### 当前失败语义
当前 exact lane 采用的是 **全量成功 / 否则失败**
- 只要 scope 内任意 asset:
- 缺文件
- 解码失败
- hash 提取失败
就整体标记:
- `job_status = failed`
- `failure_reason = unreadable_audio_assets`
这样不会把“部分成功”伪装成 `completed`
### 当前依赖策略
当前 exact lane 不再强依赖 `librosa`
- 优先使用 `librosa`(如果环境里存在)
- 否则回退到:
- Python `wave`
- `numpy` 线性重采样
- `numpy` FFT spectrogram
这使得 worker contract 能在更瘦的运行环境里继续工作。
### 当前幂等保护
`audio_fingerprint` 现在补了:
- `UNIQUE(feature_set_id, asset_id)`
对应 worker 写入改成:
- `INSERT ... ON CONFLICT DO UPDATE`
因此 exact lane 对同一 `(feature_set_id, asset_id)` 的重复写入不再依赖应用层先查再写。
### 7.2 Embedding worker
`run_embedding_job.py` 现在已经不再只是简单 dry-run。当前它已经具备:
1. 真实读取 `reference_set -> audio_window -> recording_asset` scope
2. 真实检查目标向量表是否存在且与维度匹配
3. 真实检查模型 runtime 依赖是否齐全
4. 真实检查 source audio 是否存在
5. 把 blocker 明确写回 `feature_extraction_job.metadata_json`
6. 在 blocker 存在时把 job 诚实标记为 `failed`
### 当前失败语义
semantic lane 当前采用的是 **preflight all-or-nothing**
- 只要 scope 内音频路径不可达 / 文件不存在,记为:
- `unreadable_audio_assets`
- 只要模型 runtime 依赖导入不满足,记为:
- `model_runtime_unavailable`
- 只要目标向量表非法 / 缺失 / 维度不匹配,记为对应 blocker
worker 会把这些 blocker 聚合到:
- `failure_reason = preflight_failed`
- `preflight_blockers = [...]`
这样不会把“模型没法跑”误写成 completed,也不会只暴露第一个错误。
### 当前 vector table 负例证据
除了正常 `audio_embedding_vector_768` 存在性校验外,本轮还对 semantic lane 补了 3 类 live 负例:
- `audio_embedding_vector_192` -> `vector_table_dim_mismatch`
- `audio_embedding_vector_1024` -> `vector_table_not_allowlisted`
- 缺失 `audio_embedding_vector_768` 的隔离 schema -> `vector_table_missing_in_schema`
对应产物:
- `acr-engine/scripts/run_embedding_vector_table_negative_matrix_live.py`
- `acr-engine/data/pgvector_eval/music20/embedding_vector_table_negative_matrix_report.json`
这说明 semantic worker 当前不只是会在“环境缺依赖”时失败,也能把 **配置错误的向量表** 精确落账。
### 当前 live 证据
MERT 5s/2.5s job (`extraction_job_id=2`) 在 `acr_test` 上已经真实验证:
- `scope_window_count = 20`
- `job_status = failed`
- `output_count = 0`
- `preflight_blockers = ['unreadable_audio_assets', 'model_runtime_unavailable']`
- `runtime_report.missing_dependencies = ['torch', 'torchaudio', 'transformers']`
- `audio_embedding_vector_768` 已通过存在性与维度校验
对应产物:
- `acr-engine/data/pgvector_eval/music20/phase1_worker_embedding_write_attempt.json`
- `acr-engine/data/pgvector_eval/music20/phase1_worker_embedding_write_guard_report.json`
- `acr-engine/data/pgvector_eval/music20/phase1_worker_embedding_post_state.json`
### 当前幂等保护
为了服务后续真正的 window embedding upsert,`audio_embedding` 现在补了两条唯一键:
- `UNIQUE(feature_set_id, window_id) WHERE window_id IS NOT NULL`
- `UNIQUE(feature_set_id, asset_id) WHERE window_id IS NULL AND asset_id IS NOT NULL`
这让后续真实 encoder 接入后可以直接做:
- window 级 embedding upsert
- asset 级 embedding upsert
而不需要先查再写。
当前这两条唯一键里,asset-level 路径也已经有 live 证据:
- `scripts/validate_audio_embedding_asset_upsert_live.py`
- `audio_embedding_asset_upsert_live_report.json`
已验证:
- 重复 `INSERT` 会被 `uq_audio_embedding_feature_asset` 拒绝
- `ON CONFLICT ... DO UPDATE` 会复用同一个 `embedding_id`
- `audio_embedding` / `audio_embedding_vector_192` 行数都保持为 `1`
### 下一步替换点
当 runtime 与音频挂载到位后,只需要把 guarded failure path 替换成真实 inference:
1. 加载 `MERT` / `MuQ` / `ECAPA`
2. 提取向量
3.`audio_embedding`
4.`audio_embedding_vector_<dim>`
5. 更新 `output_count`
6. 标记 `completed`
也就是说,**PostgreSQL worker contract 已经固定,下一步换的是 encoder adapter,不是 orchestration 结构。**
---
## 8. 解决了什么问题
这次 worker contract 落地,主要解决了 4 个问题:
1. **planner 不再只是纸面计划**
2. **job status 有了真实推进器**
3. **后续换模型不用重做 orchestration**
4. **可以先 dry-run 验证执行链,再接入重模型**
---
## 9. 当前边界
当前还没有完成的部分:
- exact lane 虽已有真实写入路径,但当前 live 环境仍被 `/workspace/downloads` 缺失阻塞
- semantic lane 已有真实 preflight failure contract,但还没有接上真正的 `MERT / MuQ / ECAPA` inference adapter
- `failed` 重试策略
- job 分片执行器
- 更完整的 embedding artifact / checksum 治理策略
但现在已经足够支撑下一阶段:
> **把真实 extractor 接到已经验证过的 PostgreSQL worker contract 上。**
# PostgreSQL DB Schema Samples / 融合优先 DDL 草案与查询样例
# PostgreSQL Schema Samples / song-centric 4 表 DDL 与样例
> 更新:2026-06-04
> 目标:把当前 **song-centric + 融合优先** 设计落成一版可以直接评审和继续实现的 PostgreSQL DDL 草案。
> 更新:2026-06-04
> SQL 文件:[`acr-engine/sql/acr_pg_schema_songcentric_v1.sql`](../acr-engine/sql/acr_pg_schema_songcentric_v1.sql)
> live smoke:[`acr-engine/scripts/smoke_songcentric_schema_live.py`](../acr-engine/scripts/smoke_songcentric_schema_live.py)
---
## 一页结论
## 1. 一页结论
当前默认物理模型只看 4 张表
当前默认物理模型:
```text
media_entity -> audio_object -> feature_fact -> set_membership
```
对应逻辑语义:
当前默认逻辑语义:
```text
song -> asset -> window -> fingerprint / embedding
```
其中:
- `media_entity`:当前默认只承载 `song`
- `audio_object`:统一承载 `asset``window`
- `feature_fact`:统一承载 `fingerprint``embedding`
- `set_membership`:统一承载 `reference / hot / eval` 等集合关系
- `audio_object` 统一承载原始音频和切片
- `feature_fact` 统一承载 exact/semantic 特征
- `set_membership` 统一承载 reference/eval/hot 集关系
---
## 1. 4 张表分别存什么
## 2. 切片 / 模型 / feature 落在哪张表
| 表 | 当前主要 type | 存什么 | 为什么存在 |
| 对象 | 表 | 关键字段 | 示例 |
|---|---|---|---|
| `media_entity` | `song` | 歌曲主实体 | 最终归属对象是 `song_id` |
| `audio_object` | `asset`, `window` | 原始音频文件 + 切片 | 同一个 song 下可有多个音频,切片仍需 evidence |
| `feature_fact` | `fingerprint`, `embedding` | 模型、feature set、特征结果 | 统一 exact/semantic 特征事实 |
| `set_membership` | `reference_set`, `eval_set`, `hot_set` | 谁属于哪个集合 | 管理 reference 与评测范围 |
| song | `media_entity` | `entity_type='song'` | `song_000001` |
| asset | `audio_object` | `object_type='asset'` | 一首歌的原始 wav/mp3/flac |
| window | `audio_object` | `object_type='window'` | `0-5000ms`, `2500-7500ms` |
| fingerprint | `feature_fact` | `feature_type='fingerprint'` | chromaprint |
| embedding | `feature_fact` | `feature_type='embedding'` | MERT/MuQ/fallback vector |
| model | `feature_fact` | `model_name`, `model_version` | `mert-v1-95m`, `muq-base`, `local_wavehash_embed` |
| feature set | `feature_fact` | `feature_set_name`, `feature_schema_ver` | `mert_5s_hop2.5_v1` |
---
## 2. 当前推荐 DDL 草案
## 3. DDL
### 2.1 `media_entity`
### 3.1 `media_entity`
```sql
create table if not exists media_entity (
......@@ -62,16 +62,9 @@ create table if not exists media_entity (
constraint fk_media_entity_parent
foreign key (parent_entity_id) references media_entity(entity_id)
);
create unique index if not exists uq_media_entity_song_biz_key
on media_entity(entity_type, biz_key)
where biz_key is not null;
create index if not exists idx_media_entity_root_song
on media_entity(root_song_id);
```
### 2.2 `audio_object`
### 3.2 `audio_object`
```sql
create table if not exists audio_object (
......@@ -99,23 +92,9 @@ create table if not exists audio_object (
or (object_type = 'window' and parent_object_id is not null)
)
);
create index if not exists idx_audio_object_song_type
on audio_object(song_id, object_type);
create index if not exists idx_audio_object_parent
on audio_object(parent_object_id);
create unique index if not exists uq_audio_object_asset_checksum
on audio_object(song_id, checksum)
where object_type = 'asset' and checksum is not null;
create unique index if not exists uq_audio_object_window_range
on audio_object(parent_object_id, start_ms, end_ms)
where object_type = 'window';
```
### 2.3 `feature_fact`
### 3.3 `feature_fact`
```sql
create table if not exists feature_fact (
......@@ -142,23 +121,9 @@ create table if not exists feature_fact (
or (feature_type = 'embedding' and (embedding_uri is not null or vector_table_name is not null))
)
);
create index if not exists idx_feature_fact_object_type
on feature_fact(object_id, feature_type);
create index if not exists idx_feature_fact_song_type
on feature_fact(song_id, feature_type);
create unique index if not exists uq_feature_fact_embedding
on feature_fact(object_id, model_name, model_version, feature_set_name, feature_type)
where feature_type = 'embedding';
create unique index if not exists uq_feature_fact_fingerprint
on feature_fact(object_id, model_name, model_version, feature_set_name, feature_type)
where feature_type = 'fingerprint';
```
### 2.4 `set_membership`
### 3.4 `set_membership`
```sql
create table if not exists set_membership (
......@@ -174,285 +139,245 @@ create table if not exists set_membership (
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create unique index if not exists uq_set_membership_unique
on set_membership(set_type, set_name, member_type, member_id);
create index if not exists idx_set_membership_set_lookup
on set_membership(set_type, set_name, is_active, priority);
```
---
## 3. 切片 / 模型 / feature 到底落哪张表
| 对象 | 落表 | 关键字段 |
|---|---|---|
| song | `media_entity` | `entity_type='song'` |
| 原始音频 | `audio_object` | `object_type='asset'` |
| 切片窗口 | `audio_object` | `object_type='window'`, `parent_object_id=<asset_id>` |
| 指纹特征 | `feature_fact` | `feature_type='fingerprint'` |
| embedding 特征 | `feature_fact` | `feature_type='embedding'` |
| 模型名/版本 | `feature_fact` | `model_name`, `model_version` |
| feature set | `feature_fact` | `feature_set_name`, `feature_schema_ver` |
| reference 集归属 | `set_membership` | `set_type='reference_set'` |
---
## 4. 流程图
## 4. 典型写入流程图
### 4.1 落库流程
### 4.1 表写入顺序
```mermaid
flowchart TD
A[media_entity\nentity_type=song] --> B[audio_object\nobject_type=asset]
B --> C[audio_object\nobject_type=window]
C --> D1[feature_fact\nfeature_type=fingerprint]
C --> D2[feature_fact\nfeature_type=embedding]
B --> E[set_membership\nreference_set]
A[insert song] --> B[insert asset]
B --> C[insert windows]
C --> D1[insert fingerprint facts]
C --> D2[insert embedding facts]
A --> E[insert set_membership]
B --> E
C --> E
```
### 4.2 查询回溯流程
### 4.2 查询回溯顺序
```mermaid
flowchart LR
A[feature_fact] --> B[audio_object window]
B --> C[audio_object asset]
C --> D[media_entity song]
A[query features] --> B[feature_fact]
B --> C[window]
C --> D[asset]
D --> E[song]
```
### 4.3 写入时序图
```mermaid
sequenceDiagram
participant ING as Ingest/Extract Job
participant DB as PostgreSQL
ING->>DB: insert media_entity(song)
ING->>DB: insert audio_object(asset)
ING->>DB: insert audio_object(window)
ING->>DB: insert feature_fact(fingerprint)
ING->>DB: insert feature_fact(embedding)
ING->>DB: insert set_membership(reference_set)
```
---
## 5. 样例数据
### 4.4 Phase-1 bootstrap 流程
### 5.1 写 song
```mermaid
flowchart TD
A[bootstrap_songcentric_phase1_live.py] --> B[media_entity song x N]
B --> C[audio_object asset x N]
C --> D[audio_object window x N]
D --> E1[feature_fact fingerprint x N]
D --> E2[feature_fact embedding x N]
C --> F[set_membership reference_set x N]
```sql
insert into media_entity (
entity_type, biz_key, title, artist_name, metadata_json
) values (
'song', 'song_000001', 'Song Alpha', 'Artist A',
'{"source":"catalog_import","language":"zh"}'::jsonb
)
returning entity_id;
```
当前 live bootstrap 脚本:[`acr-engine/scripts/bootstrap_songcentric_phase1_live.py`](../acr-engine/scripts/bootstrap_songcentric_phase1_live.py)
### 5.2 写 asset
### 4.5 Manifest 导入流程
```mermaid
flowchart TD
A[songcentric_manifest_sample.jsonl] --> B[import_songcentric_manifest_live.py]
B --> C[media_entity song]
B --> D[audio_object asset]
B --> E[audio_object window x N]
B --> F[feature_fact]
B --> G[set_membership]
```sql
insert into audio_object (
object_type, song_id, source_type, storage_uri, storage_scheme,
checksum, codec, sample_rate, channels, duration_ms, metadata_json
) values (
'asset', :song_id, 'catalog_master',
's3://bucket/song_alpha/clip1.wav', 's3',
'sha256:asset001', 'wav', 44100, 2, 183000,
'{"uploader":"pipeline_v1"}'::jsonb
)
returning object_id;
```
当前样例 manifest:[`acr-engine/data/pgvector_eval/music20/songcentric_manifest_sample.jsonl`](../acr-engine/data/pgvector_eval/music20/songcentric_manifest_sample.jsonl)
当前导入脚本:[`acr-engine/scripts/import_songcentric_manifest_live.py`](../acr-engine/scripts/import_songcentric_manifest_live.py)
### 5.3 写 window
当前带 feature 样例 manifest:[`acr-engine/data/pgvector_eval/music20/songcentric_feature_manifest_sample.jsonl`](../acr-engine/data/pgvector_eval/music20/songcentric_feature_manifest_sample.jsonl)
### 4.6 真实目录生成 manifest 流程
```mermaid
flowchart TD
A[real audio directory] --> B[build_songcentric_manifest_from_directory.py]
B --> C[songcentric_directory_manifest.jsonl]
C --> D[import_songcentric_manifest_live.py]
D --> E[media_entity]
D --> F[audio_object]
D --> G[set_membership]
```sql
insert into audio_object (
object_type, song_id, parent_object_id,
start_ms, end_ms, duration_ms, metadata_json
) values
('window', :song_id, :asset_id, 0, 5000, 5000, '{"hop_ms":2500}'::jsonb),
('window', :song_id, :asset_id, 2500, 7500, 5000, '{"hop_ms":2500}'::jsonb)
returning object_id;
```
当前目录构建脚本:[`acr-engine/scripts/build_songcentric_manifest_from_directory.py`](../acr-engine/scripts/build_songcentric_manifest_from_directory.py)
### 4.7 真实目录补特征再导入流程
### 5.4 写 fingerprint
```mermaid
flowchart TD
A[real audio directory] --> B[build_songcentric_manifest_from_directory.py]
B --> C[songcentric_directory_manifest.jsonl]
C --> D[enrich_songcentric_manifest_with_local_features.py]
D --> E[songcentric_directory_manifest_with_features.jsonl]
E --> F[import_songcentric_manifest_live.py]
F --> G[feature_fact]
```sql
insert into feature_fact (
feature_type, object_id, song_id,
model_name, model_version, feature_set_name, feature_schema_ver,
fingerprint_value, checksum, metadata_json
) values (
'fingerprint', :window_id, :song_id,
'chromaprint', '1.0', 'chromaprint_5s_v1', 'v1',
'AQAAE0mUaEkSZSo...', 'sha256:fp001',
'{"lane":"exact"}'::jsonb
);
```
当前特征补全脚本:[`acr-engine/scripts/enrich_songcentric_manifest_with_local_features.py`](../acr-engine/scripts/enrich_songcentric_manifest_with_local_features.py)
### 4.8 目录链中的 exact lane 提升
### 5.5 写 embedding
当前 `enrich_songcentric_manifest_with_local_features.py` 已优先复用仓库内 `ChromaprintMatcher` 生成 fingerprint;只有失败时才回退到 `local_wavehash`
本轮 fresh evidence:
- `wav_windows_seen = 5`
- `matcher_fingerprint_count = 5`
- `fallback_fingerprint_count = 0`
这说明当前目录链里的 exact lane 已经不只是临时 hash,而是优先接上了仓库现有 fingerprint 提取能力。
### 4.9 目录链中的 semantic lane 运行时选择
当前 `enrich_songcentric_manifest_with_local_features.py` 对 semantic lane 采用 **runtime-aware** 选择:
- 如果 `torch / torchaudio / transformers` 可用,则预留真实 semantic adapter 入口
- 如果不可用,则明确落到 `local_wavehash_embed` fallback,并把缺失依赖写进 metadata/report
本轮 fresh evidence:
- `semantic_runtime_available = false`
- `semantic_runtime_missing = ["torch", "torchaudio", "transformers"]`
- `semantic_fallback_count = 5`
这说明当前 host 上 semantic lane 还未接真实模型,但链路已经具备明确的运行时分流与可审计证据。
### 4.10 一键 song-centric 目录链 runner
```mermaid
flowchart TD
A[run_songcentric_directory_pipeline_live.py] --> B[build manifest]
B --> C[enrich features]
C --> D[import manifest]
D --> E[runner report]
```sql
insert into feature_fact (
feature_type, object_id, song_id,
model_name, model_version, feature_set_name, feature_schema_ver,
embedding_dim, embedding_uri, vector_table_name, checksum, metadata_json
) values (
'embedding', :window_id, :song_id,
'mert-v1-95m', 'hf-main', 'mert_5s_hop2.5_v1', 'v1',
768, 's3://bucket/embeddings/song_alpha_win0001.npy', 'audio_embedding_vector_768',
'sha256:emb001', '{"lane":"semantic"}'::jsonb
);
```
当前 runner:[`acr-engine/scripts/run_songcentric_directory_pipeline_live.py`](../acr-engine/scripts/run_songcentric_directory_pipeline_live.py)
### 5.6 写 set membership
它会在一条命令里输出:
- 目录扫描结果
- exact lane 是否走 `ChromaprintMatcher`
- semantic lane 是否 runtime-ready
- live PostgreSQL 导入后的计数与 lineage 样例
```sql
insert into set_membership (
set_type, set_name, member_type, member_id, song_id, priority, metadata_json
) values
('reference_set', 'phase1_hot_reference_v1', 'song', :song_id, :song_id, 100, '{}'::jsonb),
('reference_set', 'phase1_hot_reference_v1', 'asset', :asset_id, :song_id, 100, '{}'::jsonb),
('reference_set', 'phase1_hot_reference_v1', 'window', :window_id, :song_id, 100, '{}'::jsonb);
```
---
## 5. 最常用 SQL 样例
## 6. 典型查询
### 5.1 写一首歌
### 6.1 查看某首歌有哪些 asset
```sql
insert into media_entity (entity_type, biz_key, title, artist_name)
values ('song', 'song-10001', 'Song 10001', 'Artist A')
returning entity_id;
select object_id, storage_uri, checksum, duration_ms
from audio_object
where song_id = :song_id
and object_type = 'asset'
order by object_id;
```
### 5.2 写一个 asset
### 6.2 查看某个 asset 切了哪些 window
```sql
insert into audio_object (
object_type, song_id, source_type, storage_uri, storage_scheme,
checksum, codec, sample_rate, channels, duration_ms
) values (
'asset', :song_id, 'official', 's3://bucket/song10001/master.wav', 's3',
'sha256:xxx', 'wav', 44100, 2, 215000
) returning object_id;
select object_id, start_ms, end_ms, duration_ms
from audio_object
where parent_object_id = :asset_id
and object_type = 'window'
order by start_ms;
```
### 5.3 写一个 window
### 6.3 查看某个 window 被哪些模型编码过
```sql
insert into audio_object (
object_type, song_id, parent_object_id, start_ms, end_ms, duration_ms
) values (
'window', :song_id, :asset_id, 30000, 35000, 5000
) returning object_id;
select feature_type, model_name, model_version, feature_set_name, embedding_dim,
fingerprint_value, embedding_uri, vector_table_name
from feature_fact
where object_id = :window_id
order by feature_type, model_name, model_version;
```
### 5.4 写一条 embedding
### 6.4 从 feature 回查 song
```sql
insert into feature_fact (
feature_type, object_id, song_id,
model_name, model_version, feature_set_name,
feature_schema_ver, embedding_dim, embedding_uri, vector_table_name
) values (
'embedding', :window_id, :song_id,
'mert', 'v1-95m', 'mert_5s_hop2.5_meanpool',
'v1', 768, 's3://bucket/emb/song10001_win0001.npy', 'audio_embedding_vector_768'
);
select ff.feature_id,
ff.feature_type,
ff.model_name,
w.object_id as window_id,
w.start_ms,
w.end_ms,
a.object_id as asset_id,
a.storage_uri,
s.entity_id as song_id,
s.title,
s.artist_name
from feature_fact ff
join audio_object w
on w.object_id = ff.object_id
and w.object_type = 'window'
join audio_object a
on a.object_id = w.parent_object_id
and a.object_type = 'asset'
join media_entity s
on s.entity_id = ff.song_id
where ff.feature_id = :feature_id;
```
### 5.5 把 asset 挂到 reference 集
### 6.5 查询 reference set 中的全部 window
```sql
insert into set_membership (
set_type, set_name, member_type, member_id, song_id, priority
) values (
'reference_set', 'phase1_hot_reference_v1', 'asset', :asset_id, :song_id, 100
);
select sm.set_name,
sm.member_id as window_id,
sm.song_id,
ao.parent_object_id as asset_id,
ao.start_ms,
ao.end_ms
from set_membership sm
join audio_object ao
on ao.object_id = sm.member_id
and sm.member_type = 'window'
where sm.set_type = 'reference_set'
and sm.set_name = 'phase1_hot_reference_v1'
and sm.is_active = true
order by sm.song_id, ao.start_ms;
```
### 5.6 从 embedding 回查 song
---
```sql
select ff.feature_id,
ff.model_name,
ff.model_version,
ff.feature_set_name,
win.object_id as window_id,
ast.object_id as asset_id,
song.entity_id as song_id,
song.title,
song.artist_name
from feature_fact ff
join audio_object win
on win.object_id = ff.object_id
and win.object_type = 'window'
join audio_object ast
on ast.object_id = win.parent_object_id
and ast.object_type = 'asset'
join media_entity song
on song.entity_id = ff.song_id
and song.entity_type = 'song'
where ff.feature_id = :feature_id;
## 7. 一个最小存储样例怎么理解
```text
song(Song Alpha)
-> asset(clip1.wav)
-> window(0-5000ms)
-> fingerprint(chromaprint)
-> embedding(mert-v1-95m)
-> window(2500-7500ms)
-> fingerprint(chromaprint)
-> embedding(mert-v1-95m)
```
落表后意味着:
- `song``media_entity`
- `asset/window` 都在 `audio_object`
- `chromaprint/mert` 都在 `feature_fact`
- 它们是否属于 hot reference 在 `set_membership`
---
## 6. 当前设计意图
## 8. 设计意图总结
这套结构主要解决:
- 同一 song 下多个音频文件
- 同一 asset 下多个切片窗口
- 同一 window 被多个模型重复编码
- fingerprint / embedding 统一落库
- reference/eval/hot 统一集合治理
- 查询后快速归属到 `song_id`
### 为什么切片和原始音频统一用 `audio_object`
- 新同学更容易理解
- asset/window 共用大量字段
- 减少专用表数量
---
### 为什么模型和特征统一用 `feature_fact`
- 不再一模型一张表
- 不再 fingerprint 一张表、embedding 一张表后继续扩散
- 更适合未来继续换 MERT / MuQ / 新模型
## 9. 当前最该关注的后续演进点
### 为什么 reference 集用 `set_membership`
- song / asset / window / feature 都可以挂集合
- reference / eval / hot 切换统一处理
1. 保持 4 表主链不变
2. 给 semantic lane 接真实 `MERT` / `MuQ` adapter
3. 继续复用 `feature_fact.model_name/model_version/feature_set_name` 做模型演进
4. 必要时再补更重的 registry / vector table 治理
---
## 7. 当前最推荐的实现顺序
## 10. 相关文档
1. 先建 `media_entity`
2. 再建 `audio_object`
3. 再建 `feature_fact`
4. 最后建 `set_membership`
5. 先打通 `song -> asset -> window -> embedding/fingerprint`
6. 再继续补更重的治理能力
- [README.md](./README.md)
- [start-here.md](./start-here.md)
- [session-handoff.md](./session-handoff.md)
- [postgresql-data-model.md](./postgresql-data-model.md)
......
# PostgreSQL 数据模型与 DDL 设计说明
# PostgreSQL 数据模型 / 当前 song-centric 4 表方案
> 更新:2026-06-04
> 关联 SQL:[`acr-engine/sql/acr_pg_schema_v2.sql`](../acr-engine/sql/acr_pg_schema_v2.sql)
> 目标:给出面向版权保护 / 大规模曲库 / 可替换 encoder 的 PostgreSQL 数据字典、DDL 设计意图、流程图与典型使用路径。
## 一页结论
当前推荐的 PostgreSQL 设计,不再围绕“某一个模型的 embedding 表”来建,而是围绕下面这条稳定主链来建:
```text
canonical_song -> work -> recording -> recording_asset -> audio_window
-> model_registry -> feature_set_registry -> audio_embedding / audio_fingerprint
-> reference_set_registry -> retrieval_index_registry -> retrieval_candidate -> match_decision
```
这套设计解决的是:
1. **song/work/recording 混在一起的问题**
2. **未来换模型就得改表的问题**
3. **窗口级检索无法回溯证据的问题**
4. **exact / semantic / future cover lane 无法统一聚合的问题**
---
## 0.1 为什么会感觉链路很多
本质上当前文档把 **3 类问题** 放在同一个总图里,所以看起来链路很长:
1. **业务归属层**`canonical_song / work / recording`
- 解决“最终归哪个 song_id / work_id / recording_id”
2. **物理音频层**`recording_asset / audio_window`
- 解决“实际文件是什么、切成了哪些检索窗口”
3. **检索计算层**`model_registry / feature_set_registry / audio_embedding / audio_fingerprint / retrieval_index_registry`
- 解决“用了哪个模型、哪套特征、哪套索引”
所以这不是一条单链,而是:
- 一条 **归属回溯链**
- 一条 **音频资产链**
- 一条 **特征/索引链**
把三者混看,就会误以为每次查询都要手工经过所有表。实际上在线检索真正关心的是:
```text
window -> candidate -> recording -> song
```
---
## 0.2 当前是否可以简化
可以。
如果当前阶段的目标是:
> 先服务版权保护场景,让 query 能快速稳定地命中正确 `song_id`
那么 Phase-1 完全可以收敛为下面这套 **最小可用骨架**
```text
song -> recording -> recording_asset -> audio_window
-> audio_fingerprint
-> audio_embedding
```
为了支持模型替换,再保留一个轻量版本登记层:
```text
feature_set_registry
```
也就是说,Phase-1 最小主链可以压缩成:
```text
song -> recording -> asset -> window -> feature
```
其中 `feature` 可具体落成:
- `audio_fingerprint`
- `audio_embedding`
---
## 0.3 哪些层建议 Phase-1 保留,哪些层可以弱化
### 建议保留
| 层 | 是否保留 | 原因 |
|---|---|---|
| `song` | 保留 | 最终业务返回对象 |
| `recording` | 保留 | 同一 song 下会有多个版本/录音 |
| `recording_asset` | 保留 | 一个 recording 可能有多个真实文件 |
| `audio_window` | 保留 | 检索和 evidence 的最小计算单元 |
| `feature_set_registry` | 保留 | 避免把 embedding/fingerprint 固化成表列 |
| `audio_embedding` / `audio_fingerprint` | 保留 | 真正的检索特征事实表 |
### 可以弱化或延期
| 层 | 当前建议 | 原因 |
|---|---|---|
| `work` | 可延期 | 如果当前只需稳定返回 `song_id`,可先不显式拆作品层 |
| `canonical_song` | 可与 `song` 合并理解 | 当前重点不是权利层深治理,而是先完成可用归属主键 |
| `retrieval_index_registry` | 可先弱化 | Phase-1 可先把索引治理做轻,不必一开始做太重 |
| `match_decision` 全量审计 | 可逐步补齐 | 先保证召回闭环,再加强审计/解释性 |
---
## 0.3.1 `recording` 和 `recording_asset` 能不能合并
可以合并,但**只适合非常早期、非常受控的数据集**
### 什么时候可以临时合并
只有当下面条件基本都成立时,才可以把二者临时看成一个对象:
1. 每个 `song` 只有一个可用录音版本
2. 每个录音只有一个音频文件
3. 不区分 master / distribution / captured / query_sample
4. 不需要追踪同一录音的多个来源文件
5. 不需要后续补高码率、补母带、补平台版本
在这种情况下,可以暂时把模型理解成:
```text
song -> recording_asset -> audio_window
```
也就是让 `recording_asset` 同时承担“版本对象 + 物理文件对象”的职责。
### 为什么长期不建议合并
因为 `recording``recording_asset` 回答的是两个不同问题:
- `recording` 回答:**这是哪个录音版本**
- `recording_asset` 回答:**这个录音版本对应哪个具体文件**
一旦进入真实版权保护场景,下面几类情况会非常常见:
1. **同一录音有多个文件版本**
例如 wav/flac/mp3、不同码率、不同平台导出件。
2. **同一 song 有多个录音版本**
例如 official/live/remaster/short/bgm cut。
3. **同一录音要接多个来源**
例如平台抓取、业务导出、人工补档。
4. **query 命中的是 asset,但归属要落到 recording/song**
如果不拆层,后面聚合和去重会比较乱。
### 当前最推荐的判断
对于你现在这个目标:
-`100w` 音频
-`30w` 歌曲
- 面向版权保护 / 听歌识曲 / 版本归属
**不建议把 `recording` 和 `recording_asset` 合并进正式 schema。**
原因很直接:
- 数据量已经不小
- 后续大概率会遇到多版本、多来源、多文件问题
- 现在省掉一层,后面重构成本会更高
### 更务实的折中方案
如果你觉得当前实现心智负担太高,可以不在产品/算法讨论里反复强调 `recording_asset`,而是采用下面口径:
```text
song -> recording -> asset -> window -> feature
```
也就是说:
- **概念上保留** `recording``asset` 两层
- **沟通上简写**`recording -> asset`
- **实现上继续分表**,避免未来返工
这通常是 Phase-1 最稳妥的折中。
> 更新:2026-06-04
> 关联 SQL:[`acr-engine/sql/acr_pg_schema_songcentric_v1.sql`](../acr-engine/sql/acr_pg_schema_songcentric_v1.sql)
---
## 0.4 一个更容易理解的口径
建议把当前体系理解为下面两条核心链:
## 1. 一页结论
### 归属链
当前默认只认 4 张核心物理表:
```text
window -> asset -> recording -> song
media_entity -> audio_object -> feature_fact -> set_membership
```
作用:
- 检索命中后,回溯最终归属到哪个 `song_id`
### 特征链
逻辑语义这样理解:
```text
window -> fingerprint / embedding -> candidate -> aggregate
song -> asset -> window -> fingerprint / embedding
```
作用:
- 真正完成召回、打分、聚合与排序
这样看时,整个设计就不再是“很多层没必要”,而是:
- **归属层负责回答是谁**
- **窗口层负责回答命中了哪一段**
- **特征层负责回答怎么检索出来**
---
## 1. 设计意图
## 1.1 这套设计想解决什么
### 问题 A:同一首歌可能有多个录音版本
所以必须区分:
- `canonical_song`:业务最终归一 song
- `work`:作品层
- `recording`:具体录音版本
### 问题 B:一个录音可能有多个文件资产
所以必须有:
- `recording_asset`
### 问题 C:检索真正命中的是片段,不是整首歌
所以必须有:
- `audio_window`
### 问题 D:未来底座会切换
所以必须有:
- `model_registry`
- `feature_set_registry`
### 问题 E:你会同时存在多个索引后端
所以必须有:
- `retrieval_index_registry`
这套设计的核心价值:
- **song-centric**:最终稳定返回 `song_id`
- **融合优先**:减少 `recording/work/version` 首阶段理解成本
- **特征统一**:exact lane 和 semantic lane 统一落到 `feature_fact`
- **模型可替换**:换 `model_name/model_version/feature_set_name` 不必重拆 schema
- **证据可回溯**:任何召回都能回查到具体 `window -> asset -> song`
---
## 1.2 为什么不用“reference_embeddings / query_embeddings”那种原型表继续扩
因为原型表有几个限制:
1. 维度写死,如 `vector(192)`
2. 数据对象太扁平,只围绕 `song_id`
3. 无法优雅支持多个 encoder
4. 无法表达同一 recording 下的多资产、多窗口、多 feature_set
所以原型版 SQL 适合 demo,不适合你现在的 100w 音频目标。
## 2. 为什么现在收敛成 4 表
### 当前最建议的简化口径
当前目标不是先建一个最完整的音乐版权知识图谱,而是先把下面这件事做稳:
如果团队正在进入 Phase-1 实施,不必把所有表同时视为“首批必须上线的复杂系统”。
更推荐按下面顺序理解和落库:
> 收到一个录音/BGM/片段/翻唱相关查询后,能够快速定位它最可能对应哪个 `song_id`。
1. `song -> recording -> recording_asset -> audio_window`
2. `feature_set_registry -> audio_fingerprint / audio_embedding`
3. `reference_set_registry` 与更重的索引治理随后补齐
因此当前优先级是:
1. 先固定 `song` 作为最终归属对象
2. 保留 `asset`,支持同一 `song` 下多个音频文件
3. 保留 `window`,支持切片级 evidence 与 offset
4. 用一张 `feature_fact` 同时承载 fingerprint 与 embedding
5. 用一张 `set_membership` 管理 reference/eval/hot 集合
---
## 1.2 当前业务前提变化:版本暂不重要,先做 song-centric
## 3. 4 张表分别解决什么问题
如果当前业务约束是:
> **同一个歌曲下可以有多个录音或多个音频,但暂时不关心版本语义,只需要最终稳定归到同一个 `song_id`**
那么当前 Phase-1 最推荐的默认口径应进一步收敛为:
```text
song -> asset -> window -> feature
```
也就是说:
- `song` 是当前唯一必须稳定返回的归属对象
- 同一个 `song` 下允许存在多个音频文件
- 这些音频文件可以是官方、抓取、BGM、片段、query sample 等不同来源
- 现阶段先不把“录音版本差异”提升成必须单独建模的核心层
### 当前最推荐的物理实现
在这个业务前提下,最推荐直接采用 **3+1 张融合表**
| 物理表 | 主要 type | 当前作用 |
| 表 | 当前主要 type | 解决的问题 |
|---|---|---|
| `media_entity` | `song` | 只承载最终业务归属对象 |
| `audio_object` | `asset`, `window` | 承载音频文件与切片窗口 |
| `feature_fact` | `fingerprint`, `embedding` | 承载检索特征事实 |
| `set_membership` | `reference_set`, `hot_set`, `eval_set` | 承载 reference / eval 等集合关系 |
对应逻辑主链:
| `media_entity` | `song` | 最终归属对象是谁 |
| `audio_object` | `asset`, `window` | 实际音频文件是什么、切成了哪些窗口 |
| `feature_fact` | `fingerprint`, `embedding` | 每个窗口/对象用了哪个模型、产出了什么特征 |
| `set_membership` | `reference_set`, `eval_set`, `hot_set` | 哪些 song/asset/window/feature 属于哪个集合 |
```text
song -> asset -> window -> feature
```
### 切片数据、模型、feature 具体落在哪些表
---
在当前 **song-centric + 融合优先** 设计下,可以直接按下面理解:
## 4. 切片 / 模型 / feature 分别在哪张表
| 你关心的对象 | 当前推荐表 | 关键 type / 字段 | 作用 |
| 业务对象 | 物理表 | 关键字段 | 用途 |
|---|---|---|---|
| 歌曲主实体 | `media_entity` | `entity_type=song` | 最终归属到哪个 `song_id` |
| 原始音频文件 | `audio_object` | `object_type=asset`, `song_id`, `storage_uri`, `checksum` | 保存同一 song 下的多个音频文件 |
| 切片窗口 | `audio_object` | `object_type=window`, `parent_object_id=<asset_id>`, `start_ms`, `end_ms` | 保存由 asset 切出来的检索窗口 |
| 模型信息 | `feature_fact` | `model_name`, `model_version`, `feature_set_name` | 记录这条特征是哪个模型、哪套参数算的 |
| fingerprint 特征 | `feature_fact` | `feature_type=fingerprint`, `fingerprint_value` | 保存 exact lane 特征 |
| embedding 特征 | `feature_fact` | `feature_type=embedding`, `embedding_dim`, `embedding_uri`, `vector_table_name` | 保存 semantic lane 特征 |
| reference / eval 归属 | `set_membership` | `set_type`, `member_type`, `member_id` | 决定哪些 asset/window/song 进入 reference 集 |
| song | `media_entity` | `entity_type='song'` | 最终返回 `song_id` |
| asset | `audio_object` | `object_type='asset'` | 存原始音频文件元数据 |
| window | `audio_object` | `object_type='window'`, `parent_object_id=<asset_id>` | 存切片范围、offset、evidence |
| fingerprint | `feature_fact` | `feature_type='fingerprint'`, `fingerprint_value` | exact lane 检索 |
| embedding | `feature_fact` | `feature_type='embedding'`, `embedding_uri/vector_table_name`, `embedding_dim` | semantic lane 检索 |
| model identity | `feature_fact` | `model_name`, `model_version` | 区分 MERT / MuQ / ECAPA / fallback |
| feature set identity | `feature_fact` | `feature_set_name`, `feature_schema_ver` | 区分特征配置、窗口策略、schema 版本 |
| reference routing | `set_membership` | `set_type`, `set_name` | 控制 reference/eval/hot 范围 |
### 4.1 一个关键设计点
当前 **模型信息不单独放 registry 表作为默认主链依赖**,而是先直接沉淀在 `feature_fact`
- 这样 Phase-1 更轻
- 更适合“直接复用开源 encoder,不先训练/微调”的当前策略
- 后续如果要补 registry,也可以把 `feature_fact` 中已有事实反向注册
最关键的一点是:
---
> **切片本身也落在 `audio_object`,只是 `object_type=window`;模型与特征统一落在 `feature_fact`。**
## 5. 核心流程图
### 对应流程图
### 5.1 落库流程
```mermaid
flowchart TD
A[media_entity
entity_type=song] --> B[audio_object
object_type=asset]
B --> C[audio_object
object_type=window]
C --> D1[feature_fact
feature_type=fingerprint]
C --> D2[feature_fact
feature_type=embedding]
D1 --> E[set_membership
reference_set / eval_set]
D2 --> E
A[media_entity\nentity_type=song] --> B[audio_object\nobject_type=asset]
B --> C[audio_object\nobject_type=window]
C --> D1[feature_fact\nfeature_type=fingerprint]
C --> D2[feature_fact\nfeature_type=embedding]
A --> E[set_membership]
B --> E
C --> E
```
### 对应写入流程
### 5.2 查询回溯流程
```mermaid
sequenceDiagram
participant ING as Ingest Job
participant DB as PostgreSQL
ING->>DB: 写 media_entity(song)
ING->>DB: 写 audio_object(asset)
ING->>DB: 切窗后写 audio_object(window)
ING->>DB: 写 feature_fact(fingerprint)
ING->>DB: 写 feature_fact(embedding)
ING->>DB: 写 set_membership(reference/eval)
flowchart LR
A[query audio] --> B[切片成 query windows]
B --> C[抽 fingerprint / embedding]
C --> D[命中 feature_fact]
D --> E[audio_object window]
E --> F[audio_object asset]
F --> G[media_entity song]
G --> H[输出 song_id + evidence]
```
### 一个最实用的查询回溯口径
如果 query 命中了一条 embedding/fingerprint,回溯路径就是:
### 5.3 表职责视图
```text
feature_fact -> audio_object(window) -> audio_object(asset) -> media_entity(song)
```mermaid
flowchart TB
M[media_entity\n谁] --> A[audio_object\n哪份音频/哪段切片]
A --> F[feature_fact\n用了哪个模型/产出什么特征]
M --> S[set_membership\n属于哪个 reference/eval/hot 集]
A --> S
F --> R[召回/匹配/聚合]
```
这条链已经足够支撑你当前最关心的问题:
- 这个切片来自哪个音频文件
- 这个音频文件归到哪个 `song_id`
- 这条特征是哪个模型/feature set 算出来的
---
### 为什么现在可以先不把 `recording` 做成强实体
因为你当前不关心:
- official / live / remaster 的严格版本区分
- cover/version lane 的独立归档
- 返回结果必须精确到 recording_id
你当前真正关心的是:
> 这一批不同来源、不同形式的音频,最后是否都能被稳定归到同一个 `song_id`
在这个目标下,把 `recording` 作为强主层,会增加理解成本,但对当前第一阶段收益有限。
### 但这不代表未来永远不要 `recording`
推荐的处理方式是:
- **当前 schema 默认不强推 `recording`**
- 如果未来开始关心版本归属,再把 `recording``media_entity(entity_type=recording)``audio_object.metadata_json` 中提升出来
换句话说:
- **当前先做 song-centric 检索归属**
- **未来再演进到 recording-centric / work-centric 治理**
---
## 1.2.1 融合优先:逻辑分层保留,物理表尽量收敛
如果你的核心诉求是:
> **尽量减少表数量,用 `type` + 通用关联表达多种对象,而不是一路拆很多表再 join**
那么推荐采用下面这个口径:
- **逻辑层** 当前默认保留 `song / asset / window / feature``recording` 仅保留为未来扩展语义
- **物理层** 尽量融合成少数几张通用表
也就是说:
> **概念上分层,落库上收敛。**
### 推荐的融合优先物理视图
| 物理表 | 主要 type | 作用 |
|---|---|---|
| `media_entity` | `song`(当前默认), `work`/`recording`(未来扩展) | 承载业务归属对象 |
| `audio_object` | `asset`, `window` | 承载真实音频文件与切片对象 |
| `feature_fact` | `fingerprint`, `embedding` | 承载检索特征事实 |
| `set_membership` | `reference_set`, `hot_set`, `eval_set` | 承载集合归属关系 |
这样,Phase-1 在物理表层面可以被收敛成:
```text
media_entity -> audio_object -> feature_fact -> set_membership
```
而不是新同学第一眼就看到很多高度专用表。
## 6. 每张表的设计意图
### 对应的逻辑语义
### 6.1 `media_entity`
#### `media_entity`
`entity_type` 区分:
- `song`(当前默认必用)
- `work`(可选)
- `recording`(未来扩展)
用途:
- 作为 song 主实体表
- 统一承载 `song_id`
- 后续如需要,也允许保留 `work/recording` type,但当前默认只把 `song` 当主语义
公共字段可统一为
当前最常用字段
- `entity_id`
- `entity_type`
- `parent_entity_id`
- `root_song_id`
- `biz_key`
- `title`
- `artist_name`
- `entity_status`
- `metadata_json`
#### `audio_object`
`object_type` 区分:
- `asset`
- `window`
设计意图:
- 不再把 song 相关字段散落到多张表
- 先把最终归属对象固定下来
公共字段可统一为:
- `object_id`
### 6.2 `audio_object`
用途:
- 同时管理 `asset``window`
-`parent_object_id` 建立 `asset -> window` 父子关系
当前最常用字段:
- `object_type`
- `recording_entity_id`
- `song_id`
- `parent_object_id`
- `storage_uri`
- `codec`
- `sample_rate`
- `start_ms` / `end_ms`
- `duration_ms`
- `checksum`
- `metadata_json`
- `duration_ms`
- `start_ms`
- `end_ms`
解释:
- `asset` 行表示真实音频文件
- `window` 行表示由某个 `asset` 切出来的检索窗口
设计意图:
- 同一 `song` 下可有多个音频文件
- 同一音频文件可切成多个检索窗口
- 查询命中后可以回查具体 offset
#### `feature_fact`
`feature_type` 区分:
- `fingerprint`
- `embedding`
### 6.3 `feature_fact`
公共字段可统一为:
- `feature_id`
用途:
- 统一存 exact lane 和 semantic lane 的特征事实
- 统一挂模型信息、特征集信息、特征载荷位置
当前最常用字段:
- `feature_type`
- `object_id`
- `song_id`
- `model_name`
- `model_version`
- `feature_set_name`
- `embedding_dim`
- `fingerprint_value` / `embedding_uri`
- `fingerprint_value`
- `embedding_uri`
- `vector_table_name`
- `metadata_json`
这样可以避免:
- 一套模型一张表
- 一类特征一张表
- 后续换模型就改 schema
### 为什么这比“纯拆表”更适合当前 Phase-1
优点:
1. **新同学更容易理解**:看到的是 3~4 张核心表,而不是十几张专用表
2. **更符合当前业务前提**:多个音频直接挂到同一个 `song_id`,先不强区分 recording
3. **模型演进更平滑**`feature_fact` 可以同时容纳不同模型与不同特征
4. **更符合当前目标**:先把识别闭环跑通,而不是先把治理模型拆到很细
### 但不要融合过头
虽然推荐物理收敛,但仍然不建议极端融合成一张大全表。
例如下面这种仍然过度扁平:
```text
song_everything
```
原因是它会把:
- 归属对象
- 音频对象
- 检索特征
- 集合关系
全部揉在一起,导致:
- 空字段过多
- 约束难写
- 批量写入难做
- 查询语义不清晰
因此更推荐的边界是:
```text
实体一张表 + 音频对象一张表 + 特征事实一张表 + 集合关系一张表
```
这是“融合优先但不过度融合”的平衡点。
---
## 1.3 Phase-1 极简 schema 视图
如果只从“第一阶段必须落哪些表”来理解,推荐优先采用“融合优先”的最小表集合:
| 层 | 融合优先推荐表 | 当前作用 |
|---|---|---|
| 实体层 | `media_entity` | 当前默认只承载 `song` |
| 音频对象层 | `audio_object` | 统一承载 `asset/window` |
| 特征层 | `feature_fact` | 统一承载 `fingerprint/embedding` |
| 集合层 | `set_membership` | 统一承载 `reference/hot/eval` 等集合关系 |
也就是说,Phase-1 如果按物理实现优先,真正应该先落稳的是:
```text
media_entity -> audio_object -> feature_fact -> set_membership
```
如果按逻辑语义理解,则当前默认对应:
```text
song -> asset/window -> fingerprint/embedding -> reference membership
```
### 这版极简 schema 明确不要求第一天就重投入的内容
可以后补:
- `work`
- 更重的 `retrieval_index_registry`
- 更细的 `retrieval_candidate / match_decision` 在线审计表
- 复杂的多 lane 重排治理表
### 但是极简不等于扁平
即使走极简版,也**不建议**退回到下面这种扁平结构:
```text
song -> embedding
song -> fingerprint
```
原因:
- 没有 `recording`,版本信息会丢
- 没有 `asset`,文件来源与去重会乱
- 没有 `window`,evidence/offset/多段聚合会弱很多
- 没有 `feature_set_registry`,模型升级会把 schema 写死
### 一个最实用的实现口径
如果团队现在就要开干,最推荐的实施顺序是:
1. 先落 `song / recording / recording_asset / audio_window`
2. 再落 `feature_set_registry / audio_fingerprint / audio_embedding`
3. 再落 `reference_set_registry / reference_set_member`
4. 最后再补 `work / retrieval_index_registry / match_decision` 等增强层
这样既能保持当前 Phase-1 简洁,也不会破坏未来扩展。
---
## 2. 数据主链
```mermaid
flowchart LR
A[canonical_song] --> B[work]
B --> C[recording]
C --> D[recording_asset]
D --> E[audio_window]
E --> F[audio_embedding]
E --> G[audio_fingerprint]
F --> H[retrieval_index_registry]
G --> H
H --> I[retrieval_candidate]
I --> J[match_decision]
```
---
## 3. 表分组
| 分组 | 表 | 作用 |
|---|---|---|
| 版权与实体 | `canonical_song`, `work`, `recording` | 统一业务归属 |
| 资产层 | `recording_asset` | 管理真实文件资产 |
| 窗口层 | `audio_window` | 管理检索最小证据片段 |
| 模型与特征 | `model_registry`, `feature_set_registry`, `audio_embedding`, `audio_fingerprint` | 管理模型版本与特征事实 |
| reference 集 | `reference_set_registry`, `reference_set_member` | 管理热 reference 集与版本化切换 |
| 索引层 | `retrieval_index_registry` | 记录后端索引 |
| 匹配层 | `retrieval_candidate`, `match_decision` | 在线召回与最终归一 |
---
## 4. 关键表说明
设计意图:
- 避免为不同模型建一堆平行 embedding 表
- 未来换 MERT / MuQ / 其他 encoder 时只增 feature rows,不改主 schema
- exact / semantic 两条 lane 可以共用同一归属链
## 4.1 `canonical_song`
最终业务主键。
### 6.4 `set_membership`
用途:
- 服务最终返回 `canonical_song_id`
- 权利归属、产品展示、对外业务都以它为准
- 统一管理 reference_set / eval_set / hot_set
- member 可以是 `song/asset/window/feature`
## 4.2 `work`
作品层。
用途:
- 同一首歌的不同翻唱/演绎归一到作品层
- future phase 的 cover/version lane 常常先聚到 `work_id`
## 4.3 `recording`
录音层。
用途:
- official/live/remaster/cover/ugc 等不同版本分开管理
- 允许多个 recording 最终映射到同一个 `canonical_song`
## 4.4 `recording_asset`
文件资产层。
用途:
- 同一个 recording 可有多个文件版本
- 可区分 master/reference/distribution/captured/query_sample
## 4.5 `audio_window`
窗口层。
用途:
- 建指纹
- 抽 embedding
- 在线输出 evidence window
- 对 intro/chorus 等片段做后续治理
## 4.6 `model_registry`
模型注册表。
用途:
- 记录 `model_name/model_version/output_embedding_dim`
- 未来切换 MERT/MuQ/其他底座时不改业务表
## 4.7 `feature_set_registry`
特征版本表。
用途:
- 记录窗长、hop、pooling、layer、metric
- 同一模型不同用法变成不同 feature_set
## 4.8 `audio_embedding`
embedding 元数据事实表。
用途:
- 记录某个 asset/window 由哪个 feature_set 生成了什么 embedding
- 可指向 pgvector,也可只指向外部 parquet/npy
## 4.9 `reference_set_registry` / `reference_set_member`
reference 集版本表。
用途:
- 把“当前线上热 reference 集”提升成显式对象
- 支持 A/B、灰度、回滚、历史回放
-`is_reference` 从单条 recording 标签升级为“可切换集合”
## 4.10 `retrieval_index_registry`
索引注册表。
用途:
- 同一 feature_set 可挂多个 backend / shard / version
- 支持 pgvector / faiss / milvus 并存
## 4.11 `retrieval_candidate`
召回候选。
用途:
- 保存 exact lane / semantic lane / future cover lane 的候选
- 便于线下分析与线上回放
## 4.12 `match_decision`
最终判定。
用途:
- 输出 `canonical_song_id / work_id / recording_id`
- 保留判定理由与分数
设计意图:
- reference 范围不硬编码到 song 表里
- 评测集、热集、灰度集能共用一张关系表
---
## 5. 示例流程图
## 7. 为什么“切片数据 + 模型 + feature”这样分布最合理
## 5.1 离线建库流程
### 切片数据放 `audio_object`
因为切片本质是音频对象的一种:
- 它有父 asset
- 它有 `start_ms/end_ms`
- 它需要被回溯和复用
```mermaid
flowchart TD
A[导入音频资产] --> B[写 recording_asset]
B --> C[切窗并写 audio_window]
C --> D[注册 model_registry / feature_set_registry]
D --> E[抽取 embedding / fingerprint]
E --> F[写 audio_embedding / audio_fingerprint]
F --> G[构建 retrieval index]
G --> H[登记 retrieval_index_registry]
```
### 模型信息放 `feature_fact`
因为模型是“某次特征计算”的属性:
- 同一个 window 可能被多个模型重复编码
- 同一个模型也可能有多个版本
- 模型名和版本应该和 feature 结果绑定,而不是只和 asset 绑定
## 5.2 在线检索流程
### feature 放 `feature_fact`
因为 feature 是事实:
- 某个对象
- 用某个模型
- 以某个 feature set
- 产出某个结果
```mermaid
sequenceDiagram
participant Q as Query Audio
participant DB as PostgreSQL
participant IDX as Retrieval Index
participant SVC as Matching Service
Q->>SVC: 输入 query
SVC->>DB: 读取 active feature_set
SVC->>IDX: exact lane / semantic lane 查询
IDX-->>SVC: 候选 window / recording
SVC->>DB: 回查 window -> recording -> work -> canonical_song
SVC->>DB: 写 retrieval_candidate
SVC->>DB: 写 match_decision
SVC-->>Q: 返回 canonical_song_id + evidence
```
这正好就是一条事实记录。
---
## 5.3 生产冻结前建议补硬的 4 个点
### A. lineage 硬约束
建议通过 trigger / transaction invariant 保证以下链路永远一致:
- `recording.work_id -> work.work_id`
- `recording.canonical_song_id -> work.canonical_song_id`
- `audio_window.asset_id -> recording_asset.recording_id -> recording/work/song`
- `audio_embedding.window_id -> audio_window.recording/work/song`
### B. reference set 版本化
建议把“热 reference 集”提升成显式对象,而不是只依赖 `is_reference`
这样可以支持:
- hot/cold reference 切换
- A/B 对照
- encoder 升级期间的双索引并存
- 历史回放
### C. 候选实体多态约束
`candidate_level + candidate_id` 很灵活,但生产化时至少要加枚举/约束,避免数据面上出现无效 level。
### D. 向量维度扩展规则
当前 `192/768` 物理表是热路径实现,不是最终维度上限。新增 encoder 维度时应遵循固定 playbook:
1. 新增一张 `audio_embedding_vector_<dim>` 物理表
2. 回填对应 `feature_set` 的 embeddings
3. 构建对应索引
4. 通过 `retrieval_index_registry` 切换 active 热索引
## 8. 第一个阶段如何服务 100w 音频 / 30w 歌曲
---
### 建议的落盘顺序
## 6. 推荐 DDL 的主要原则
1. 先写 `media_entity(song)`
2. 再写 `audio_object(asset)`
3. 再批量切 `audio_object(window)`
4. 再按模型批次写 `feature_fact`
5. 最后写 `set_membership(reference_set/hot_set/eval_set)`
## 原则 1:对象关系稳定,模型可变
稳定的是:
- `song/work/recording/asset/window`
### 为什么这样落
可变的是:
- `model_name`
- `feature_set`
- `index_backend`
## 原则 2:向量不要写死为唯一真相
推荐把向量事实拆成:
- PostgreSQL 元数据主表
- 向量可在 pgvector 分表或外部文件中存放
## 原则 3:窗口是最小证据粒度
因为版权保护最终不只是“命中这首歌”,还要回答:
- 命中的是哪一段
- 哪个录音版本
- 归属到哪个 work/song
因为这能把“音频对象生命周期”和“模型计算生命周期”解耦:
- 音频先入库
- 切片先固定
- exact lane 可先跑
- semantic lane 之后补跑也不影响主链
---
## 7. 推荐的物理实现思路
## 9. Phase-1 推荐策略
## 7.1 PostgreSQL 负责
- 主数据
- 模型注册
- 特征注册
- 索引注册
- 检索候选
- 审核/决策
### 9.1 exact lane
- 默认:`ChromaprintMatcher`
- 落到:`feature_fact(feature_type='fingerprint')`
## 7.2 pgvector 负责
- 热 reference 集合
- 线上低延迟近邻查询
### 9.2 semantic lane
- 当前优先:`MERT`
- challenger:`MuQ`
- 当前 host 若 runtime 不可用,保留 fallback
- 落到:`feature_fact(feature_type='embedding')`
## 7.3 外部对象存储/文件层负责
- 原始音频
- 标准化音频
- 大体量 embedding parquet/npy
- 索引 shard 文件
### 9.3 为什么不是 ECAPA-TDNN 主导
- ECAPA 更偏 speaker/audio identity 方向
- 当前目标是版权保护 / song-level ACR
- `MERT` / `MuQ` 更适合作为 song semantic baseline/challenger
---
## 8. 为什么这个设计更适合 SOTA 演进
因为未来你最可能变化的不是 `canonical_song` 结构,而是:
| 会变化的东西 | 对应表 |
|---|---|
| 底座模型 | `model_registry` |
| 特征版本 | `feature_set_registry` |
| embedding dim | `model_registry.output_embedding_dim` |
| 池化与层选择 | `feature_set_registry.pooling_strategy/layer_selection` |
| 索引后端 | `retrieval_index_registry.index_backend` |
## 10. 当前方案解决的问题
所以 schema 的目标是:
> **允许模型变、索引变、特征变,但不让主数据和业务归属逻辑跟着崩。**
这套 4 表设计,当前主要解决:
- 同一 `song` 下多音频文件管理
- 切片级 evidence 管理
- fingerprint 与 embedding 统一落库
- 模型切换时不重构主 schema
- reference/eval/hot 集统一治理
- 检索命中后快速回到 `song_id`
---
## 9. DDL 文件说明
推荐直接使用:
- [`acr-engine/sql/acr_pg_schema_v2.sql`](../acr-engine/sql/acr_pg_schema_v2.sql)
其中包含:
- 主数据表
- 模型注册表
- 特征表
- 向量物理表(192/768 维示例)
- 索引建议
而原有:
- [`acr-engine/sql/pgvector_schema.sql`](../acr-engine/sql/pgvector_schema.sql)
## 11. 当前不刻意解决的问题
建议视为:
- 原型版 / demo 版 / 兼容参考
Phase-1 暂不强求:
- 复杂 `work / recording / version` 治理
- 完整权利层图谱
- 训练/微调闭环
- 重型 registry-first 体系
---
## 10. 实施顺序建议
### 第一批必须先落
1. `canonical_song`
2. `work`
3. `recording`
4. `recording_asset`
5. `audio_window`
6. `model_registry`
7. `feature_set_registry`
8. `audio_embedding`
9. `retrieval_index_registry`
### 第二批再补
1. `retrieval_candidate`
2. `match_decision`
3. `audio_fingerprint`
4. 更多维度的向量物理表
这些都可以后续逐步加,但不该反向阻塞当前主链。
---
## 11. 典型注册与查询示例
## 11.1 注册一个开源模型
```sql
insert into model_registry (
model_name, model_family, model_version, model_source, model_uri,
input_sample_rate, default_window_sec, default_hop_sec, output_embedding_dim,
pooling_supported, layer_selection_supported, is_trainable
) values (
'mert', 'music_ssl', 'v1-95m', 'github', 'https://github.com/yizhilll/MERT',
24000, 5.0, 2.5, 768,
array['mean','cls'], true, false
);
```
## 11.2 注册一个 feature set
```sql
insert into feature_set_registry (
model_id, feature_name, feature_level, extraction_granularity,
window_sec, hop_sec, embedding_dim, pooling_strategy, layer_selection,
normalize_l2, distance_metric, quantization_type, feature_schema_version
)
select
model_id, 'semantic_embedding', 'window', 'sliding_window',
5.0, 2.5, 768, 'mean', 'final',
true, 'cosine', 'none', 'v1'
from model_registry
where model_name = 'mert' and model_version = 'v1-95m';
```
## 11.3 查询当前激活的 reference feature set
```sql
select fs.feature_set_id, mr.model_name, mr.model_version,
fs.window_sec, fs.hop_sec, fs.pooling_strategy, fs.distance_metric
from feature_set_registry fs
join model_registry mr on mr.model_id = fs.model_id
where fs.status = 'active'
and fs.feature_level = 'window'
and fs.feature_name = 'semantic_embedding'
order by fs.feature_set_id desc;
```
## 11.4 从候选 window 回查到最终 song
```sql
select rc.query_id, rc.rank_no, rc.normalized_score,
aw.window_id, aw.start_sec, aw.end_sec,
r.recording_id, r.version_type,
w.work_id,
cs.canonical_song_id, cs.title, cs.primary_artist
from retrieval_candidate rc
join audio_window aw on aw.window_id = rc.evidence_window_id
join recording r on r.recording_id = aw.recording_id
join work w on w.work_id = aw.work_id
join canonical_song cs on cs.canonical_song_id = aw.canonical_song_id
where rc.query_id = :query_id
order by rc.rank_no asc;
```
## 11.5 查询某个 song 的全部 reference 资产和窗口
```sql
select cs.canonical_song_id, cs.title,
r.recording_id, r.version_type, r.is_reference,
ra.asset_id, ra.storage_uri,
aw.window_id, aw.window_index, aw.start_sec, aw.end_sec
from canonical_song cs
join recording r on r.canonical_song_id = cs.canonical_song_id
join recording_asset ra on ra.recording_id = r.recording_id
left join audio_window aw on aw.asset_id = ra.asset_id
where cs.canonical_song_id = :canonical_song_id
order by r.reference_priority asc, ra.asset_id asc, aw.window_index asc;
```
## 11.6 查询某个 feature set 是否已完成索引构建
```sql
select fs.feature_set_id, mr.model_name, mr.model_version,
ri.index_backend, ri.index_type, ri.row_count, ri.index_status, ri.built_at
from feature_set_registry fs
join model_registry mr on mr.model_id = fs.model_id
left join retrieval_index_registry ri on ri.feature_set_id = fs.feature_set_id
where fs.feature_set_id = :feature_set_id;
```
---
## 12. 当前建议结论
如果你今天就要开始 PostgreSQL 落库,最推荐的做法是:
## 12. 相关文档
1. 先把 `song/work/recording/asset/window` 落稳
2. 同时把 `model_registry / feature_set_registry` 落稳
3. Phase-1 只注册开源 encoder feature set,不写死到某个 embedding 列
4. 先把热 reference 集上 pgvector,冷数据通过外部文件或后续索引层接入
- [README.md](./README.md)
- [start-here.md](./start-here.md)
- [session-handoff.md](./session-handoff.md)
- [postgres_db_schema_samples.md](./postgres_db_schema_samples.md)
......
# Production Encoder Freeze & Embedding Strategy / 生产 Encoder 冻结与 Embedding 策略答疑
> 更新:2026-06-03
> 关联文档:[持续开发交接文档](./session-handoff.md) · [PostgreSQL 数据模型](./postgresql-data-model.md) · [Phase-1 实施清单](./phase1-implementation-checklist.md)
## 一页结论
围绕你当前最关心的生产问题,可以先压缩成 6 句话:
1. **当前先冻结 encoder 是正确决定。** 对 30 万首生产曲库来说,先稳定 embedding 空间,比继续频繁改模型更重要。
2. **当前结构具备泛化能力。** 线上识别主路径是“固定 encoder 抽 embedding + reference 检索”,不是只能识别训练时见过的 closed-set 分类标签。
3. **模型权重可以外置复用。** 你可以把当前 `best_model.pt` 当成独立 encoder,用在别的 wav/mp3/flac/ogg 歌曲集合上。
4. **如果 encoder 变了,向量库就必须重建。** 歌曲元数据不用重做,但所有旧 embedding / ANN index / pgvector 向量都应视为旧版本。
5. **30 万首场景下必须做 embedding 版本化。** 不建议“直接覆盖旧库”,而应并行维护 `encoder_version=v1/v2/...`
6. **最稳的上线策略是:先冻结 v1,上线使用;新模型只做离线 shadow build + A/B,收益足够大再切换。**
---
## 1. 当前项目进度:已经走到哪里
## 1.1 当前状态
根据 [持续开发交接文档](./session-handoff.md),当前项目已经从“原型是否能跑通”进入“真实数据验证 + 工程化推进”阶段:
- synthetic 数据链路已跑通:生成、训练、建索引、识别、评测都已具备
- 开放数据链路已闭环:`inspect-local -> prepare-local -> validate-local -> train -> build-index -> evaluate -> generate_artifacts`
- 当前最佳候选方向已收敛到 `hum_focus`
- 真实 FMA smoke 已跨过训练,进入了 `build-index` 阶段
这说明当前不是“从零开始想方案”,而是已经具备:
1. **一个可以独立抽 embedding 的 encoder**
2. **一个可以对 reference 曲库建索引的 pipeline**
3. **一个可以对 query 做识别的 hybrid 检索链路**
## 1.2 当前最适合的策略
在这个阶段,最重要的不是继续频繁换 encoder,而是:
```mermaid
flowchart TD
A[已有可运行模型与索引链] --> B[冻结生产 Encoder v1]
B --> C[完成 30 万首建库与使用]
C --> D[收集真实线上/离线问题]
D --> E[离线评估新 Encoder v2]
E --> F[证明显著收益后再迁移]
```
也就是:**先让一版可用的 embedding 体系稳定下来,再做后续升级。**
---
## 2. 当前结构是否有泛化能力
## 2.1 简短回答
**有。**
但这里的“泛化”要分成两层理解:
| 泛化层次 | 当前是否支持 | 说明 |
|---|---|---|
| 新歌曲入库后可被识别 | 支持 | 只要用同一 encoder 生成 reference embedding 即可 |
| 完全未知分布上保持稳定高精度 | 尚未完全证明 | 当前更多是结构可行、真实大规模效果还需继续验证 |
## 2.2 为什么说当前结构具备泛化能力
当前识别主链路不是:
- “输入 query -> 分类头直接输出某个固定 song class”
而是:
- “输入 query -> encoder 抽 embedding -> 在 reference embedding 库中做相似度检索”
结构图如下:
```mermaid
flowchart LR
Q[query wav/mp3 片段] --> E1[冻结 Encoder v1]
E1 --> QE[query embedding]
QE --> S[similarity search]
R[30 万首 reference 曲库] --> E2[同一个冻结 Encoder v1]
E2 --> RE[reference embeddings]
RE --> S
S --> O[候选歌曲 + 分数]
```
这种结构天然支持:
- 新歌曲加入 reference 曲库
- 不重训模型的情况下扩歌库
- 针对 query 做检索而不是固定类分类
## 2.3 但泛化能力的上限受什么影响
当前真实效果主要还受这些因素制约:
1. **训练数据分布**
- 目前 hard case 已关注 `humming_like` / `confused`
- 但你的线上 30 万首曲库是否和当前训练/验证分布一致,仍需要真实数据验证
2. **reference 建库方式**
- 当前 reference 端是 **5 秒窗口 + 2.5 秒 stride** 的重叠滑窗
- 这对片段识别是好的,但代价是窗口数上升、建库成本变高
3. **query 形式差异**
- 无损整曲、压缩 mp3、录音片段、截短片段、混响/手机采样等都会影响效果
4. **混合检索策略**
- 当前不是纯 embedding 检索,而是 `chromaprint + embedding + melody rerank` 的 hybrid
- 这对鲁棒性是加分项,但也意味着线上治理时要同时考虑指纹索引和向量索引
---
## 3. 为什么当前先冻结 Encoder
## 3.1 业务原因
你现在的生产环境里有 **30 万首歌曲**。这会带来一个决定性的工程事实:
> **一旦 encoder 改了,就不是“换个模型文件”这么简单,而是整套向量空间都可能变。**
如果还没冻结 encoder,就会出现这些问题:
- 今天建的 30 万 embedding,明天可能全部作废
- pgvector / Faiss / Milvus / 自研 ANN 索引都要全量重建
- 离线评测和线上灰度无法稳定对齐
- 回滚困难:你很难知道 query 是按哪个模型算的,reference 又是按哪个模型建的
## 3.2 工程原因
冻结 encoder 后,你才能稳定以下这些对象:
| 资产 | 是否应随 encoder 冻结 | 原因 |
|---|---|---|
| `best_model.pt` | 是 | 它决定向量空间 |
| `n_mels/sample_rate/window/stride` | 是 | 这些决定 embedding 分布 |
| `embedding_dim` | 是 | 向量表结构和索引依赖它 |
| reference embeddings | 是 | 必须和 query embedding 同版本 |
| ANN index / pgvector 索引 | 是 | 建立在具体 embedding 空间上 |
## 3.3 当前建议的冻结定义
建议把当前生产 encoder 冻结为一组明确配置,而不是只记一个文件名。
推荐至少记录这些字段:
```yaml
encoder_version: ecapa_hum_focus_v1
checkpoint_path: /abs/path/to/best_model.pt
embedding_dim: 192
sample_rate: 16000
n_mels: 128
n_fft: 512
hop_length: 160
reference_window_sec: 5.0
reference_stride_sec: 2.5
query_runtime_window_sec: 5.0
feature_family: mel
index_family: hybrid_chromaprint_ecapa
status: frozen_for_production
```
---
## 4. 模型权重能否外置,给其他歌曲使用
## 4.1 简短回答
**可以,而且应该这样做。**
你可以把当前冻结好的 `best_model.pt` 视为一个独立的音乐 embedding encoder,用来处理:
- 新增歌曲入库
- 旧歌库全量建 embedding
- 任意 query 音频片段识别
- 后续 pgvector / ANN 向量服务对接
## 4.2 外置使用时的推荐组织方式
推荐把“模型”和“索引产物”分开管理:
```mermaid
flowchart TD
A[models/] --> A1[ecapa_hum_focus_v1/best_model.pt]
A --> A2[ecapa_hum_focus_v1/encoder_manifest.yaml]
B[indexes/] --> B1[ecapa_hum_focus_v1/chromaprint.pkl]
B --> B2[ecapa_hum_focus_v1/reference_embs.npy]
B --> B3[ecapa_hum_focus_v1/reference_ids.npy]
B --> B4[ecapa_hum_focus_v1/index_metadata.json]
C[metadata/] --> C1[song_catalog.csv]
C --> C2[manifests/catalog.json]
```
## 4.3 外置后能做什么
### 场景 A:直接给新歌曲建库
- 你有一批新歌(wav/mp3/flac/ogg)
- 不重训
- 用冻结 encoder 直接建 reference embedding
- 加入曲库识别
### 场景 B:给 query 片段做识别
- 你有 5~10 秒左右的查询片段
- 用同一 encoder 抽 query embedding
- 在 reference 库做相似度匹配
### 场景 C:离线批量生成 pgvector/ANN 数据
- 你可以把 reference embeddings 作为离线产物导出
- 再灌入 PostgreSQL + pgvector / Faiss / Milvus / 自研检索服务
---
## 5. 你手头有无损、压缩、片段等 wav/mp3 文件集合,应该怎么直接使用
## 5.1 总体原则
你手头的文件不需要先人工区分“是不是训练专用格式”。
当前最重要的是把它们统一进入这套结构:
```mermaid
flowchart LR
A[原始 wav/mp3/flac/ogg] --> B[标准化目录]
B --> C[manifest]
C --> D[reference 建库]
C --> E[query 验证]
D --> F[线上识别/检索]
```
## 5.2 建议先分三类素材
| 类别 | 作用 | 建议处理方式 |
|---|---|---|
| 完整歌曲 / 主版本 | 做 reference | 全部入库 |
| 截断片段 / 业务 query 样本 | 做评测 query | 固定留作测试集 |
| 低码率/手机录音/混响压缩片段 | 做 hard case query | 用来验证鲁棒性 |
## 5.3 最短直接使用流程
### 第 1 步:冻结 encoder v1
先不要继续换模型,先把当前决定好的 checkpoint 固定下来。
### 第 2 步:准备音频目录
例如:
```text
input_music/
song_a.flac
song_b.mp3
song_c.wav
...
```
### 第 3 步:检查目录是否适合进入当前链路
```bash
cd /root/vprecog/acr-engine
/usr/local/miniconda3/bin/python src/data/manifest_tools.py inspect-audio-dir \
/abs/path/to/input_music \
--query-duration 8.0 \
--eval-ratio 0.2
```
### 第 4 步:生成统一 manifest
```bash
cd /root/vprecog/acr-engine
/usr/local/miniconda3/bin/python src/data/manifest_tools.py audio-dir-to-splits \
/abs/path/to/input_music \
/abs/path/to/output_dataset \
--source-dataset prod_music \
--eval-ratio 0.2 \
--query-duration 8.0 \
--query-strategy hybrid \
--query-stride 2.5
```
建议说明:
- `query-duration=8.0`:适合作为线下验证 query 长度
- `query-strategy=hybrid`:更贴近当前项目已有的音乐感知切片方向
- `query-stride=2.5`:如果你希望验证覆盖率更高,可以生成更多 query
### 第 5 步:用冻结 encoder 建 reference index
```bash
cd /root/vprecog/acr-engine
/usr/local/miniconda3/bin/python run_demo.py build-index \
--data /abs/path/to/output_dataset/manifests \
--model /abs/path/to/frozen/best_model.pt \
--output /abs/path/to/output_index \
--device cpu
```
### 第 6 步:做离线识别验证
```bash
cd /root/vprecog/acr-engine
/usr/local/miniconda3/bin/python evaluate.py \
--data /abs/path/to/output_dataset/manifests \
--model /abs/path/to/frozen/best_model.pt \
--index-prefix /abs/path/to/output_index/reference \
--split test \
--device cpu \
--fast-eval \
--output-json /abs/path/to/output_reports/eval.json
```
### 第 7 步:确认后再推到生产向量库
当离线评测满足最低要求后,再把 reference embedding 导入生产检索系统,而不是一上来直接刷 30 万首正式库。
---
## 6. 如果要快速微调,应该怎么做
## 6.1 原则
**“快速微调”不等于“马上用 30 万首全量重训”。**
对你当前场景,最合理的是:
1. 先冻结生产 encoder v1
2. 用一个代表性子集训练/微调候选 v2
3. 只在离线环境评估 v2
4. 证明收益大于迁移成本,才考虑升级生产 encoder
## 6.2 推荐的微调子集组成
建议优先抽一个 **几千到几万首规模的代表性集合**,覆盖:
- 无损高质量版本
- 常见压缩版本(mp3/aac 等)
- 短片段
- 开头/中段/结尾片段
- 旋律近似、编曲相似的易混淆歌曲
- 手机采样 / 录屏 / 二次压缩音频
- 业务上最常见失败样本
## 6.3 推荐微调流程
```mermaid
flowchart TD
A[冻结 Encoder v1] --> B[抽代表性子集]
B --> C[生成 manifests]
C --> D[训练 v2 候选]
D --> E[建 v2 索引]
E --> F[固定测试集评测]
F --> G{收益显著?}
G -- 否 --> H[继续使用 v1]
G -- 是 --> I[准备 30 万首离线重刷 v2]
```
## 6.4 微调时的判断标准
不要只看训练 loss,至少要看:
| 指标 | 作用 |
|---|---|
| top1 / topk | 基本识别率 |
| hard-case 命中率 | 看压缩/片段/混淆样本提升是否真实 |
| 新增模型对旧强项的回退 | 防止“补一个洞,漏一大片” |
| 全量建库速度 | 看新模型是否导致生产成本显著上升 |
| 线上 query 延迟 | 看推理成本是否可接受 |
## 6.5 微调后的发布原则
微调完成后不要立刻替换 v1,而要:
1. 标记为 `encoder_version=v2_candidate`
2. 只做离线建库和评测
3. 在真实样本上和 v1 做 A/B
4. 显著更优后再升级
---
## 7. 如果 embedding 变了,哪些数据必须重建
## 7.1 要区分“元数据”和“向量数据”
| 数据类型 | 是否需要重建 | 说明 |
|---|---|---|
| 歌曲主数据(song_id/路径/业务标签) | 通常不需要 | 这些不依赖向量空间 |
| manifest | 通常不需要全重做 | 除非你的切片策略/数据治理规则也变了 |
| reference embeddings | 必须重建 | 因为 encoder 变了 |
| query embeddings 缓存 | 必须重建 | 否则和 reference 不同空间 |
| pgvector 行数据 | 必须重建 | 向量本体变了 |
| ANN 索引(Faiss/Milvus/HNSW/IVF 等) | 必须重建 | 建立在旧向量之上 |
| chromaprint 索引 | 不一定 | 只要指纹算法不变,可独立复用 |
## 7.2 为什么必须重建
因为向量相似度检索默认假设:
> query embedding 和 reference embedding 来自同一个特征空间。
如果 query 用的是 `encoder v2`,reference 还停留在 `encoder v1`,就会出现:
- 分数不可比
- recall 明显下降
- 线上结果随机波动
- A/B 结论失真
## 7.3 推荐的迁移策略
不要“原地覆盖旧 embedding”,而应采用双版本并行:
```mermaid
flowchart LR
A[Encoder v1] --> B[index_v1]
C[Encoder v2] --> D[index_v2]
E[Query] --> F{使用哪个版本?}
F --> B
F --> D
```
推荐步骤:
1. 保留 `v1` 生产库不动
2. 离线刷 `v2` 的 30 万 embedding
3.`v2` 的 ANN / pgvector 索引
4. 用相同 query 集对比 v1/v2
5. 确认收益后切主流量
6. 回滚时直接切回 v1
---
## 8. 30 万首生产环境推荐的版本化方案
## 8.1 推荐最小字段
### 歌曲元数据表
| 字段 | 说明 |
|---|---|
| `song_id` | 稳定歌曲 ID |
| `audio_uri` | 原始音频路径/对象存储地址 |
| `duration_sec` | 时长 |
| `codec/container` | 格式信息 |
| `catalog_status` | 是否可入 reference 库 |
| `business_tags` | 业务标签 |
### embedding 表
| 字段 | 说明 |
|---|---|
| `song_id` | 对应歌曲 |
| `encoder_version` | 如 `ecapa_hum_focus_v1` |
| `window_index` | 第几个 reference window |
| `offset_sec` | 窗口起点 |
| `embedding_dim` | 例如 192 |
| `embedding` | 向量本体 |
| `built_at` | 构建时间 |
| `source_audio_hash` | 原音频指纹/摘要,便于查重与失效控制 |
### index manifest
| 字段 | 说明 |
|---|---|
| `encoder_version` | 当前索引对应的 encoder |
| `checkpoint_path` | 模型文件 |
| `feature_config` | mel/n_mels/sample_rate 等 |
| `reference_window_sec` | 例如 5.0 |
| `reference_stride_sec` | 例如 2.5 |
| `catalog_size` | 曲库规模 |
| `num_reference_windows` | 总窗口数 |
| `built_at` | 构建时间 |
## 8.2 推荐目录结构
```text
prod_artifacts/
models/
ecapa_hum_focus_v1/
best_model.pt
encoder_manifest.yaml
indexes/
ecapa_hum_focus_v1/
chromaprint.pkl
chromaprint_progress.json
reference_embs.npy
reference_ids.npy
index_metadata.json
reports/
ecapa_hum_focus_v1/
eval.json
hard_case_eval.json
```
---
## 9. 当前建议的生产操作手册
## 9.1 目标
当前目标不是继续改模型,而是先完成:
1. 冻结 encoder v1
2. 用 v1 支撑第一版 30 万首曲库建库
3. 建立版本化规范
4. 为后续 v2 升级预留迁移机制
## 9.2 分步操作
### Phase 1:冻结与归档
- [ ] 选定当前生产 checkpoint
- [ ] 为该 checkpoint 生成 `encoder_manifest.yaml`
- [ ] 记录 `encoder_version`
- [ ] 固定 reference window / stride / mel 配置
- [ ] 禁止直接覆盖此 checkpoint
### Phase 2:小规模真实集验证
- [ ] 抽 1k~10k 首真实歌曲做第一次建库
- [ ] 抽真实 query 集做评测
- [ ] 统计 top1/topk/hard-case 结果
- [ ] 验证索引构建速度、磁盘占用、查询延迟
### Phase 3:30 万首全量离线建库
- [ ] 清洗 song_id 与元数据
- [ ] 生成标准 catalog/manifests
- [ ] 用冻结 encoder v1 批量生成 reference embeddings
- [ ] 生成 chromaprint / 向量索引
- [ ] 导入生产检索服务
### Phase 4:上线与观测
- [ ] 上线 v1
- [ ] 记录 query 失败样本
- [ ] 归档 hard-case
- [ ] 为 v2 微调准备离线样本集
### Phase 5:未来升级
- [ ] 训练 `v2_candidate`
- [ ] 离线全量重刷 `index_v2`
- [ ] 做离线 A/B
- [ ] 收益显著后再切换主版本
---
## 10. 常见问题 FAQ
## 10.1 我现在有一大堆 wav/mp3,可以直接用吗?
**可以。**
先不要纠结格式本身,先把它们组织成统一音频目录,再生成 manifests,再用冻结 encoder 建 reference index。
## 10.2 无损和压缩版本要不要分开?
**建议保留来源信息,但 reference 主库优先保留“主版本”。**
如果同一首歌有多个编码版本:
- 主版本作为 reference 主资产
- 其他压缩版本优先作为评测 query / hard case
这样更利于真实评估鲁棒性。
## 10.3 片段文件可以直接拿来做 query 吗?
**可以。**
尤其适合作为:
- clean query
- compressed query
- hard case query
但如果要把它当训练样本,最好仍然能回溯到稳定的 `song_id` 和原 reference。
## 10.4 如果后面发现 encoder 不够好怎么办?
不要直接替换现网。正确做法是:
1. 保持 v1 不动
2. 离线训练 v2
3. 离线重刷 v2 的全量 embedding
4. 对比 v1/v2
5. 确认收益后再切
## 10.5 现在有没有必要立刻上全量 30 万首?
**建议先做一轮中等规模验证,再上全量。**
推荐先做:
- 1k 首小验证
- 1w~5w 首中等规模验证
- 验证速度/精度/存储后,再上 30 万
## 10.6 现阶段最重要的一句话建议是什么?
> **先冻结 encoder v1,把 embedding/version/index 治理做好,再讨论 v2。**
---
## 11. 最终建议
对你现在的阶段,我的建议优先级是:
1. **冻结当前 encoder**
2. **建立 embedding 版本化规范**
3. **先做小到中规模真实集建库验证**
4. **再推进 30 万首全量建库**
5. **把新模型升级变成“离线重刷 + A/B + 切换”的标准动作**
这样做的好处是:
- 你现在就能开始用
- 后面也不会因为继续调模型把生产库拖乱
- 未来升级有明确路径,不会出现“模型变了,数据全乱了”的问题
## Sources
- [持续开发交接文档](./session-handoff.md)
- [postgresql-data-model.md](./postgresql-data-model.md)
- [phase1-implementation-checklist.md](./phase1-implementation-checklist.md)
- [phase1-worker-contract.md](./phase1-worker-contract.md)
- [acr-engine/train.py](../acr-engine/train.py)
- [acr-engine/run_demo.py](../acr-engine/run_demo.py)
- [acr-engine/src/engines/ecapa_embedder.py](../acr-engine/src/engines/ecapa_embedder.py)
- [acr-engine/src/data/manifest_tools.py](../acr-engine/src/data/manifest_tools.py)
# Session Handoff / 持续开发交接文档
> 更新:2026-06-04
> 目的:让下次启动的新 session 在 **3~10 分钟内** 明确:
> 1. 当前项目已经走到哪里
> 2. 应该先读哪些文档
> 3. 应该从哪一步开始推进
> 4. 哪些是稳定结论,哪些还是待验证缺口
> 目标:让下次启动的新 session 在 **3~10 分钟内** 知道从哪里开始。
---
## 0. 下次启动先做什么
## 1. 下次启动先做什么
如果下次启动要继续当前主线(**song-centric 真实目录 -> feature -> PostgreSQL**),先执行
优先直接跑当前主线
```bash
cd /workspace
......@@ -22,8 +17,15 @@ cd /workspace
--output-dir acr-engine/data/pgvector_eval/music20
```
或:
```bash
acr-engine/scripts/start_songcentric_shortest_path.sh 'postgres://d2:d2pass@127.0.0.1:5432/d2'
```
当前 fresh evidence:
- `song_count = 2`
- `asset_count = 2`
- `window_count = 5`
- `matcher_fingerprint_count = 5`
- `fallback_fingerprint_count = 0`
......@@ -31,198 +33,128 @@ cd /workspace
- `semantic_runtime_missing = [torch, torchaudio, transformers]`
- `import_counts = media_entity:9 / audio_object:22 / feature_fact:24 / set_membership:9`
如果只是回归历史 Phase-1 planner/worker 合同,再执行:
```bash
cd /workspace/acr-engine
/usr/local/miniconda3/bin/python scripts/run_planner_validation_commands_live.py \
--dsn 'postgres://d2:d2pass@127.0.0.1:5432/d2' \
--output data/pgvector_eval/music20/planner_validation_commands_runner_report.json
```
也可以用包装脚本:`acr-engine/scripts/start_phase1_shortest_path.sh 'postgres://d2:d2pass@127.0.0.1:5432/d2'`
当前 fresh evidence:
- `executed_count = 4`
- `all_passed = true`
这条 runner 会一次性执行:
1. `prereq_audit`
2. `worker_contract_smoke`
3. `semantic_vector_negative_matrix`
4. `asset_level_upsert_validation`
如果结果仍是:
- `downloads_root_exists = false`
- `ready_jobs = 0`
- exact = `failed/unreadable_audio_assets`
- semantic = `4/4 failed`
则说明当前优先级仍然是:
1.`/workspace/downloads` 挂载
2.`torch / torchaudio / transformers / speechbrain`
而不是回头怀疑 PostgreSQL contract。
---
## 1. 当前项目一句话状态
## 2. 当前一句话状态
项目已经从“原型能否跑通”转为:
> **4 表 song-centric schema 已在 live PostgreSQL 上真实打通了“真实目录 -> 切片 -> exact/semantic feature enrichment -> import -> feature_fact”的宿主链。**
> **面向版权保护 / 听歌识曲 / 版本归属的可演进音乐 ACR 系统。**
当前目标是让 `100w` 音频、约 `30w` 歌曲能通过稳定的数据主链和模型注册机制,支撑检索、归属、升级与回滚。
下一步最应该做的是:
> **在不破坏这条宿主链的前提下,把 semantic lane 从 runtime-aware fallback 升级到真实 MERT / MuQ adapter。**
---
## 2. 当前稳定结论
## 3. 当前稳定结论
### 技术路线
- exact lane:`Chromaprint`
- semantic baseline:`MERT-v1-95M`
- semantic challenger:`MuQ`
- `ECAPA`:historical baseline
- Phase-1:先走 **encoder-only**,先不用微调底座
### 3.1 默认物理模型
### 数据主链
```text
canonical_song -> work -> recording -> recording_asset -> audio_window
media_entity -> audio_object -> feature_fact -> set_membership
```
### 模型主链
### 3.2 默认逻辑语义
```text
model_registry -> feature_set_registry -> audio_embedding / audio_fingerprint -> retrieval_index_registry
song -> asset -> window -> fingerprint / embedding
```
### reference set 结论
- 保留 `is_reference=true`
- 但生产切换必须依赖 `reference_set_registry / reference_set_member`
- 后续 A/B、热切换、回滚都围绕 reference set 版本化进行
### 3.3 关键设计取舍
- 最终归属对象当前只要求稳定返回 `song_id`
- 同一个 `song` 下允许多个音频文件
- `window` 仍保留,因为它是切片/evidence/offset/召回最小单元
- `feature_fact` 统一承载 `fingerprint``embedding`
- Phase-1 不先训练/微调,先直接复用开源 encoder
---
## 3. 当前已经完成的关键交付
## 4. 切片 / 模型 / feature 分别在哪张表
### 文档与设计
- 文档主入口已收敛到 `README -> start-here -> session-handoff`
- SOTA 演进路径已明确
- PostgreSQL 主数据与特征模型已固定为 v2 推荐方案
- Phase-1 实施 checklist / registry bootstrap / worker contract 文档已齐备
### PostgreSQL / live contract
- `acr-engine/sql/acr_pg_schema_v2.sql` 已落地
- `model_registry / feature_set_registry / reference_set_registry` 已 live bootstrap 验证
- `audio_embedding` 的 asset-level 幂等 upsert 已 live 验证
- semantic vector-table 负例矩阵已 live 验证
- planner validation commands 已可被 runner 一键执行
### worker / script
- `run_chromaprint_job.py` 已具备真实写入路径
- `run_embedding_job.py` 已具备 preflight failure contract
- `run_phase1_prereq_audit_live.py` 已能输出 host 前置条件审计
- `run_planner_validation_commands_live.py` 已收敛最短验证链路
| 对象 | 表 | 关键字段 |
|---|---|---|
| song | `media_entity` | `entity_type='song'` |
| asset | `audio_object` | `object_type='asset'` |
| window | `audio_object` | `object_type='window'`, `parent_object_id=<asset_id>` |
| model identity | `feature_fact` | `model_name`, `model_version`, `feature_set_name` |
| fingerprint payload | `feature_fact` | `feature_type='fingerprint'`, `fingerprint_value` |
| embedding payload | `feature_fact` | `feature_type='embedding'`, `embedding_uri/vector_table_name`, `embedding_dim` |
| set routing | `set_membership` | `set_type`, `set_name`, `member_type`, `member_id` |
---
## 4. 当前明确的 blocker
### 环境 blocker
- `/workspace/downloads` 缺失
- 缺少 `torch`
- 缺少 `torchaudio`
- 缺少 `transformers`
- 缺少 `speechbrain`
### 能力 blocker
- 还未真实跑通 `MERT / MuQ` inference
- 还未完成线上融合策略
- 还未接入更大规模真实 reference set
因此当前最该优先推进的是:
> **把环境补齐,再把 semantic lane 从 guarded failure 推到真实抽特征。**
---
## 5. 下次启动先读什么
按这个顺序即可:
1. [README.md](./README.md)
2. [start-here.md](./start-here.md)
3. [acr-architecture.md](./acr-architecture.md)
4. [postgresql-data-model.md](./postgresql-data-model.md)
5. [phase1-implementation-checklist.md](./phase1-implementation-checklist.md)
6. [model-feature-registry-bootstrap.md](./model-feature-registry-bootstrap.md)
7. [phase1-worker-contract.md](./phase1-worker-contract.md)
8. [CHANGELOG.md](./CHANGELOG.md)
## 5. 当前流程图
```mermaid
flowchart TD
A[song / media_entity] --> B[asset / audio_object]
B --> C[window / audio_object]
C --> D1[fingerprint / feature_fact]
C --> D2[embedding / feature_fact]
A --> E[set_membership]
B --> E
C --> E
D1 --> F[召回与归属到 song_id]
D2 --> F
```
---
## 6. 下次启动优先动作
## 6. 当前已经真实验证过什么
### 路线 A:继续环境恢复
1. 检查 `/workspace/downloads` 是否已挂载
2. 检查 `torch / torchaudio / transformers / speechbrain` 是否已可导入
3. 重跑 planner validation runner
4. 确认 `ready_jobs` 是否从 `0` 开始提升
### live PostgreSQL
- DSN: `postgres://d2:d2pass@127.0.0.1:5432/d2`
- schema: `acr_songcentric_test`
### 路线 B:继续语义特征抽取实现
1. 查看 `acr-engine/workers/run_embedding_job.py`
2. 保持现有失败语义 contract
3. 接入真实 inference adapter
4. 复用现有 `audio_embedding` upsert 逻辑
### 路线 C:继续数据规模化落库
1. 查看 [postgresql-data-model.md](./postgresql-data-model.md)
2. 查看 [postgres_db_schema_samples.md](./postgres_db_schema_samples.md)
3. 规划 100w 音频导入批次
4. 固定 `reference_set` / `feature_set` / `index` 版本治理
### 已验证链路
1. `acr-engine/sql/acr_pg_schema_songcentric_v1.sql` 可真实建表
2. `bootstrap_songcentric_phase1_live.py` 可重复 seed
3. `import_songcentric_manifest_live.py` 可幂等导入 `song/asset/window/membership`
4. manifest 中 `windows[].features[]` 已可直接落 `feature_fact`
5. 真实目录 -> manifest -> import 已验证通过
6. 真实目录 -> fingerprint enrichment -> import 已验证通过
7. exact lane 已优先复用仓库内 `ChromaprintMatcher`
8. semantic lane 已 runtime-aware,但当前 host 因依赖缺失仍走 fallback
---
## 7. 当前不要再重复讨论的结论
## 7. 当前 host 的真实 blocker
- 不要回退到只有 `song_id` 的扁平表
- 不要把 embedding 设计成固定列
- 不要在 Phase-1 先讨论重新训练底座
- 不要把当前问题误判成 PostgreSQL schema 问题
- `torch`
- `torchaudio`
- `transformers`
- 因此当前 `semantic_runtime_available = false`
---
这说明当前主要 blocker 是:
> **语义 encoder runtime 还没就绪,不是 schema 没设计好。**
## 8. 关键文件入口
---
### 文档
- [README.md](./README.md)
- [start-here.md](./start-here.md)
- [postgresql-data-model.md](./postgresql-data-model.md)
- [phase1-worker-contract.md](./phase1-worker-contract.md)
## 8. 下次继续时先看哪些文件
### 代码与脚本
- `acr-engine/sql/acr_pg_schema_v2.sql`
- `acr-engine/workers/run_chromaprint_job.py`
- `acr-engine/workers/run_embedding_job.py`
- `acr-engine/scripts/run_planner_validation_commands_live.py`
- `acr-engine/scripts/run_phase1_prereq_audit_live.py`
1. [README.md](./README.md)
2. [start-here.md](./start-here.md)
3. [postgresql-data-model.md](./postgresql-data-model.md)
4. [postgres_db_schema_samples.md](./postgres_db_schema_samples.md)
5. [CHANGELOG.md](./CHANGELOG.md)
关键代码:
- `acr-engine/sql/acr_pg_schema_songcentric_v1.sql`
- `acr-engine/scripts/run_songcentric_directory_pipeline_live.py`
- `acr-engine/scripts/build_songcentric_manifest_from_directory.py`
- `acr-engine/scripts/enrich_songcentric_manifest_with_local_features.py`
- `acr-engine/scripts/import_songcentric_manifest_live.py`
- `acr-engine/scripts/start_songcentric_shortest_path.sh`
---
## 9. 当前验证状态摘要
### 已验证
- planner validation runner 可执行且 `all_passed = true`
- exact lane 当前会诚实落成 `failed/unreadable_audio_assets`
- semantic lane 当前会诚实落成 `preflight_failed`
- asset-level embedding upsert 幂等合同已验证
- vector table 负例矩阵已验证
- prerequisites audit 已验证
## 9. 下一步优先顺序
### 未验证
- MERT / MuQ 真实 inference
- 更大规模生产 reference set 导入
- 最终线上融合与重排策略
1. 保持当前 4 表 schema 不回退
2.`enrich_songcentric_manifest_with_local_features.py` 接真实 semantic adapter
3. 保留 fallback 分支,不破坏当前 host 的可运行性
4. 重新跑主链 runner,确认 semantic lane 有 fresh 证据
---
## 一句话 handoff
> 下次接手不要再从总方案开始,先跑 runner;若结果仍显示 downloads/runtime 缺失,就优先补环境,再推进 semantic lane 真实抽特征
> 下次不要再从总方案争论开始,直接跑 song-centric runner;如果 exact 正常、semantic 仍 fallback,就继续补真实 semantic adapter 和依赖
......
# SOTA 演进方案说明 / SOTA Evolution Guide
> 更新:2026-06-04
> 目标:给出一个“先不上微调、先用开源 encoder”的 Phase-1 路线,并明确后续如何演进到更强的版权保护 / 版本归属系统。
## 一页结论
如果当前约束是:
- 先不微调底座
- 先要落数据规范
- 先解决 100w 音频 / 30w 歌曲的检索与归属基础问题
那么最合理的 Phase-1 路线不是“重训一套新模型”,而是:
1. **保留 exact lane**:Chromaprint / fingerprint
2. **semantic lane 主底座**:MERT-v1-95M
3. **semantic lane challenger**:MuQ
4. **数据库先稳住**`model_registry + feature_set_registry + audio_embedding + retrieval_index_registry`
5. **结果先按层聚合**:window -> recording -> work -> canonical_song
---
## 1. 为什么当前要走 encoder-only Phase-1
因为你当前最紧迫的问题不是“模型精度极限”,而是:
- 曲库很大:100w 音频 / 30w 歌曲
- 数据关系复杂:同曲可能有多录音、多版本、多来源资产
- 如果数据规范不稳,未来任何模型升级都会反复返工
所以 Phase-1 目标应该是:
```mermaid
flowchart LR
A[冻结数据规范] --> B[接入开源 encoder]
B --> C[建立 semantic baseline]
C --> D[做大规模索引与聚合验证]
D --> E[再决定是否进入微调 / version lane]
```
---
## 2. 推荐的阶段划分
## Phase-0:当前仓库阶段(已具备)
- `Chromaprint + ECAPA + melody rerank`
- 可跑通训练/建索引/评测/服务闭环
- 适合作为 baseline,而不是最终生产底座
## Phase-1:Encoder-only foundation baseline(当前推荐)
- exact lane:Chromaprint
- semantic lane:MERT-v1-95M
- challenger:MuQ
- 不微调底座
- 只做 feature extraction + index + aggregation
## Phase-2:Version / Cover lane
- 在 Phase-1 数据模型稳定后
- 引入 cover/version 专门分支
- 强化 work-level 归属
## Phase-3:Industrial retrieval stack
- ANN + reranker
- online/offline artifact registry
- 监控、回放、审计、人工复核
---
## 3. Phase-1 的推荐模型组合
## 3.1 Exact lane
### 选型
- Chromaprint / landmark hash
### 作用
- 原曲片段
- 平台转码
- near-duplicate
- 局部片段强匹配
### 为什么保留
版权保护不能只靠 semantic embedding。exact lane 在很多真实投诉/取证场景里仍然是最快且证据最强的第一条路径。
---
## 3.2 Semantic lane 主模型:MERT-v1-95M
### 推荐原因
- 是 music SSL foundation model
- 已有公开论文与实现
- 比自训小型 ECAPA 更符合音乐任务底座定位
- Phase-1 直接做 frozen encoder 成本与风险都更低
### Phase-1 中的角色
- 作为主 encoder 产出 window embedding
- 负责 noisy/BGM/一般跨域检索 baseline
- 后面可继续作为 teacher 或兼容旧索引版本
### 推荐 feature set
1. `mert_v1_95m__window_5s_hop_2.5s__meanpool__l2`
2. `mert_v1_95m__window_10s_hop_5s__meanpool__l2`
### 为什么先做两套
- `5s/2.5s`:更利于局部定位
- `10s/5s`:更利于整体语义稳定
---
## 3.3 Semantic lane Challenger:MuQ
### 推荐原因
- 更新、更接近下一代 music foundation model 路线
- 值得作为 challenger baseline
- 即使不开微调,也有希望在部分 MIR 任务上优于较早底座
### 当前建议
- Phase-1 先作为对照组,不立即替代 MERT
- 重点验证:向量分布稳定性、窗口级检索表现、内存/推理成本
---
## 3.4 为什么 Phase-1 不直接以 CoverHunter 为主线
因为 CoverHunter 的优势在:
- cover song identification
- alignment / refined attention / coarse-to-fine 训练
而你当前约束是:
- 先不用微调
- 先用开源 encoder
- 先把数据和检索规范落稳
所以它更适合作为 **Phase-2 的 version/cover lane 方向**,而不是 Phase-1 的主 baseline。
---
## 4. 角色关注点
## 4.1 模型底座角色
重点关注:
- 哪些 encoder 已注册到 `model_registry`
- 每个 encoder 的 input SR、window、pooling、embedding dim
- 哪些 feature set 是线上候选,哪些只是实验候选
## 4.2 检索角色
重点关注:
- 指纹 lane 与 semantic lane 如何组合
- `recording/work/song` 聚合规则
- top-k 候选如何稳定输出
## 4.3 数据角色
重点关注:
- 资产去重
- reference 资产选择
- window manifest
- 是否支持全量重建特征与索引
## 4.4 运维 / 平台角色
重点关注:
- encoder 版本切换是否可灰度
- 索引重建是否可并行
- 热/冷索引、历史索引是否可回滚
---
## 5. Phase-1 的实施顺序
```mermaid
flowchart TD
A[冻结 PostgreSQL 数据规范] --> B[导入 canonical/work/recording/asset/window]
B --> C[注册 model_registry / feature_set_registry]
C --> D[抽取 MERT 特征]
C --> E[抽取 MuQ 特征]
D --> F[构建 semantic index]
E --> F
F --> G[与 fingerprint lane 做聚合]
G --> H[输出 canonical_song_id / work_id / recording_id]
```
---
## 6. 每阶段解决的问题
| 阶段 | 解决的问题 | 暂不解决的问题 |
|---|---|---|
| Phase-1 | 数据规范、开源底座 baseline、索引可重建、song/work/recording 聚合 | 底座微调、cover 专项训练、melody tower |
| Phase-2 | version/cover 归属、work-level recall | 更复杂跨模态 humming |
| Phase-3 | 工业化服务、回放、监控、人工审核闭环 | 极致 research SOTA |
---
## 7. 与当前仓库的关系
### 当前保留
- `ECAPA baseline`:保留做对照,不作为长期主底座
- `Chromaprint`:保留,且在版权保护场景里非常重要
- `melody rerank`:保留为辅助 lane
### 当前新增
- `model_registry`
- `feature_set_registry`
- foundation encoder 特征抽取与注册
- 更清晰的 `canonical_song / work / recording` 数据结构
---
## 8. 当前推荐结论
如果今天就要给 Phase-1 定方案,我建议:
1. **先不改训练主线,不删 ECAPA**
2. **新增 MERT-v1-95M semantic lane**
3. **新增 MuQ challenger lane**
4. **只把 `is_reference=true` 的主参考窗口先做成热索引**
5. **先把 PostgreSQL 设计当成主交付**
换句话说:
> Phase-1 的核心不是“哪一个模型最终赢”,而是“数据规范 + 模型注册 + 特征注册 + 索引注册”这套长期结构先稳定下来。
# Start Here / 新同学接手入口
> 目标:让新来的同学在 **10 分钟内**知道:先跑什么、先读什么、当前卡在哪、下一步该做什么。
> 目标:让新同学在 **10 分钟内** 知道现在的主链、先跑什么、先看什么。
---
## 1. 先执行这条命令
如果当前目标是验证 **song-centric 真实目录 -> feature -> PostgreSQL** 主链,优先跑:
```bash
cd /workspace
/usr/local/miniconda3/bin/python acr-engine/scripts/run_songcentric_directory_pipeline_live.py \
......@@ -17,167 +15,116 @@ cd /workspace
--output-dir acr-engine/data/pgvector_eval/music20
```
或:
```bash
acr-engine/scripts/start_songcentric_shortest_path.sh 'postgres://d2:d2pass@127.0.0.1:5432/d2'
```
当前 fresh evidence:
- `song_count = 2`
- `asset_count = 2`
- `window_count = 5`
- `matcher_fingerprint_count = 5`
- `fallback_fingerprint_count = 0`
- `semantic_runtime_available = false`
- `import_counts.feature_fact = 24`
如果你当前目标是验证老的 Phase-1 planner/worker 合同,再跑下面这条:
```bash
cd /workspace/acr-engine
/usr/local/miniconda3/bin/python scripts/run_planner_validation_commands_live.py \
--dsn 'postgres://d2:d2pass@127.0.0.1:5432/d2' \
--output data/pgvector_eval/music20/planner_validation_commands_runner_report.json
```
也可以用包装脚本:`acr-engine/scripts/start_phase1_shortest_path.sh 'postgres://d2:d2pass@127.0.0.1:5432/d2'`
### 当前 fresh evidence
- `executed_count = 4`
- `all_passed = true`
### 这条命令会执行
1. `prereq_audit`
2. `worker_contract_smoke`
3. `semantic_vector_negative_matrix`
4. `asset_level_upsert_validation`
### 看到下面这些结果时应该如何判断
如果你看到:
- `downloads_root_exists = false`
- `ready_jobs = 0`
- exact lane = `failed/unreadable_audio_assets`
- semantic lane = `4/4 failed`
说明当前优先级是:
1. 挂载 `/workspace/downloads`
2. 安装 `torch / torchaudio / transformers / speechbrain`
也就是说:
> 当前首要问题是运行环境前置条件,不是 PostgreSQL schema,也不是 worker contract 设计错误。
- `semantic_runtime_missing = [torch, torchaudio, transformers]`
- `import_counts = media_entity:9 / audio_object:22 / feature_fact:24 / set_membership:9`
---
## 2. 接手时只读这 5 份文档
## 2. 只读这 4 份文档
1. [README.md](./README.md)
2. [session-handoff.md](./session-handoff.md)
3. [acr-architecture.md](./acr-architecture.md)
4. [postgresql-data-model.md](./postgresql-data-model.md)
5. [phase1-implementation-checklist.md](./phase1-implementation-checklist.md)
如果你负责算法或检索,再补:
- [sota-evolution-guide.md](./sota-evolution-guide.md)
- [model-feature-registry-bootstrap.md](./model-feature-registry-bootstrap.md)
- [phase1-worker-contract.md](./phase1-worker-contract.md)
3. [postgresql-data-model.md](./postgresql-data-model.md)
4. [postgres_db_schema_samples.md](./postgres_db_schema_samples.md)
---
## 3. 用一句话理解项目
## 3. 用一句话理解当前项目
我们在做的是一个面向 **版权保护 / 听歌识曲 / 版本归属** 的音乐 ACR 系统,
目标是`100w` 音频、约 `30w` 歌曲中,快速定位正确的 `song_id` 归属;当前阶段暂不把版本/recording 作为必须返回对象
我们当前做的是一个 **面向版权保护的 song-centric 音乐 ACR 系统**
目标是在约 `100w` 音频、约 `30w` 歌曲里,把录音、BGM、片段、翻唱相关查询尽快定位到应归属的 `song_id`
---
## 4. 当前主线方案
## 4. 当前最重要的设计结论
### 检索主线
- exact lane:`Chromaprint`
- semantic lane baseline:`MERT-v1-95M`
- semantic lane challenger:`MuQ`
- historical baseline:`ECAPA`
### 4.1 不再默认走旧的多层 v2 体系
当前默认只认 4 张核心物理表:
### 当前 Phase-1 最小主线
```text
song -> asset -> window
media_entity -> audio_object -> feature_fact -> set_membership
```
### 可演进完整版主线
```text
canonical_song -> work -> recording -> recording_asset -> audio_window
```
### 4.2 逻辑语义这样理解
### 模型主线
```text
model_registry -> feature_set_registry -> audio_embedding / audio_fingerprint -> retrieval_index_registry
song -> asset -> window -> fingerprint / embedding
```
---
### 4.3 切片 / 模型 / feature 到底落哪里
## 5. 当前哪些已经稳定
| 对象 | 表 | 关键字段 |
|---|---|---|
| song | `media_entity` | `entity_type='song'` |
| 原始音频文件 | `audio_object` | `object_type='asset'` |
| 切片窗口 | `audio_object` | `object_type='window'`, `parent_object_id=<asset_id>` |
| 指纹特征 | `feature_fact` | `feature_type='fingerprint'`, `fingerprint_value` |
| embedding 特征 | `feature_fact` | `feature_type='embedding'`, `embedding_uri/vector_table_name` |
| 模型信息 | `feature_fact` | `model_name`, `model_version`, `feature_set_name` |
| reference/eval/hot 集 | `set_membership` | `set_type`, `set_name` |
- PostgreSQL v2 schema 已落地
- registry bootstrap 已有 live 验证
- worker contract 已有 live 验证
- exact / semantic 的失败语义已可审计
- planner 已能输出 validation commands
- planner validation runner 已可一键执行
---
## 6. 当前哪些还没完成
## 5. 当前主链流程图
- 还没有真正跑通 MERT / MuQ inference
- 当前 host 没有 `/workspace/downloads`
- 当前 host 缺 `torch / torchaudio / transformers / speechbrain`
- 还没完成最终线上融合策略
- 还没接入更大规模真实 reference set
```mermaid
flowchart TD
A[media_entity\nentity_type=song] --> B[audio_object\nobject_type=asset]
B --> C[audio_object\nobject_type=window]
C --> D1[feature_fact\nfingerprint]
C --> D2[feature_fact\nembedding]
B --> E[set_membership\nreference_set / eval_set / hot_set]
C --> E
```
---
## 7. 如果你现在继续推进,按这个顺序
### 路线 A:先解环境
1. 挂载 `/workspace/downloads`
2. 安装 semantic runtime 依赖
3. 重跑 planner validation runner
4. 确认 `ready_jobs` 是否开始恢复
## 6. 当前哪些已经稳定
### 路线 B:先解实现
1. 阅读 [phase1-worker-contract.md](./phase1-worker-contract.md)
2. 阅读 `acr-engine/workers/run_embedding_job.py`
3. 用真实 inference adapter 替换 guarded failure path
4. 保持当前 PostgreSQL contract 不变
### 路线 C:先解数据
1. 阅读 [postgresql-data-model.md](./postgresql-data-model.md)
2. 阅读 [postgres_db_schema_samples.md](./postgres_db_schema_samples.md)
3. 准备更大的 reference set
4. 保持 `reference_set_registry / reference_set_member` 版本化
- live PostgreSQL schema 已真实建表通过
- 真实目录 -> manifest -> import 已打通
- 真实目录 -> fingerprint enrichment -> import 已打通
- semantic lane 已做成 runtime-aware
- 当前 host 无 `torch/torchaudio/transformers` 时会明确 fallback,不会伪装成功
- 当前 exact lane 已优先复用仓库内 `ChromaprintMatcher`
---
## 8. 当前不建议优先做的事
## 7. 当前最该继续什么
- 不要重新讨论要不要 `song/work/recording` 分层
- 不要回退到只有 `song_id` 的扁平表
- 不要先讨论重新训练底座
- 不要把当前问题误判成 PostgreSQL contract 设计问题
### 第一优先级
把 semantic lane 从 fallback 升级成真实 encoder adapter,且不破坏现有宿主链。
---
### 当前 host 事实
- `torch` 缺失
- `torchaudio` 缺失
- `transformers` 缺失
- 当前因此 `semantic_runtime_available = false`
## 9. 仓库常用入口
---
### 文档
- [README.md](./README.md)
- [session-handoff.md](./session-handoff.md)
- [postgresql-data-model.md](./postgresql-data-model.md)
- [postgres_db_schema_samples.md](./postgres_db_schema_samples.md)
- [phase1-worker-contract.md](./phase1-worker-contract.md)
## 8. 当前不要再绕回去的点
### 脚本
- `acr-engine/scripts/run_planner_validation_commands_live.py`
- `acr-engine/scripts/run_phase1_prereq_audit_live.py`
- `acr-engine/scripts/run_phase1_worker_contract_smoke_live.py`
- `acr-engine/scripts/run_embedding_vector_table_negative_matrix_live.py`
- `acr-engine/scripts/validate_audio_embedding_asset_upsert_live.py`
- `acr-engine/scripts/run_songcentric_directory_pipeline_live.py`
- 不要回退到旧的 v2 schema 作为默认口径
- 不要重新引入 `recording/work/version` 作为 Phase-1 必须返回对象
- 不要先讨论训练/微调
- 不要把“模型底座可替换”误解成“数据库要重新拆很多层”
---
## 一句话结论
> 新同学接手时,先跑 runner,再读 5 份核心文档;当前首要问题是环境前置条件,不是 schema/contract 本身
> 当前最重要的是守住 4 表 song-centric 主链,并在这个主链上把 semantic encoder 真正接起来
......