# 已识别说话人：出现统计接口需求

## 先说需求与缺口

录音页的「已识别说话人」同时展示已标记的人和未标记的声音候选。每张列表卡片需要展示**出现天数**和**最近一次发声时间**，且口径为当前账号可见录音的完整历史，而不是创建标记的时间、录音结束时间或前端最近加载的几天。

一个已标记的人可能包含一个或多个原始识别轨道。若只有一个，使用该轨道的出现事实；若有多个，按这些轨道实际归属的同一人聚合：出现天数是日期集合的并集，最近时间是最后一个有效发声片段的结束时间。不能简单相加各轨道的天数，也不能仅凭相同昵称合并不同人。

**目前只缺已标记说话人的全历史出现统计读取能力。**现有 `/api/v1/audio/get-speaker-ls` 只给 `id/ref_usr_id/nick_name`，不含出现统计；`/api/v1/audio/unmarked-speakers/list` 已给未标记候选的 `day_count` 和最近时间，但会排除已被正式标记的片段，不能拿它的结果为已标记者相加。

## 现有接口能先完成什么

| 页面数据 | 已有接口 | 可呈现程度 |
| --- | --- | --- |
| 已标记身份、名称、绑定用户 | `POST /api/v1/audio/get-speaker-ls` | 全量身份列表；无历史出现统计 |
| 未标记候选、出现天数和最近时间 | `POST /api/v1/audio/unmarked-speakers/list` | 该候选投影已覆盖的完整历史；需保留 `building/stale/failed` 提示 |
| 已标记者最近几天的临时统计 | `POST /api/v1/audio/get-calender-summary` + 逐日 `POST /api/v1/audio/one-day-trans` | 只能限定时间窗现算；必须明确显示「近 7 天」，不得称作全历史 |
| 声纹授权目录 | `POST /api/v1/audio/voiceprint/recognized-speakers/list` | 只有目录/授权信息，不提供出现天数或最后发声时间 |
| 手工说话人相关录音 | `POST /api/v1/audio/related-recordings/by-speaker` | 仅适用于未绑定用户的手工 Speaker，且按录音 Session 返回，不等于实际发声统计；不适合列表逐人调用 |

临时 Web 版本已实现：最多扫描最近 7 个自然日中有录音的日期，并把结果明确标为「近 7 天」。它不会把缺席解释为历史上从未出现，也不会持久保存成全历史数字。此方案只是等待新接口时的可见效果，不是后端接口的替代品。

## 请后端提供一个账号级批量接口

建议沿用已在本地草拟的契约：

`POST /api/v1/audio/speaker-presence/list`，请求体 `{}`。从登录态取得录音 owner，不接受客户端传入任意用户 ID。一次返回当前账号全部有效已标记 Speaker 的统计，以 `speaker_id` 与 `get-speaker-ls` 对接，避免每张卡片发一条请求。

```json
{
  "state": "fresh",
  "updated_at": 1790470000000,
  "items": [
    { "speaker_id": "有效的Speaker ID", "day_count": 12, "last_seen_at": 1790469000000 }
  ]
}
```

`state` 建议支持 `building/fresh/stale/failed`；构建中可返回 `retry_after_ms`。`day_count` 是非负整数，`last_seen_at` 是毫秒时间戳。完全没有有效发声片段的已标记身份可以不出现在 `items` 中；前端据此显示「暂无可统计的录音片段」，但**只有 `fresh` 状态才可作此结论**。构建中、失败或老版本投影不可用时，不得伪造零天。

### 统计与更新口径

1. 以当前 owner **可见且未删除**的录音和有效 ASR 发声片段为事实；静音、仅创建 Speaker、仅改昵称、尚未完成转写的录音不算出现。
2. 每个片段先按现有统一身份优先级解析最终生效的人工 Speaker（包括 Session 编号批量标记、单条覆盖及取消标记），再聚合到已标记身份。不要把同一片段同时计入已标记和未标记。
3. 一人对应多条轨道时，先合并片段，再按**录音本地时区的自然日**去重。`last_seen_at` 取有效片段的绝对结束时间最大值。多个 Speaker ID 若绑定同一 `ref_usr_id`，它们在页面上是同一人，应得到一致的合并统计；没有明确绑定关系的同名 Speaker 不自动合并。
4. 标记、批量标记、取消标记、录音上传与转写完成、录音删除或恢复、Speaker 绑定变化后，应使投影变为 `stale` 并异步重建。读取接口不应同步扫描用户全部历史。旧快照若继续返回，须显式标为 `stale`。
5. 结果只暴露 ID、天数、时间及构建状态，不返回 ASR 原文、声纹、其他人的录音或越权身份。大账号需要分页扫描源事实、一次账号级聚合，不能用逐 Speaker 的 N+1 查询。

### 验收用例

- 同一个已标记人由两个原始轨道组成，同一天各有发声：出现 **1 天**，不是 2 天。
- 两个轨道分布在不同日：出现 **2 天**；最新时间来自最后一个有效片段，而不是录音文件末尾。
- 同一个 `ref_usr_id` 下有两个 Speaker ID：列表只呈现同一人，统计一致；同昵称但不同身份不合并。
- 取消单条覆盖、批量取消标记后，对应片段从已标记统计移走；标记后未标记候选同步移除该片段。
- 只有静音或尚未转写时不显示虚假的出现时间；删除/恢复与跨时区日期口径正确。
- `building/stale/failed`、跨账号访问、超大历史分页重建均有测试。

## 补充：已标记者详情中的原始识别身份

用户点击已标记的人，还需要看见这个人当前对应哪些系统识别出的「说话人 A/B/C」或数字编号。`get-speaker-ls` 仅有正式 Speaker 身份；`unmarked-speakers/list` 会排除已标记片段，都不能作为完整映射。现有逐日 `one-day-trans` 的 `spk_ls` 包含 `inner_spk_id/day_spk_id/speaker_display_number` 和正式 `spk_id`，因此 Web 已能展示**近 7 天可核实的关联**，但不能据此断言完整历史。

请后端在账号级投影中增加按已标记人读取的详情能力，例如 `POST /api/v1/audio/speaker-presence/detail`，请求 `{ "speaker_id": "当前账号的正式 Speaker ID" }`。建议返回同一稳定自然人（包括同一 `ref_usr_id` 下的多个正式 Speaker）关联的原始识别身份列表：每项包含不泄露其他账号数据的稳定身份引用、展示编号或字母、该身份的出现天数与最近有效发声时间，以及 `state/updated_at`。原始身份按持久编号或 InnerSpeaker/DaySpeaker 的稳定 ID 去重；相同显示字母但不同身份不能合并，只有人工单条指定而无可核实原始身份的片段不应伪造一个。标记、取消、身份聚合变化和录音删除/恢复后应使此投影失效重建。前端只在 `fresh` 全历史结果下把列表称为完整关联；其他状态继续标注范围或等待。

## 当前开发状态，供接手人参考

本地 `jotmo-audio` 分支 `codex/ts0927-01-说话人出现统计` 已有一个**未合并、未部署**的实现草稿：`gin/api/audio_speaker_presence.go`、`internal/audio/speaker_presence_stats.go`，并复用现有未标记候选的分页投影源事实。桌面插件已接入上述接口；服务端未部署前，3100 预览对已标记者显示现有接口算出的「近 7 天」统计，对未标记者继续显示现有候选投影的统计。请以后端实际合入、部署和验收结果为准，不要把本地草稿视为线上能力。
