feat(dashboard): 新增音频播放器及决策类型筛选功能
- 在L2审核面板新增音频播放器组件,实现播放、进度条、时间显示等交互 - 音频播放器支持多个实例,点击播放按钮互斥播放 - 页面布局中拆分详情为粘性区域和可滚动区域,更好展示新入库歌词和召回候选音频 - 后端接口增强,返回查询和候选音频URL数据支持前端播放 - 决策过滤条件扩展,支持l1命中、l2重复、l2审核、冲突、新歌、跳过等更多分类 - 更新筛选逻辑和选项标签,提升用户筛选灵活性和准确性
Showing
2 changed files
with
218 additions
and
17 deletions
| ... | @@ -115,9 +115,100 @@ | ... | @@ -115,9 +115,100 @@ |
| 115 | 115 | ||
| 116 | .content { | 116 | .content { |
| 117 | overflow: auto; | 117 | overflow: auto; |
| 118 | padding: 0; | ||
| 119 | } | ||
| 120 | |||
| 121 | .sticky-info { | ||
| 122 | position: sticky; | ||
| 123 | top: 0; | ||
| 124 | z-index: 3; | ||
| 125 | background: var(--bg); | ||
| 126 | padding: 14px 18px 0; | ||
| 127 | } | ||
| 128 | |||
| 129 | .scroll-detail { | ||
| 118 | padding: 14px 18px 24px; | 130 | padding: 14px 18px 24px; |
| 119 | } | 131 | } |
| 120 | 132 | ||
| 133 | .audio-player { | ||
| 134 | display: flex; | ||
| 135 | align-items: center; | ||
| 136 | gap: 10px; | ||
| 137 | padding: 8px 10px; | ||
| 138 | background: #fbfcfd; | ||
| 139 | border: 1px solid var(--line); | ||
| 140 | border-radius: 8px; | ||
| 141 | margin-top: 10px; | ||
| 142 | } | ||
| 143 | |||
| 144 | .audio-player .play-btn { | ||
| 145 | width: 30px; | ||
| 146 | height: 30px; | ||
| 147 | border-radius: 50%; | ||
| 148 | background: var(--accent); | ||
| 149 | color: #fff; | ||
| 150 | border: none; | ||
| 151 | font-size: 16px; | ||
| 152 | display: inline-flex; | ||
| 153 | align-items: center; | ||
| 154 | justify-content: center; | ||
| 155 | cursor: pointer; | ||
| 156 | flex: none; | ||
| 157 | padding: 0; | ||
| 158 | } | ||
| 159 | |||
| 160 | .audio-player .play-btn:hover:not(:disabled) { | ||
| 161 | opacity: 0.85; | ||
| 162 | } | ||
| 163 | |||
| 164 | .audio-player.disabled .play-btn { | ||
| 165 | background: #c8d0d9; | ||
| 166 | cursor: not-allowed; | ||
| 167 | } | ||
| 168 | |||
| 169 | .audio-player.disabled .audio-progress-bar { | ||
| 170 | background: #eef1f4; | ||
| 171 | } | ||
| 172 | |||
| 173 | .audio-player.disabled .audio-time { | ||
| 174 | color: #aab4bf; | ||
| 175 | } | ||
| 176 | |||
| 177 | .audio-progress-wrap { | ||
| 178 | flex: 1; | ||
| 179 | display: flex; | ||
| 180 | flex-direction: column; | ||
| 181 | gap: 2px; | ||
| 182 | min-width: 0; | ||
| 183 | } | ||
| 184 | |||
| 185 | .audio-progress-bar { | ||
| 186 | width: 100%; | ||
| 187 | height: 6px; | ||
| 188 | border-radius: 3px; | ||
| 189 | background: #e4e8ed; | ||
| 190 | cursor: pointer; | ||
| 191 | position: relative; | ||
| 192 | overflow: hidden; | ||
| 193 | } | ||
| 194 | |||
| 195 | .audio-progress-bar .filled { | ||
| 196 | height: 100%; | ||
| 197 | background: var(--accent); | ||
| 198 | border-radius: 3px; | ||
| 199 | width: 0%; | ||
| 200 | transition: width 0.15s linear; | ||
| 201 | } | ||
| 202 | |||
| 203 | .audio-time { | ||
| 204 | font-size: 12px; | ||
| 205 | color: var(--muted); | ||
| 206 | white-space: nowrap; | ||
| 207 | flex: none; | ||
| 208 | min-width: 80px; | ||
| 209 | text-align: right; | ||
| 210 | } | ||
| 211 | |||
| 121 | .section { | 212 | .section { |
| 122 | margin-bottom: 14px; | 213 | margin-bottom: 14px; |
| 123 | background: var(--panel); | 214 | background: var(--panel); |
| ... | @@ -447,6 +538,7 @@ | ... | @@ -447,6 +538,7 @@ |
| 447 | main { grid-template-columns: 1fr; height: auto; } | 538 | main { grid-template-columns: 1fr; height: auto; } |
| 448 | aside { border-right: 0; border-bottom: 1px solid var(--line); max-height: 420px; } | 539 | aside { border-right: 0; border-bottom: 1px solid var(--line); max-height: 420px; } |
| 449 | .detail-grid, .lyrics-grid { grid-template-columns: 1fr; } | 540 | .detail-grid, .lyrics-grid { grid-template-columns: 1fr; } |
| 541 | .sticky-info { position: relative; padding: 10px; } | ||
| 450 | header { height: auto; min-height: 56px; flex-wrap: wrap; padding: 10px; } | 542 | header { height: auto; min-height: 56px; flex-wrap: wrap; padding: 10px; } |
| 451 | .toolbar { flex-wrap: wrap; } | 543 | .toolbar { flex-wrap: wrap; } |
| 452 | } | 544 | } |
| ... | @@ -460,13 +552,15 @@ | ... | @@ -460,13 +552,15 @@ |
| 460 | <select id="runSelect"></select> | 552 | <select id="runSelect"></select> |
| 461 | <label id="topKLabel" for="topKSelect">topK</label> | 553 | <label id="topKLabel" for="topKSelect">topK</label> |
| 462 | <select id="topKSelect"></select> | 554 | <select id="topKSelect"></select> |
| 463 | <label for="decisionFilter">决策</label> | 555 | <label for="decisionFilter">类型</label> |
| 464 | <select id="decisionFilter"> | 556 | <select id="decisionFilter"> |
| 465 | <option value="">全部</option> | 557 | <option value="">全部</option> |
| 466 | <option value="new">new</option> | 558 | <option value="l1_hit">L1 命中</option> |
| 467 | <option value="merge">merge</option> | 559 | <option value="l2_duplicate">L2 重复</option> |
| 468 | <option value="review">review</option> | 560 | <option value="l2_review">L2 审核</option> |
| 469 | <option value="skip">skip</option> | 561 | <option value="conflict">L1/L2 冲突</option> |
| 562 | <option value="new">新歌</option> | ||
| 563 | <option value="skip">已跳过</option> | ||
| 470 | </select> | 564 | </select> |
| 471 | <input id="searchInput" type="search" placeholder="搜索歌名 / ID"> | 565 | <input id="searchInput" type="search" placeholder="搜索歌名 / ID"> |
| 472 | <label for="pageSizeSelect">每页</label> | 566 | <label for="pageSizeSelect">每页</label> |
| ... | @@ -511,7 +605,8 @@ | ... | @@ -511,7 +605,8 @@ |
| 511 | </aside> | 605 | </aside> |
| 512 | 606 | ||
| 513 | <section class="content"> | 607 | <section class="content"> |
| 514 | <div id="detail"></div> | 608 | <div id="stickyInfo" class="sticky-info"></div> |
| 609 | <div id="scrollDetail" class="scroll-detail"></div> | ||
| 515 | </section> | 610 | </section> |
| 516 | </main> | 611 | </main> |
| 517 | 612 | ||
| ... | @@ -550,7 +645,8 @@ | ... | @@ -550,7 +645,8 @@ |
| 550 | metrics: document.getElementById('metrics'), | 645 | metrics: document.getElementById('metrics'), |
| 551 | sampleCount: document.getElementById('sampleCount'), | 646 | sampleCount: document.getElementById('sampleCount'), |
| 552 | queryList: document.getElementById('queryList'), | 647 | queryList: document.getElementById('queryList'), |
| 553 | detail: document.getElementById('detail') | 648 | detail: document.getElementById('scrollDetail'), |
| 649 | stickyInfo: document.getElementById('stickyInfo') | ||
| 554 | }; | 650 | }; |
| 555 | 651 | ||
| 556 | function esc(value) { | 652 | function esc(value) { |
| ... | @@ -680,6 +776,7 @@ | ... | @@ -680,6 +776,7 @@ |
| 680 | els.queryList.innerHTML = '<div class="empty">没有样本</div>'; | 776 | els.queryList.innerHTML = '<div class="empty">没有样本</div>'; |
| 681 | state.queryId = ''; | 777 | state.queryId = ''; |
| 682 | state.queryRows = []; | 778 | state.queryRows = []; |
| 779 | els.stickyInfo.innerHTML = ''; | ||
| 683 | renderDetail(); | 780 | renderDetail(); |
| 684 | return; | 781 | return; |
| 685 | } | 782 | } |
| ... | @@ -781,9 +878,74 @@ | ... | @@ -781,9 +878,74 @@ |
| 781 | deleted: '已删除', | 878 | deleted: '已删除', |
| 782 | }; | 879 | }; |
| 783 | 880 | ||
| 881 | function fmtDuration(sec) { | ||
| 882 | if (!sec || !isFinite(sec)) return '0:00'; | ||
| 883 | const m = Math.floor(sec / 60); | ||
| 884 | const s = Math.floor(sec % 60); | ||
| 885 | return `${m}:${s.toString().padStart(2, '0')}`; | ||
| 886 | } | ||
| 887 | |||
| 888 | function audioPlayerHtml(id, url) { | ||
| 889 | const disabled = !url; | ||
| 890 | return ` | ||
| 891 | <div class="audio-player${disabled ? ' disabled' : ''}" id="${id}Player"> | ||
| 892 | <button class="play-btn" id="${id}PlayBtn" ${disabled ? 'disabled' : ''}>▶</button> | ||
| 893 | <div class="audio-progress-wrap"> | ||
| 894 | <div class="audio-progress-bar" id="${id}ProgressBar"><div class="filled" id="${id}ProgressFilled"></div></div> | ||
| 895 | </div> | ||
| 896 | <span class="audio-time" id="${id}Time">${disabled ? '无音频' : '0:00 / 0:00'}</span> | ||
| 897 | ${url ? `<audio id="${id}El" preload="metadata" src="${esc(url)}"></audio>` : ''} | ||
| 898 | </div>`; | ||
| 899 | } | ||
| 900 | |||
| 901 | function bindAudioPlayer(id) { | ||
| 902 | const audioEl = document.getElementById(`${id}El`); | ||
| 903 | const playBtn = document.getElementById(`${id}PlayBtn`); | ||
| 904 | const progressBar = document.getElementById(`${id}ProgressBar`); | ||
| 905 | const progressFilled = document.getElementById(`${id}ProgressFilled`); | ||
| 906 | const audioTime = document.getElementById(`${id}Time`); | ||
| 907 | if (!audioEl || !playBtn) return; | ||
| 908 | const pauseAllAudio = () => { | ||
| 909 | for (const el of document.querySelectorAll('.audio-player audio')) { | ||
| 910 | el.pause(); | ||
| 911 | const btn = el.closest('.audio-player').querySelector('.play-btn'); | ||
| 912 | if (btn) btn.innerHTML = '▶'; | ||
| 913 | } | ||
| 914 | }; | ||
| 915 | audioEl.addEventListener('loadedmetadata', () => { | ||
| 916 | audioTime.textContent = `0:00 / ${fmtDuration(audioEl.duration)}`; | ||
| 917 | }); | ||
| 918 | playBtn.addEventListener('click', () => { | ||
| 919 | if (audioEl.paused) { | ||
| 920 | pauseAllAudio(); | ||
| 921 | audioEl.play(); | ||
| 922 | playBtn.innerHTML = '▮▮'; | ||
| 923 | } else { | ||
| 924 | audioEl.pause(); | ||
| 925 | playBtn.innerHTML = '▶'; | ||
| 926 | } | ||
| 927 | }); | ||
| 928 | audioEl.addEventListener('timeupdate', () => { | ||
| 929 | const pct = audioEl.duration ? (audioEl.currentTime / audioEl.duration) * 100 : 0; | ||
| 930 | progressFilled.style.width = `${pct}%`; | ||
| 931 | audioTime.textContent = `${fmtDuration(audioEl.currentTime)} / ${fmtDuration(audioEl.duration)}`; | ||
| 932 | }); | ||
| 933 | audioEl.addEventListener('ended', () => { | ||
| 934 | playBtn.innerHTML = '▶'; | ||
| 935 | progressFilled.style.width = '0%'; | ||
| 936 | }); | ||
| 937 | progressBar.addEventListener('click', (e) => { | ||
| 938 | if (!audioEl.duration) return; | ||
| 939 | const rect = progressBar.getBoundingClientRect(); | ||
| 940 | const ratio = (e.clientX - rect.left) / rect.width; | ||
| 941 | audioEl.currentTime = ratio * audioEl.duration; | ||
| 942 | }); | ||
| 943 | } | ||
| 944 | |||
| 784 | async function renderDetail() { | 945 | async function renderDetail() { |
| 785 | const rows = selectedQueryRows(); | 946 | const rows = selectedQueryRows(); |
| 786 | if (!rows.length) { | 947 | if (!rows.length) { |
| 948 | els.stickyInfo.innerHTML = ''; | ||
| 787 | els.detail.innerHTML = '<div class="empty">选择一个样本查看详情</div>'; | 949 | els.detail.innerHTML = '<div class="empty">选择一个样本查看详情</div>'; |
| 788 | return; | 950 | return; |
| 789 | } | 951 | } |
| ... | @@ -800,8 +962,11 @@ | ... | @@ -800,8 +962,11 @@ |
| 800 | const selectedReview = candidate ? getReview(candidate) : {}; | 962 | const selectedReview = candidate ? getReview(candidate) : {}; |
| 801 | const conflict = candidate ? isConflict(candidate) : false; | 963 | const conflict = candidate ? isConflict(candidate) : false; |
| 802 | const l1 = candidate ? l1Match(candidate) : false; | 964 | const l1 = candidate ? l1Match(candidate) : false; |
| 965 | const queryAudioUrl = query.audio_url || ''; | ||
| 966 | const candidateAudioUrl = candidate?.candidate_audio_url || ''; | ||
| 803 | 967 | ||
| 804 | els.detail.innerHTML = ` | 968 | // Sticky area: 新入库歌词 + 召回候选(各含音频播放器) |
| 969 | els.stickyInfo.innerHTML = ` | ||
| 805 | <div class="detail-grid"> | 970 | <div class="detail-grid"> |
| 806 | <div class="section"> | 971 | <div class="section"> |
| 807 | <div class="section-head"> | 972 | <div class="section-head"> |
| ... | @@ -815,6 +980,7 @@ | ... | @@ -815,6 +980,7 @@ |
| 815 | <div>作曲</div><div>${esc(query.query_composer || '-')}</div> | 980 | <div>作曲</div><div>${esc(query.query_composer || '-')}</div> |
| 816 | <div>歌词</div><div class="path">${fmtPath(query.query_lyrics_path)}</div> | 981 | <div>歌词</div><div class="path">${fmtPath(query.query_lyrics_path)}</div> |
| 817 | </div> | 982 | </div> |
| 983 | ${audioPlayerHtml('queryAudio', queryAudioUrl)} | ||
| 818 | </div> | 984 | </div> |
| 819 | </div> | 985 | </div> |
| 820 | <div class="section"> | 986 | <div class="section"> |
| ... | @@ -831,10 +997,13 @@ | ... | @@ -831,10 +997,13 @@ |
| 831 | <div>L1</div><div>${l1 ? '<span class="pill l1">元数据命中</span>' : '<span class="pill">未命中</span>'} ${conflict ? '<span class="pill conflict">L1/L2 冲突</span>' : ''}</div> | 997 | <div>L1</div><div>${l1 ? '<span class="pill l1">元数据命中</span>' : '<span class="pill">未命中</span>'} ${conflict ? '<span class="pill conflict">L1/L2 冲突</span>' : ''}</div> |
| 832 | <div>歌词</div><div class="path">${fmtPath(candidatePath)}</div> | 998 | <div>歌词</div><div class="path">${fmtPath(candidatePath)}</div> |
| 833 | </div> | 999 | </div> |
| 1000 | ${audioPlayerHtml('candAudio', candidateAudioUrl)} | ||
| 834 | </div> | 1001 | </div> |
| 835 | </div> | 1002 | </div> |
| 836 | </div> | 1003 | </div>`; |
| 837 | 1004 | ||
| 1005 | // Scrollable area: 人工标注 + Top5候选 + 歌词对比 | ||
| 1006 | els.detail.innerHTML = ` | ||
| 838 | <div class="section"> | 1007 | <div class="section"> |
| 839 | <div class="section-head"> | 1008 | <div class="section-head"> |
| 840 | <h2 class="section-title">人工标注</h2> | 1009 | <h2 class="section-title">人工标注</h2> |
| ... | @@ -881,6 +1050,9 @@ | ... | @@ -881,6 +1050,9 @@ |
| 881 | </div> | 1050 | </div> |
| 882 | </div>`; | 1051 | </div>`; |
| 883 | 1052 | ||
| 1053 | bindAudioPlayer('queryAudio'); | ||
| 1054 | bindAudioPlayer('candAudio'); | ||
| 1055 | |||
| 884 | for (const card of els.detail.querySelectorAll('.candidate')) { | 1056 | for (const card of els.detail.querySelectorAll('.candidate')) { |
| 885 | card.addEventListener('click', () => { | 1057 | card.addEventListener('click', () => { |
| 886 | state.candidateId = card.dataset.candidateId; | 1058 | state.candidateId = card.dataset.candidateId; | ... | ... |
| ... | @@ -163,6 +163,7 @@ def _staging_dashboard_row(row: dict[str, object]) -> dict[str, str]: | ... | @@ -163,6 +163,7 @@ def _staging_dashboard_row(row: dict[str, object]) -> dict[str, str]: |
| 163 | "reviewed_at": str(row.get("reviewed_at") or ""), | 163 | "reviewed_at": str(row.get("reviewed_at") or ""), |
| 164 | "l1_metadata_match": "1" if row.get("l1_matched_id") else "0", | 164 | "l1_metadata_match": "1" if row.get("l1_matched_id") else "0", |
| 165 | "l1_l2_conflict": "0", | 165 | "l1_l2_conflict": "0", |
| 166 | "audio_url": str(row.get("audio_url") or ""), | ||
| 166 | } | 167 | } |
| 167 | 168 | ||
| 168 | 169 | ||
| ... | @@ -176,8 +177,21 @@ def _staging_groups_response(mode: str, term: str, page: int, page_size: int, de | ... | @@ -176,8 +177,21 @@ def _staging_groups_response(mode: str, term: str, page: int, page_size: int, de |
| 176 | # hits 模式下只展示有命中的记录(merge/review) | 177 | # hits 模式下只展示有命中的记录(merge/review) |
| 177 | if mode == "hits": | 178 | if mode == "hits": |
| 178 | where_clauses.append("dedup_action IN ('merge', 'review')") | 179 | where_clauses.append("dedup_action IN ('merge', 'review')") |
| 179 | # 决策筛选:new / merge / review / skip | 180 | # 决策筛选:l1_hit / l2_hit / conflict / new / skip |
| 180 | if decision and decision in ('new', 'merge', 'review', 'skip'): | 181 | if decision == 'l1_hit': |
| 182 | where_clauses.append("l1_matched_id IS NOT NULL") | ||
| 183 | elif decision == 'l2_duplicate': | ||
| 184 | where_clauses.append("dedup_action = 'merge'") | ||
| 185 | elif decision == 'l2_review': | ||
| 186 | where_clauses.append("dedup_action = 'review'") | ||
| 187 | elif decision == 'conflict': | ||
| 188 | where_clauses.append("(l1_matched_id IS NOT NULL AND dedup_action = 'new') OR (l1_matched_id IS NULL AND dedup_action IN ('merge', 'review'))") | ||
| 189 | elif decision == 'new': | ||
| 190 | where_clauses.append("dedup_action = 'new' AND l1_matched_id IS NULL") | ||
| 191 | elif decision == 'skip': | ||
| 192 | where_clauses.append("dedup_action = 'skip'") | ||
| 193 | # 兼容旧的 dedup_action 筛选 | ||
| 194 | elif decision and decision in ('merge', 'review'): | ||
| 181 | where_clauses.append("dedup_action = %s") | 195 | where_clauses.append("dedup_action = %s") |
| 182 | params.append(decision) | 196 | params.append(decision) |
| 183 | where = "WHERE " + " AND ".join(where_clauses) if where_clauses else "" | 197 | where = "WHERE " + " AND ".join(where_clauses) if where_clauses else "" |
| ... | @@ -242,14 +256,14 @@ def _staging_query_rows(query_id: str) -> dict[str, object]: | ... | @@ -242,14 +256,14 @@ def _staging_query_rows(query_id: str) -> dict[str, object]: |
| 242 | if matched_song_id: | 256 | if matched_song_id: |
| 243 | # 先尝试目标表查找(matched_id 是目标库 ID 的情况) | 257 | # 先尝试目标表查找(matched_id 是目标库 ID 的情况) |
| 244 | cursor.execute( | 258 | cursor.execute( |
| 245 | f"SELECT name, lyricist, composer, lyrics_url FROM {TARGET_TABLE_NAME} WHERE id = %s", | 259 | f"SELECT name, lyricist, composer, lyrics_url, audio_url FROM {TARGET_TABLE_NAME} WHERE id = %s", |
| 246 | (matched_song_id,), | 260 | (matched_song_id,), |
| 247 | ) | 261 | ) |
| 248 | matched_row = cursor.fetchone() | 262 | matched_row = cursor.fetchone() |
| 249 | # 目标表找不到时,回退到暂存表查找(同批次内匹配,matched_id 是源库 source_song_id) | 263 | # 目标表找不到时,回退到暂存表查找(同批次内匹配,matched_id 是源库 source_song_id) |
| 250 | if not matched_row: | 264 | if not matched_row: |
| 251 | cursor.execute( | 265 | cursor.execute( |
| 252 | f"SELECT name, lyricist, composer, lyrics_url FROM {TARGET_TABLE_NAME_TMP}" | 266 | f"SELECT name, lyricist, composer, lyrics_url, audio_url FROM {TARGET_TABLE_NAME_TMP}" |
| 253 | f" WHERE source_song_id = %s ORDER BY staging_id DESC LIMIT 1", | 267 | f" WHERE source_song_id = %s ORDER BY staging_id DESC LIMIT 1", |
| 254 | (str(matched_song_id),), | 268 | (str(matched_song_id),), |
| 255 | ) | 269 | ) |
| ... | @@ -266,6 +280,7 @@ def _staging_query_rows(query_id: str) -> dict[str, object]: | ... | @@ -266,6 +280,7 @@ def _staging_query_rows(query_id: str) -> dict[str, object]: |
| 266 | if cand_lyrics and not cand_lyrics.startswith(("http://", "https://", "/")): | 280 | if cand_lyrics and not cand_lyrics.startswith(("http://", "https://", "/")): |
| 267 | cand_lyrics = f"_raw:{cand_lyrics}" | 281 | cand_lyrics = f"_raw:{cand_lyrics}" |
| 268 | dashboard_row["candidate_lyrics_path"] = cand_lyrics | 282 | dashboard_row["candidate_lyrics_path"] = cand_lyrics |
| 283 | dashboard_row["candidate_audio_url"] = str(matched_row.get("audio_url") or "") | ||
| 269 | 284 | ||
| 270 | rows = [dashboard_row] | 285 | rows = [dashboard_row] |
| 271 | 286 | ||
| ... | @@ -295,7 +310,7 @@ def _staging_query_rows(query_id: str) -> dict[str, object]: | ... | @@ -295,7 +310,7 @@ def _staging_query_rows(query_id: str) -> dict[str, object]: |
| 295 | with _target_conn() as tconn, tconn.cursor() as tcursor: | 310 | with _target_conn() as tconn, tconn.cursor() as tcursor: |
| 296 | # 目标表查找 | 311 | # 目标表查找 |
| 297 | tcursor.execute( | 312 | tcursor.execute( |
| 298 | f"SELECT id, name, lyricist, composer, lyrics_url" | 313 | f"SELECT id, name, lyricist, composer, lyrics_url, audio_url" |
| 299 | f" FROM {TARGET_TABLE_NAME} WHERE id IN ({placeholders})", | 314 | f" FROM {TARGET_TABLE_NAME} WHERE id IN ({placeholders})", |
| 300 | recalled_ids_int, | 315 | recalled_ids_int, |
| 301 | ) | 316 | ) |
| ... | @@ -306,7 +321,7 @@ def _staging_query_rows(query_id: str) -> dict[str, object]: | ... | @@ -306,7 +321,7 @@ def _staging_query_rows(query_id: str) -> dict[str, object]: |
| 306 | if missing_ids: | 321 | if missing_ids: |
| 307 | ph2 = ",".join(["%s"] * len(missing_ids)) | 322 | ph2 = ",".join(["%s"] * len(missing_ids)) |
| 308 | tcursor.execute( | 323 | tcursor.execute( |
| 309 | f"SELECT source_song_id, name, lyricist, composer, lyrics_url" | 324 | f"SELECT source_song_id, name, lyricist, composer, lyrics_url, audio_url" |
| 310 | f" FROM {TARGET_TABLE_NAME_TMP}" | 325 | f" FROM {TARGET_TABLE_NAME_TMP}" |
| 311 | f" WHERE source_song_id IN ({ph2})" | 326 | f" WHERE source_song_id IN ({ph2})" |
| 312 | f" ORDER BY staging_id DESC", | 327 | f" ORDER BY staging_id DESC", |
| ... | @@ -336,11 +351,13 @@ def _staging_query_rows(query_id: str) -> dict[str, object]: | ... | @@ -336,11 +351,13 @@ def _staging_query_rows(query_id: str) -> dict[str, object]: |
| 336 | if cand_lyrics and not cand_lyrics.startswith(("http://", "https://", "/")): | 351 | if cand_lyrics and not cand_lyrics.startswith(("http://", "https://", "/")): |
| 337 | cand_lyrics = f"_raw:{cand_lyrics}" | 352 | cand_lyrics = f"_raw:{cand_lyrics}" |
| 338 | cand_row["candidate_lyrics_path"] = cand_lyrics | 353 | cand_row["candidate_lyrics_path"] = cand_lyrics |
| 354 | cand_row["candidate_audio_url"] = str(target_info.get("audio_url") or "") | ||
| 339 | else: | 355 | else: |
| 340 | cand_row["candidate_name"] = str(cand.get("name") or "") | 356 | cand_row["candidate_name"] = str(cand.get("name") or "") |
| 341 | cand_row["candidate_lyricist"] = str(cand.get("lyricist") or "") | 357 | cand_row["candidate_lyricist"] = str(cand.get("lyricist") or "") |
| 342 | cand_row["candidate_composer"] = str(cand.get("composer") or "") | 358 | cand_row["candidate_composer"] = str(cand.get("composer") or "") |
| 343 | cand_row["candidate_lyrics_path"] = "" | 359 | cand_row["candidate_lyrics_path"] = "" |
| 360 | cand_row["candidate_audio_url"] = "" | ||
| 344 | cand_row["candidate_decision"] = str(cand.get("decision") or "new") | 361 | cand_row["candidate_decision"] = str(cand.get("decision") or "new") |
| 345 | cand_row["candidate_confidence"] = str(cand.get("confidence") or "") | 362 | cand_row["candidate_confidence"] = str(cand.get("confidence") or "") |
| 346 | cand_row["candidate_jaccard"] = str(cand.get("jaccard") or "") | 363 | cand_row["candidate_jaccard"] = str(cand.get("jaccard") or "") |
| ... | @@ -642,8 +659,20 @@ def _groups_response(path: Path, top_k: str, mode: str, term: str, page: int, pa | ... | @@ -642,8 +659,20 @@ def _groups_response(path: Path, top_k: str, mode: str, term: str, page: int, pa |
| 642 | if mode == "hits": | 659 | if mode == "hits": |
| 643 | groups = [g for g in groups if g.get("has_hit")] | 660 | groups = [g for g in groups if g.get("has_hit")] |
| 644 | # 决策筛选 | 661 | # 决策筛选 |
| 645 | if decision and decision in ('new', 'merge', 'review', 'duplicate', 'skip'): | 662 | if decision == 'l1_hit': |
| 646 | groups = [g for g in groups if g.get("decision") == decision or (decision == 'merge' and g.get("decision") == 'merge')] | 663 | groups = [g for g in groups if g.get("has_l1_hint")] |
| 664 | elif decision == 'l2_duplicate': | ||
| 665 | groups = [g for g in groups if g.get("decision") == "merge"] | ||
| 666 | elif decision == 'l2_review': | ||
| 667 | groups = [g for g in groups if g.get("decision") == "review"] | ||
| 668 | elif decision == 'conflict': | ||
| 669 | groups = [g for g in groups if g.get("has_conflict")] | ||
| 670 | elif decision == 'new': | ||
| 671 | groups = [g for g in groups if g.get("decision") == "new" and not g.get("has_l1_hint")] | ||
| 672 | elif decision == 'skip': | ||
| 673 | groups = [g for g in groups if g.get("decision") == "skip"] | ||
| 674 | elif decision and decision in ('merge', 'review', 'duplicate'): | ||
| 675 | groups = [g for g in groups if g.get("decision") == decision] | ||
| 647 | filtered = [group for group in groups if _matches_term(group, term)] | 676 | filtered = [group for group in groups if _matches_term(group, term)] |
| 648 | start = max(0, (page - 1) * page_size) | 677 | start = max(0, (page - 1) * page_size) |
| 649 | end = start + page_size | 678 | end = start + page_size | ... | ... |
-
Please register or sign in to post a comment