Commit a549d1de a549d1de68fcc155b15a214b993157f3676db3b1 by cnb.bofCdSsphPA

Clarify the ACR evolution path and freeze a production-grade data model

Constraint: Phase-1 must support encoder-only open-source backbones without destabilizing future schema evolution
Rejected: extending the old flat song_id + fixed-vector schema | would couple model swaps to schema rewrites and weaken copyright lineage
Confidence: high
Scope-risk: moderate
Directive: treat canonical_song/work/recording/recording_asset/audio_window plus model/feature registries as the stable contract; evolve models and indexes around them
Tested: git diff --check on changed files; Python content/structure sanity check; architect review APPROVED; README link coverage and DDL object presence verified
Not-tested: live PostgreSQL apply not run because psql is unavailable in this environment
1 parent 2898ef26
-- ACR PostgreSQL Schema V2
-- Purpose:
-- 1. Support canonical_song/work/recording/asset/window hierarchy
-- 2. Support encoder-first evolution via model_registry + feature_set_registry
-- 3. Support pgvector-backed hot reference sets without binding the entire system to one vector table
CREATE EXTENSION IF NOT EXISTS vector;
-- =========================================================
-- 1. Canonical business entities
-- =========================================================
CREATE TABLE IF NOT EXISTS canonical_song (
canonical_song_id BIGSERIAL PRIMARY KEY,
biz_song_code TEXT UNIQUE,
title TEXT NOT NULL,
title_norm TEXT,
primary_artist TEXT,
primary_artist_norm TEXT,
language_code TEXT,
rights_status TEXT,
metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS work (
work_id BIGSERIAL PRIMARY KEY,
canonical_song_id BIGINT NOT NULL REFERENCES canonical_song(canonical_song_id),
work_code TEXT UNIQUE,
work_title TEXT NOT NULL,
work_title_norm TEXT,
composer TEXT,
lyricist TEXT,
publisher TEXT,
iswc TEXT,
metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS recording (
recording_id BIGSERIAL PRIMARY KEY,
work_id BIGINT NOT NULL REFERENCES work(work_id),
canonical_song_id BIGINT NOT NULL REFERENCES canonical_song(canonical_song_id),
recording_code TEXT UNIQUE,
recording_title TEXT,
artist_name TEXT,
album_name TEXT,
version_type TEXT NOT NULL,
is_reference BOOLEAN NOT NULL DEFAULT FALSE,
reference_priority INTEGER NOT NULL DEFAULT 100,
release_date DATE,
isrc TEXT,
duration_sec NUMERIC(10,3),
metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- =========================================================
-- 2. Assets and windows
-- =========================================================
CREATE TABLE IF NOT EXISTS recording_asset (
asset_id BIGSERIAL PRIMARY KEY,
recording_id BIGINT NOT NULL REFERENCES recording(recording_id),
asset_role TEXT NOT NULL,
storage_uri TEXT NOT NULL,
storage_scheme TEXT NOT NULL,
file_ext TEXT,
mime_type TEXT,
file_size_bytes BIGINT,
audio_sha256 TEXT,
sample_rate INTEGER,
channels INTEGER,
bit_rate_kbps INTEGER,
codec_name TEXT,
duration_sec NUMERIC(10,3),
loudness_lufs NUMERIC(8,3),
normalized_storage_uri TEXT,
ingest_status TEXT NOT NULL DEFAULT 'ready',
metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_recording_asset_sha256
ON recording_asset(audio_sha256)
WHERE audio_sha256 IS NOT NULL;
CREATE TABLE IF NOT EXISTS audio_window (
window_id BIGSERIAL PRIMARY KEY,
asset_id BIGINT NOT NULL REFERENCES recording_asset(asset_id),
recording_id BIGINT NOT NULL REFERENCES recording(recording_id),
work_id BIGINT NOT NULL REFERENCES work(work_id),
canonical_song_id BIGINT NOT NULL REFERENCES canonical_song(canonical_song_id),
window_index INTEGER NOT NULL,
start_sec NUMERIC(10,3) NOT NULL,
end_sec NUMERIC(10,3) NOT NULL,
duration_sec NUMERIC(10,3) NOT NULL,
segment_role TEXT NOT NULL DEFAULT 'reference',
segment_type TEXT,
quality_score NUMERIC(8,5),
active_for_index BOOLEAN NOT NULL DEFAULT TRUE,
metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_audio_window_asset_idx
ON audio_window(asset_id, window_index);
-- =========================================================
-- 3. Model and feature registries
-- =========================================================
CREATE TABLE IF NOT EXISTS model_registry (
model_id BIGSERIAL PRIMARY KEY,
model_name TEXT NOT NULL,
model_family TEXT NOT NULL,
model_version TEXT NOT NULL,
model_source TEXT,
model_uri TEXT,
license_name TEXT,
input_modality TEXT NOT NULL DEFAULT 'audio',
input_sample_rate INTEGER,
input_channel_mode TEXT DEFAULT 'mono',
default_window_sec NUMERIC(10,3),
default_hop_sec NUMERIC(10,3),
output_embedding_dim INTEGER,
pooling_supported TEXT[],
layer_selection_supported BOOLEAN NOT NULL DEFAULT FALSE,
is_trainable BOOLEAN NOT NULL DEFAULT FALSE,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(model_name, model_version)
);
CREATE TABLE IF NOT EXISTS feature_set_registry (
feature_set_id BIGSERIAL PRIMARY KEY,
model_id BIGINT NOT NULL REFERENCES model_registry(model_id),
feature_name TEXT NOT NULL,
feature_level TEXT NOT NULL,
extraction_granularity TEXT NOT NULL,
window_sec NUMERIC(10,3),
hop_sec NUMERIC(10,3),
embedding_dim INTEGER,
pooling_strategy TEXT,
layer_selection TEXT,
normalize_l2 BOOLEAN NOT NULL DEFAULT TRUE,
distance_metric TEXT NOT NULL,
quantization_type TEXT,
feature_schema_version TEXT NOT NULL,
config_json JSONB NOT NULL DEFAULT '{}'::jsonb,
status TEXT NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS feature_extraction_job (
extraction_job_id BIGSERIAL PRIMARY KEY,
feature_set_id BIGINT NOT NULL REFERENCES feature_set_registry(feature_set_id),
target_scope TEXT NOT NULL,
job_status TEXT NOT NULL DEFAULT 'pending',
shard_key TEXT,
input_count BIGINT,
output_count BIGINT,
started_at TIMESTAMPTZ,
finished_at TIMESTAMPTZ,
log_uri TEXT,
metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- =========================================================
-- 4. Feature facts
-- =========================================================
CREATE TABLE IF NOT EXISTS audio_embedding (
embedding_id BIGSERIAL PRIMARY KEY,
feature_set_id BIGINT NOT NULL REFERENCES feature_set_registry(feature_set_id),
extraction_job_id BIGINT REFERENCES feature_extraction_job(extraction_job_id),
asset_id BIGINT REFERENCES recording_asset(asset_id),
window_id BIGINT REFERENCES audio_window(window_id),
recording_id BIGINT NOT NULL REFERENCES recording(recording_id),
work_id BIGINT NOT NULL REFERENCES work(work_id),
canonical_song_id BIGINT NOT NULL REFERENCES canonical_song(canonical_song_id),
embedding_storage_mode TEXT NOT NULL,
embedding_uri TEXT,
vector_norm NUMERIC(12,6),
checksum TEXT,
is_indexed BOOLEAN NOT NULL DEFAULT FALSE,
metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT ck_audio_embedding_scope CHECK (asset_id IS NOT NULL OR window_id IS NOT NULL)
);
CREATE TABLE IF NOT EXISTS audio_embedding_vector_192 (
embedding_id BIGINT PRIMARY KEY REFERENCES audio_embedding(embedding_id) ON DELETE CASCADE,
embedding VECTOR(192) NOT NULL
);
CREATE TABLE IF NOT EXISTS audio_embedding_vector_768 (
embedding_id BIGINT PRIMARY KEY REFERENCES audio_embedding(embedding_id) ON DELETE CASCADE,
embedding VECTOR(768) NOT NULL
);
CREATE TABLE IF NOT EXISTS audio_fingerprint (
fingerprint_id BIGSERIAL PRIMARY KEY,
feature_set_id BIGINT NOT NULL REFERENCES feature_set_registry(feature_set_id),
asset_id BIGINT REFERENCES recording_asset(asset_id),
window_id BIGINT REFERENCES audio_window(window_id),
recording_id BIGINT NOT NULL REFERENCES recording(recording_id),
work_id BIGINT NOT NULL REFERENCES work(work_id),
canonical_song_id BIGINT NOT NULL REFERENCES canonical_song(canonical_song_id),
fingerprint_uri TEXT,
hash_count INTEGER,
is_indexed BOOLEAN NOT NULL DEFAULT FALSE,
metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS reference_set_registry (
reference_set_id BIGSERIAL PRIMARY KEY,
set_name TEXT NOT NULL UNIQUE,
description TEXT,
encoder_scope TEXT,
status TEXT NOT NULL DEFAULT 'active',
metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS reference_set_member (
reference_set_id BIGINT NOT NULL REFERENCES reference_set_registry(reference_set_id) ON DELETE CASCADE,
recording_id BIGINT NOT NULL REFERENCES recording(recording_id) ON DELETE CASCADE,
member_role TEXT NOT NULL DEFAULT 'hot_reference',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY(reference_set_id, recording_id)
);
-- =========================================================
-- 4.5 Lineage invariants (recommended production hardening)
-- =========================================================
CREATE OR REPLACE FUNCTION check_recording_lineage()
RETURNS trigger AS $$
DECLARE
work_song_id BIGINT;
BEGIN
SELECT canonical_song_id INTO work_song_id
FROM work
WHERE work_id = NEW.work_id;
IF work_song_id IS NULL THEN
RAISE EXCEPTION 'Invalid work_id=% for recording', NEW.work_id;
END IF;
IF NEW.canonical_song_id <> work_song_id THEN
RAISE EXCEPTION 'recording.canonical_song_id % mismatches work.canonical_song_id %', NEW.canonical_song_id, work_song_id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION check_audio_window_lineage()
RETURNS trigger AS $$
DECLARE
asset_recording_id BIGINT;
rec_work_id BIGINT;
rec_song_id BIGINT;
BEGIN
SELECT recording_id INTO asset_recording_id
FROM recording_asset
WHERE asset_id = NEW.asset_id;
SELECT work_id, canonical_song_id INTO rec_work_id, rec_song_id
FROM recording
WHERE recording_id = NEW.recording_id;
IF asset_recording_id IS NULL OR rec_work_id IS NULL THEN
RAISE EXCEPTION 'Invalid asset_id=% or recording_id=% for audio_window', NEW.asset_id, NEW.recording_id;
END IF;
IF NEW.recording_id <> asset_recording_id THEN
RAISE EXCEPTION 'audio_window.recording_id % mismatches recording_asset.recording_id %', NEW.recording_id, asset_recording_id;
END IF;
IF NEW.work_id <> rec_work_id OR NEW.canonical_song_id <> rec_song_id THEN
RAISE EXCEPTION 'audio_window lineage mismatch for recording_id=%', NEW.recording_id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION check_audio_embedding_lineage()
RETURNS trigger AS $$
DECLARE
parent_recording_id BIGINT;
parent_work_id BIGINT;
parent_song_id BIGINT;
BEGIN
IF NEW.window_id IS NOT NULL THEN
SELECT recording_id, work_id, canonical_song_id
INTO parent_recording_id, parent_work_id, parent_song_id
FROM audio_window
WHERE window_id = NEW.window_id;
ELSE
SELECT r.recording_id, r.work_id, r.canonical_song_id
INTO parent_recording_id, parent_work_id, parent_song_id
FROM recording_asset ra
JOIN recording r ON r.recording_id = ra.recording_id
WHERE ra.asset_id = NEW.asset_id;
END IF;
IF parent_recording_id IS NULL THEN
RAISE EXCEPTION 'Invalid parent reference for audio_embedding';
END IF;
IF NEW.recording_id <> parent_recording_id OR NEW.work_id <> parent_work_id OR NEW.canonical_song_id <> parent_song_id THEN
RAISE EXCEPTION 'audio_embedding lineage mismatch';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- =========================================================
-- 5. Retrieval index and results
-- =========================================================
CREATE TABLE IF NOT EXISTS retrieval_index_registry (
retrieval_index_id BIGSERIAL PRIMARY KEY,
feature_set_id BIGINT NOT NULL REFERENCES feature_set_registry(feature_set_id),
index_name TEXT NOT NULL,
index_backend TEXT NOT NULL,
index_type TEXT NOT NULL,
storage_uri TEXT,
shard_no INTEGER,
row_count BIGINT,
index_status TEXT NOT NULL DEFAULT 'active',
config_json JSONB NOT NULL DEFAULT '{}'::jsonb,
built_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS retrieval_candidate (
retrieval_candidate_id BIGSERIAL PRIMARY KEY,
query_id TEXT NOT NULL,
retrieval_index_id BIGINT REFERENCES retrieval_index_registry(retrieval_index_id),
feature_set_id BIGINT REFERENCES feature_set_registry(feature_set_id),
source_lane TEXT NOT NULL CHECK (source_lane IN ('fingerprint', 'semantic', 'cover', 'melody', 'fusion')),
candidate_level TEXT NOT NULL CHECK (candidate_level IN ('window', 'recording', 'work', 'canonical_song')),
candidate_id BIGINT NOT NULL,
evidence_window_id BIGINT REFERENCES audio_window(window_id),
raw_score NUMERIC(14,8),
normalized_score NUMERIC(14,8),
rank_no INTEGER,
metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS match_decision (
match_decision_id BIGSERIAL PRIMARY KEY,
query_id TEXT NOT NULL,
canonical_song_id BIGINT REFERENCES canonical_song(canonical_song_id),
work_id BIGINT REFERENCES work(work_id),
recording_id BIGINT REFERENCES recording(recording_id),
decision_status TEXT NOT NULL,
decision_score NUMERIC(14,8),
decision_reason TEXT,
metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TRIGGER trg_recording_lineage
BEFORE INSERT OR UPDATE ON recording
FOR EACH ROW EXECUTE FUNCTION check_recording_lineage();
CREATE TRIGGER trg_audio_window_lineage
BEFORE INSERT OR UPDATE ON audio_window
FOR EACH ROW EXECUTE FUNCTION check_audio_window_lineage();
CREATE TRIGGER trg_audio_embedding_lineage
BEFORE INSERT OR UPDATE ON audio_embedding
FOR EACH ROW EXECUTE FUNCTION check_audio_embedding_lineage();
-- =========================================================
-- 6. Recommended indexes
-- =========================================================
CREATE INDEX IF NOT EXISTS idx_work_canonical_song_id
ON work(canonical_song_id);
CREATE INDEX IF NOT EXISTS idx_recording_work_id
ON recording(work_id);
CREATE INDEX IF NOT EXISTS idx_recording_canonical_song_id
ON recording(canonical_song_id);
CREATE INDEX IF NOT EXISTS idx_recording_reference
ON recording(is_reference, reference_priority);
CREATE INDEX IF NOT EXISTS idx_recording_asset_recording_id
ON recording_asset(recording_id);
CREATE INDEX IF NOT EXISTS idx_audio_window_asset_id
ON audio_window(asset_id);
CREATE INDEX IF NOT EXISTS idx_audio_window_recording_id
ON audio_window(recording_id);
CREATE INDEX IF NOT EXISTS idx_audio_window_canonical_song_id
ON audio_window(canonical_song_id);
CREATE INDEX IF NOT EXISTS idx_audio_window_active_for_index
ON audio_window(active_for_index);
CREATE INDEX IF NOT EXISTS idx_audio_embedding_feature_set_id
ON audio_embedding(feature_set_id);
CREATE INDEX IF NOT EXISTS idx_audio_embedding_window_id
ON audio_embedding(window_id);
CREATE INDEX IF NOT EXISTS idx_audio_embedding_recording_id
ON audio_embedding(recording_id);
CREATE INDEX IF NOT EXISTS idx_reference_set_member_recording_id
ON reference_set_member(recording_id);
CREATE INDEX IF NOT EXISTS idx_retrieval_candidate_query_id
ON retrieval_candidate(query_id);
CREATE INDEX IF NOT EXISTS idx_match_decision_query_id
ON match_decision(query_id);
-- Optional hot-set HNSW indexes for pgvector-backed online retrieval
CREATE INDEX IF NOT EXISTS idx_audio_embedding_vector_192_cos_hnsw
ON audio_embedding_vector_192 USING hnsw (embedding vector_cosine_ops);
CREATE INDEX IF NOT EXISTS idx_audio_embedding_vector_768_cos_hnsw
ON audio_embedding_vector_768 USING hnsw (embedding vector_cosine_ops);
## 2026-06-04
- 重构文档主阅读路径,新增按角色划分的文档入口:架构、开发、运维、模型底座。
- 新增 [SOTA 演进方案说明](./sota-evolution-guide.md),明确 Phase-1 encoder-only 路线、MERT/MuQ 角色与后续 version/cover 演进。
- 重写 [ACR 系统蓝图](./acr-architecture.md),补充角色视图、离线/在线职责分工与当前实现到目标实现的映射。
- 新增 [PostgreSQL 数据模型与 DDL 设计说明](./postgresql-data-model.md),补充设计意图、解决的问题、流程图与实施顺序。
- 新增 `acr-engine/sql/acr_pg_schema_v2.sql`,提供面向 `canonical_song/work/recording/asset/window + model_registry/feature_set_registry` 的推荐版 PostgreSQL DDL。
- 根据 architect 复核意见补充:`recording_asset` 术语统一、reference set 版本化对象、候选枚举约束、以及关键 lineage trigger 设计。
- 新增 `acr-engine/scripts/export_workspace_music20_embeddings_jsonl.py``acr-engine/scripts/evaluate_songid_pgvector_path.py`,补齐 song_id 级 pgvector 评测脚手架。
- 新增 `acr-engine/data/pgvector_eval/music20/` 评测产物,当前 `faiss-as-pgvector-standin` 结果:整体 `top1=0.9091``top3=0.9545`;其中 `query_type=1` 很强(`top1=1.0`),`query_type=7` 仍明显偏弱(`top1=0.0``top3=0.5`)。
- 新增 `acr-engine/data/local_eval/voice_workspace20_type7_eval.json`,对当前 `workspace_music20` 语义做了 20 条 `type_7` 批量验证:`top1=0.0``top3=0.05`,说明业务 song_id 正确性仍明显不足。
......
# ACR Docs Overview
> 保留最新架构与最短落地入口。历史细节仍在仓库中,但默认阅读只保留下面 6 份主文档
> 面向“版权保护 / 听歌识曲 / 版本归属”的音乐 ACR 文档入口。默认先看主路径,历史细节文档作为补充材料保留
## 最短阅读顺序
## 一页结论
1. [session-handoff.md](./session-handoff.md)
2. [CHANGELOG.md](./CHANGELOG.md)
当前项目已经从“原型是否能跑通”转向“**如何把 100w 音频 / 30w 歌曲做成可演进的版权检索系统**”。
默认阅读顺序不再按“训练脚本 -> demo”,而按:
1. **系统蓝图**:当前系统是什么、未来要演进成什么
2. **SOTA 演进**:Phase-1 不微调底座时怎么做,后面如何升级
3. **PostgreSQL 数据模型**:资产、窗口、特征、索引、匹配结果如何落盘
4. **现有实现对照**:当前仓库代码和文档分别在哪
---
## 主阅读路径(推荐)
### 1. 管理 / 架构 / 跨团队负责人
1. [acr-architecture.md](./acr-architecture.md)
2. [sota-evolution-guide.md](./sota-evolution-guide.md)
3. [postgresql-data-model.md](./postgresql-data-model.md)
4. [session-handoff.md](./session-handoff.md)
### 2. 开发 / 数据 / 检索工程师
1. [postgresql-data-model.md](./postgresql-data-model.md)
2. [training-data-and-pgvector-guide.md](./training-data-and-pgvector-guide.md)
3. [acr-architecture.md](./acr-architecture.md)
4. [dataset-spec.md](./dataset-spec.md)
5. [training-data-and-pgvector-guide.md](./training-data-and-pgvector-guide.md)
6. [runbook.md](./runbook.md)
4. [runbook.md](./runbook.md)
### 3. 运维 / 平台 / 服务工程师
1. [acr-architecture.md](./acr-architecture.md)
2. [postgresql-data-model.md](./postgresql-data-model.md)
3. [service-api.md](./service-api.md)
4. [runbook.md](./runbook.md)
### 4. 模型 / 底座 / 研究工程师
1. [sota-research-2026.md](./sota-research-2026.md)
2. [sota-evolution-guide.md](./sota-evolution-guide.md)
3. [production-encoder-freeze-and-embedding-strategy.md](./production-encoder-freeze-and-embedding-strategy.md)
4. [training-data-and-pgvector-guide.md](./training-data-and-pgvector-guide.md)
---
## 新的核心文档分工
| 文档 | 作用 | 适合谁先读 |
|---|---|---|
| [acr-architecture.md](./acr-architecture.md) | 当前系统蓝图、角色分工、在线/离线链路 | 架构、开发、运维 |
| [sota-evolution-guide.md](./sota-evolution-guide.md) | SOTA 演进路径、Phase-1 encoder-only 方案、后续升级路线 | 架构、模型、检索 |
| [postgresql-data-model.md](./postgresql-data-model.md) | PostgreSQL 数据字典、DDL 设计意图、流程图、查询路径 | 数据、后端、检索、平台 |
| [training-data-and-pgvector-guide.md](./training-data-and-pgvector-guide.md) | 当前训练/manifest/pgvector 原型链说明 | 开发、数据 |
| [session-handoff.md](./session-handoff.md) | 最新状态与续跑上下文 | 新 session 接手人 |
---
## 当前实现与未来目标的关系
```mermaid
flowchart LR
A[当前实现\nChromaprint + ECAPA + Melody Rerank] --> B[Phase-1\nEncoder-only Foundation Backbone]
B --> C[Phase-2\nVersion/Cover Lane + Better Aggregation]
C --> D[Phase-3\nIndustrial Retrieval + Reranker + Governance]
```
- **当前实现** 已验证基础链路可运行。
- **Phase-1** 目标是:不微调底座,直接上更强开源 encoder,并把 PostgreSQL 数据规范先落稳。
- **Phase-2** 目标是:增强 version / cover / hard-case 归属能力。
- **Phase-3** 目标是:多索引、多角色协作、数据治理、服务化上线。
---
## 当前推荐只看这几类
## 现有实现入口
### 1. 项目架构
- [acr-architecture.md](./acr-architecture.md)
- [session-handoff.md](./session-handoff.md)
### 代码入口
- `acr-engine/src/engines/chromaprint_matcher.py`
- `acr-engine/src/engines/ecapa_embedder.py`
- `acr-engine/src/engines/hybrid_engine.py`
- `acr-engine/src/service/app.py`
- `acr-engine/sql/pgvector_schema.sql`(原型版)
- `acr-engine/sql/acr_pg_schema_v2.sql`(本轮新增的推荐版)
### 2. 数据与评测
- [dataset-spec.md](./dataset-spec.md)
- [training-data-and-pgvector-guide.md](./training-data-and-pgvector-guide.md)
- [open-dataset-workflow.md](./open-dataset-workflow.md)
### 历史/补充文档
- [sota-research-2026.md](./sota-research-2026.md)
- [production-encoder-freeze-and-embedding-strategy.md](./production-encoder-freeze-and-embedding-strategy.md)
- [project-responsibility-map.md](./project-responsibility-map.md)
- [industrialization-roadmap.md](./industrialization-roadmap.md)
### 3. 运行与服务
- [runbook.md](./runbook.md)
- [service-api.md](./service-api.md)
---
### 4. 最新 hard-case 结论
- [acr-hard-case-analysis.md](../acr-engine/../docs/acr-hard-case-analysis.md)
## 如何理解当前文档体系
## 当前架构一句话
- **主文档**:优先保证“读完就知道怎么推进”
- **历史文档**:保留实验上下文、旧方案与补充解释
- **SQL 文件**:保证可以直接落地数据库原型
- `/workspace`:样本与素材来源
- `acr-engine/`:训练、索引、识别、服务主工程
- 本地小样本验证:优先 **FAISS**
- 生产向量检索:统一 **pgvector**
如果你只读 3 份:
1. [acr-architecture.md](./acr-architecture.md)
2. [sota-evolution-guide.md](./sota-evolution-guide.md)
3. [postgresql-data-model.md](./postgresql-data-model.md)
......
# ACR 系统架构图
# ACR 系统蓝图 / Architecture Blueprint
> 更新:2026-06-02
> 更新:2026-06-04
> 目标:把当前 ACR 原型、未来 SOTA 演进路径、以及不同角色的关注点统一到一份可读的系统蓝图里。
## 一页结论
- 识别链路已不是单一模型,而是 **指纹 + 向量 + melody-aware rerank** 的混合结构
- 数据与服务已经进入工业化演进阶段
- 当前主短板在:`humming_like``confused` 的 hard-case 精度
当前仓库已经验证了一个可运行的混合识别原型:
- `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. 总体架构
## 1. 总体系统
```mermaid
flowchart LR
Q[Query Audio] --> P[Preprocess]
P --> F1[Chromaprint Features]
P --> F2[128-Mel Features]
P --> F3[Melody Signature]
F1 --> M1[Fingerprint Matcher]
F2 --> M2[ECAPA + BandSplit Embedder]
F3 --> M3[Melody Similarity]
flowchart TD
A[Audio Sources\n官方母带 / 平台音频 / 抓取音频 / UGC / 录音] --> B[Asset Normalization]
B --> C[Canonical Data Model\nSong / Work / Recording / Asset / Window]
C[Catalog References] --> I1[Fingerprint Index]
C --> I2[Embedding Window Index]
C --> I3[Reference Melody Cache]
C --> D1[Exact Lane\nChromaprint / Neural AFP]
C --> D2[Semantic Lane\nFoundation Encoder]
C --> D3[Version/Cover Lane\nPhase-2+]
M1 --> H[Hybrid Fusion]
M2 --> H
M3 --> H
D1 --> E[Candidate Aggregation]
D2 --> E
D3 --> E
H --> O[Top-K + Reject]
E --> F[Canonical Song Decision]
F --> G[Service / Review / Audit]
```
---
## 2. 在线/离线分层图
## 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 主数据 + 可替换向量/索引层 |
| 服务目标 | 验证闭环 | 版权保护 / 归属判断 / 工业化运维 |
---
## 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)
- [training-data-and-pgvector-guide.md](./training-data-and-pgvector-guide.md)
---
## 3.3 运维 / 平台角色
关注:
- 离线任务:抽特征、建索引、重建索引
- 在线服务:召回、聚合、缓存、可观测性
- 存储分层:对象存储、PostgreSQL、索引后端
- 版本化:encoder 变更如何灰度、回滚、双写/双索引
最该读:
- 本文
- [service-api.md](./service-api.md)
- [postgresql-data-model.md](./postgresql-data-model.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)
- [sota-research-2026.md](./sota-research-2026.md)
- [production-encoder-freeze-and-embedding-strategy.md](./production-encoder-freeze-and-embedding-strategy.md)
---
## 4. 离线 / 在线职责拆分
```mermaid
flowchart TD
A[Offline Pipeline] --> A1[Dataset Prep]
A --> A2[Training]
A --> A3[Index Build]
A --> A4[Benchmark]
B[Online Service] --> B1[/health]
B --> B2[/recognize]
B --> B3[/index/build]
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` + 证据
---
## 3. 关键模块表
## 5. 为什么必须把角色拆开
| 模块 | 输入 | 输出 | 作用 |
|---|---|---|---|
| Preprocess | wav | mel/chroma/f0 | 统一特征入口 |
| Fingerprint Matcher | query audio | chroma candidates | 快速召回 |
| ECAPA Embedder | mel | embeddings | 语义向量检索 |
| Melody Similarity | query/ref melody | melody score | 哼唱场景补强 |
| Hybrid Fusion | multi-scores | ranked candidates | 综合排序 |
| Service API | request | JSON result | 对外调用 |
因为这个项目已经不是单一模型脚本,而是:
1. **数据治理系统**:谁的音频、属于哪个 recording/work/song
2. **检索系统**:如何从 query 找到候选
3. **判定系统**:最终输出哪一个 `canonical_song_id`
4. **服务系统**:如何对外提供 API 与可观测性
5. **演进系统**:底座模型会变,但数据规范不能跟着乱变
---
## 4. 当前设计重点
## 6. 当前阶段建议
### 当前最重要的不是继续改训练,而是:
### 4.1 为什么是混合结构
纯指纹对哼唱弱,纯 embedding 对局部强匹配和解释性不足,因此使用混合结构更稳妥。
1. 先把 PostgreSQL 数据规范稳定下来
2. 先把 `model_registry / feature_set_registry` 结构打稳
3. Phase-1 用开源 encoder 直接做 semantic lane baseline
4. 保留当前 ECAPA 作为历史 baseline / 对照组
### 4.2 为什么加入 melody-aware
目前 hard-case 主要在哼唱/近旋律混淆,因此用 melody signature 做辅助排序。
### 当前系统中的保留项
- `Chromaprint`:保留
- `ECAPA baseline`:保留为对照组
- `melody rerank`:保留为补充 lane,不再作为主演进方向
### 4.3 为什么要 window-level index
整曲平均 embedding 会损失局部片段信息;window-level 更贴近 ACR 场景。
### 当前系统中的升级项
- semantic lane 主 encoder -> foundation model
- pgvector 原型 schema -> 可扩展 PostgreSQL 数据模型
- 扁平 song_id -> canonical/work/recording/recording_asset/audio_window
---
## 5. 细节附录
## 7. 与代码的映射
代码映射:
- `src/engines/chromaprint_matcher.py`
- `src/engines/ecapa_embedder.py`
- `src/engines/hybrid_engine.py`
- `src/service/app.py`
| 代码/文档 | 当前角色 |
|---|---|
| `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. 阅读建议
## Sources
- See `docs/references-and-sources.md` for the current source map.
如果你是:
- **架构负责人**:下一篇看 [sota-evolution-guide.md](./sota-evolution-guide.md)
- **数据/后端负责人**:下一篇看 [postgresql-data-model.md](./postgresql-data-model.md)
- **模型负责人**:先看 [sota-evolution-guide.md](./sota-evolution-guide.md) 再回到 [sota-research-2026.md](./sota-research-2026.md)
......
# PostgreSQL 数据模型与 DDL 设计说明
> 更新: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 无法统一聚合的问题**
---
## 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`
---
## 1.2 为什么不用“reference_embeddings / query_embeddings”那种原型表继续扩
因为原型表有几个限制:
1. 维度写死,如 `vector(192)`
2. 数据对象太扁平,只围绕 `song_id`
3. 无法优雅支持多个 encoder
4. 无法表达同一 recording 下的多资产、多窗口、多 feature_set
所以原型版 SQL 适合 demo,不适合你现在的 100w 音频目标。
---
## 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. 关键表说明
## 4.1 `canonical_song`
最终业务主键。
用途:
- 服务最终返回 `canonical_song_id`
- 权利归属、产品展示、对外业务都以它为准
## 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`
- 保留判定理由与分数
---
## 5. 示例流程图
## 5.1 离线建库流程
```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]
```
## 5.2 在线检索流程
```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 热索引
---
## 6. 推荐 DDL 的主要原则
## 原则 1:对象关系稳定,模型可变
稳定的是:
- `song/work/recording/asset/window`
可变的是:
- `model_name`
- `feature_set`
- `index_backend`
## 原则 2:向量不要写死为唯一真相
推荐把向量事实拆成:
- PostgreSQL 元数据主表
- 向量可在 pgvector 分表或外部文件中存放
## 原则 3:窗口是最小证据粒度
因为版权保护最终不只是“命中这首歌”,还要回答:
- 命中的是哪一段
- 哪个录音版本
- 归属到哪个 work/song
---
## 7. 推荐的物理实现思路
## 7.1 PostgreSQL 负责
- 主数据
- 模型注册
- 特征注册
- 索引注册
- 检索候选
- 审核/决策
## 7.2 pgvector 负责
- 热 reference 集合
- 线上低延迟近邻查询
## 7.3 外部对象存储/文件层负责
- 原始音频
- 标准化音频
- 大体量 embedding parquet/npy
- 索引 shard 文件
---
## 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` |
所以 schema 的目标是:
> **允许模型变、索引变、特征变,但不让主数据和业务归属逻辑跟着崩。**
---
## 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)
建议视为:
- 原型版 / demo 版 / 兼容参考
---
## 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 落库,最推荐的做法是:
1. 先把 `song/work/recording/asset/window` 落稳
2. 同时把 `model_registry / feature_set_registry` 落稳
3. Phase-1 只注册开源 encoder feature set,不写死到某个 embedding 列
4. 先把热 reference 集上 pgvector,冷数据通过外部文件或后续索引层接入
# 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 的核心不是“哪一个模型最终赢”,而是“数据规范 + 模型注册 + 特征注册 + 索引注册”这套长期结构先稳定下来。