Compare commits
2 Commits
ca8e4cdb36
...
72e6b9efdd
| Author | SHA1 | Date | |
|---|---|---|---|
| 72e6b9efdd | |||
| 44110a0ccf |
@@ -47,7 +47,7 @@
|
|||||||
- 支持指定语言(`--language zh/en/...`)或自动检测
|
- 支持指定语言(`--language zh/en/...`)或自动检测
|
||||||
- 支持音频**切分转录**:超长音频切分后逐段转录再合并
|
- 支持音频**切分转录**:超长音频切分后逐段转录再合并
|
||||||
- 支持多种音频格式:`.mp3/.m4a/.wav/.flac/.ogg/.aac`
|
- 支持多种音频格式:`.mp3/.m4a/.wav/.flac/.ogg/.aac`
|
||||||
- **Manifest key 含模型维度**:多模型多参数结果共存,互不覆盖
|
- **Manifest 嵌套 transcriptions**:同一视频/音频记录里,多模型多参数结果按 `<model>[:<suffix>]` 为 subkey 共存,互不覆盖
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -344,37 +344,33 @@ python3 transcribe_audio.py /home/user/audio/ --retry-failed
|
|||||||
|
|
||||||
## Manifest 文件说明
|
## Manifest 文件说明
|
||||||
|
|
||||||
两个脚本**共享同一个 `manifest.json`**,通过 key 前缀区分:
|
两个脚本**共享同一个 `manifest.json`**(schema v2)。每个视频文件在 manifest 中**只有一条记录**,`extract_audio.py` 与 `transcribe_audio.py` 分别写入其中的 `extraction` 与 `transcriptions` 两个子对象,互不覆盖。
|
||||||
|
|
||||||
- **视频提取记录**:key = 视频绝对路径
|
### key 选择规则
|
||||||
- **音频转录记录**:key = `transcribe:<model>[:<suffix>]:<音频绝对路径>`
|
|
||||||
|
|
||||||
### key 结构详解
|
| Key | 来源 | 场景 |
|
||||||
|
|-----|------|------|
|
||||||
|
| 视频绝对路径(如 `/path/to/x.mp4`)| `extract_audio.py` 产生的记录 | 完整流水线(视频 → 音频 → 转录)|
|
||||||
|
| 音频绝对路径(如 `/path/to/x.mp3`)| 直接对 mp3 跑 `transcribe_audio.py` | 独立音频转录(无对应视频)|
|
||||||
|
|
||||||
| Key 格式 | 场景 |
|
**反向查找**:当 `transcribe_audio.py` 处理某个 mp3 时,会先扫描 manifest,如果发现某条视频记录的 `extraction.audio_path` 等于该 mp3 路径,就把转录结果并入该视频记录,而不是新建 key。
|
||||||
|---------|------|
|
|
||||||
| `/path/to/x.mp4` | 视频提取记录 |
|
|
||||||
| `transcribe:tiny:/path/to/x.mp3` | tiny 模型转录(无 suffix) |
|
|
||||||
| `transcribe:medium:/path/to/x.mp3` | medium 模型转录(无 suffix) |
|
|
||||||
| `transcribe:base:_v1:/path/to/x.mp3` | base 模型 + `_v1` 后缀 |
|
|
||||||
| `transcribe:base:_v2:/path/to/x.mp3` | base 模型 + `_v2` 后缀 |
|
|
||||||
|
|
||||||
多个 key 可并存于同一 manifest,互不覆盖。
|
### 完整示例(v2)
|
||||||
|
|
||||||
### manifest.json 完整示例(含多模型对比)
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"version": 1,
|
"version": 2,
|
||||||
"files": {
|
"files": {
|
||||||
"/path/to/lesson1.mp4": {
|
"/path/to/lesson1.mp4": {
|
||||||
"audio_extracted": true,
|
"extraction": {
|
||||||
|
"extracted": true,
|
||||||
"audio_path": "/path/to/lesson1.mp3",
|
"audio_path": "/path/to/lesson1.mp3",
|
||||||
"split_segments": [],
|
"split_segments": [],
|
||||||
"error": null,
|
"error": null,
|
||||||
"updated_at": "2026-08-23T10:30:00"
|
"updated_at": "2026-08-23T10:30:00"
|
||||||
},
|
},
|
||||||
"transcribe:tiny:/path/to/lesson1.mp3": {
|
"transcriptions": {
|
||||||
|
"tiny": {
|
||||||
"transcribed": true,
|
"transcribed": true,
|
||||||
"txt_path": "/path/to/lesson1.tiny.txt",
|
"txt_path": "/path/to/lesson1.tiny.txt",
|
||||||
"split_segments": [],
|
"split_segments": [],
|
||||||
@@ -384,7 +380,7 @@ python3 transcribe_audio.py /home/user/audio/ --retry-failed
|
|||||||
"error": null,
|
"error": null,
|
||||||
"updated_at": "2026-08-23T10:35:00"
|
"updated_at": "2026-08-23T10:35:00"
|
||||||
},
|
},
|
||||||
"transcribe:medium:/path/to/lesson1.mp3": {
|
"medium": {
|
||||||
"transcribed": true,
|
"transcribed": true,
|
||||||
"txt_path": "/path/to/lesson1.medium.txt",
|
"txt_path": "/path/to/lesson1.medium.txt",
|
||||||
"split_segments": [],
|
"split_segments": [],
|
||||||
@@ -393,6 +389,33 @@ python3 transcribe_audio.py /home/user/audio/ --retry-failed
|
|||||||
"output_suffix": null,
|
"output_suffix": null,
|
||||||
"error": null,
|
"error": null,
|
||||||
"updated_at": "2026-08-23T10:50:00"
|
"updated_at": "2026-08-23T10:50:00"
|
||||||
|
},
|
||||||
|
"base:_v1": {
|
||||||
|
"transcribed": true,
|
||||||
|
"txt_path": "/path/to/lesson1_v1.txt",
|
||||||
|
"split_segments": [],
|
||||||
|
"model": "base",
|
||||||
|
"language": null,
|
||||||
|
"output_suffix": "_v1",
|
||||||
|
"error": null,
|
||||||
|
"updated_at": "2026-08-23T11:00:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/path/to/standalone.mp3": {
|
||||||
|
"extraction": null,
|
||||||
|
"transcriptions": {
|
||||||
|
"medium": {
|
||||||
|
"transcribed": true,
|
||||||
|
"txt_path": "/path/to/standalone.medium.txt",
|
||||||
|
"split_segments": [],
|
||||||
|
"model": "medium",
|
||||||
|
"language": "en",
|
||||||
|
"output_suffix": null,
|
||||||
|
"error": null,
|
||||||
|
"updated_at": "2026-08-23T11:15:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -400,14 +423,18 @@ python3 transcribe_audio.py /home/user/audio/ --retry-failed
|
|||||||
|
|
||||||
### 字段说明
|
### 字段说明
|
||||||
|
|
||||||
**视频提取记录字段**
|
**顶层每条记录**
|
||||||
- `audio_extracted` - 是否已提取音频(布尔)
|
- `extraction` - `extract_audio.py` 的产物;`null` 表示没有走过提取(例如直接对 mp3 跑转录)
|
||||||
|
- `transcriptions` - `transcribe_audio.py` 的产物;以 `<model>` 或 `<model>:<suffix>` 为 subkey,允许同一音频用多种模型/参数并存
|
||||||
|
|
||||||
|
**`extraction` 字段**
|
||||||
|
- `extracted` - 是否已提取音频(布尔);`false` + 有 `error` 表示上次失败
|
||||||
- `audio_path` - 生成的 MP3 路径(切分模式下为第一个片段)
|
- `audio_path` - 生成的 MP3 路径(切分模式下为第一个片段)
|
||||||
- `split_segments` - 切分片段列表(非切分模式为空)
|
- `split_segments` - 切分片段列表(非切分模式为空 `[]`)
|
||||||
- `error` - 失败时的错误信息,成功为 `null`
|
- `error` - 失败时的错误信息,成功为 `null`
|
||||||
- `updated_at` - 最后更新时间(ISO 8601)
|
- `updated_at` - 最后更新时间(ISO 8601)
|
||||||
|
|
||||||
**音频转录记录字段**
|
**`transcriptions.<subkey>` 字段**
|
||||||
- `transcribed` - 是否已转录(布尔)
|
- `transcribed` - 是否已转录(布尔)
|
||||||
- `txt_path` - 生成的 TXT 路径
|
- `txt_path` - 生成的 TXT 路径
|
||||||
- `split_segments` - 切分片段列表
|
- `split_segments` - 切分片段列表
|
||||||
@@ -420,12 +447,18 @@ python3 transcribe_audio.py /home/user/audio/ --retry-failed
|
|||||||
### 断点续传逻辑
|
### 断点续传逻辑
|
||||||
|
|
||||||
脚本判断是否需要处理某文件时,**同时校验**:
|
脚本判断是否需要处理某文件时,**同时校验**:
|
||||||
1. manifest 中标记为已完成
|
1. manifest 中标记为已完成(`extraction.extracted=true` 或 `transcriptions.<sk>.transcribed=true`)
|
||||||
2. 目标文件实际存在且非空
|
2. 目标文件实际存在且非空
|
||||||
|
|
||||||
**任一条件不满足则重新处理**(例如手动删除了 mp3 后重跑,会重新提取)。
|
**任一条件不满足则重新处理**(例如手动删除了 mp3 后重跑,会重新提取)。
|
||||||
|
|
||||||
**多模型模式**:每个 (音频 × 模型 × 后缀) 组合独立跟踪。已完成的组合被跳过,未完成的继续。
|
**多模型模式**:每个 (音频 × 模型 × 后缀) 组合独立跟踪,对应 `transcriptions` 里独立的 subkey。已完成的组合被跳过,未完成的继续。
|
||||||
|
|
||||||
|
### 从旧版 (v1) 升级
|
||||||
|
|
||||||
|
老版本使用扁平 schema:视频记录 key = 视频路径;转录记录 key = `transcribe:<model>[:<suffix>]:<音频路径>`。这些散落的记录在 v2 里会被合并到同一条视频记录里。
|
||||||
|
|
||||||
|
**升级方式**:脚本首次加载旧 manifest 时会打印警告并**以空 v2 schema 重新开始**(旧数据将被下次保存覆盖)。如果你想保留旧记录,运行升级前**先手动备份 manifest.json**。所有已提取的 mp3 和已转录的 txt 会在下次运行时被识别为"存在但 manifest 无记录"→ 重新处理并写入新 schema。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -56,13 +56,32 @@ def log(msg: str, level: str = "INFO"):
|
|||||||
print(f"[{ts}] {prefix} {msg}", flush=True)
|
print(f"[{ts}] {prefix} {msg}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
# ─── Manifest 操作 ─────────────────────────────────────────────────────────────
|
# ─── Manifest 操作 (v2 schema) ────────────────────────────────────────────────
|
||||||
|
#
|
||||||
|
# v2 schema:每个条目形如
|
||||||
|
# {
|
||||||
|
# "extraction": { "extracted": bool, "audio_path": str, "split_segments": [],
|
||||||
|
# "error": str|None, "updated_at": str },
|
||||||
|
# "transcriptions": {
|
||||||
|
# "<model>[:<suffix>]": { "transcribed": bool, "txt_path": str, ... }
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
# transcribe_audio.py 会把转录结果并入同一条视频记录的 transcriptions 里。
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
MANIFEST_VERSION = 2
|
||||||
|
|
||||||
|
|
||||||
def load_manifest(manifest_path: Path) -> dict:
|
def load_manifest(manifest_path: Path) -> dict:
|
||||||
if manifest_path.exists():
|
if not manifest_path.exists():
|
||||||
|
return {"version": MANIFEST_VERSION, "files": {}}
|
||||||
with open(manifest_path, encoding="utf-8") as f:
|
with open(manifest_path, encoding="utf-8") as f:
|
||||||
return json.load(f)
|
data = json.load(f)
|
||||||
return {"version": 1, "files": {}}
|
# 旧版 manifest (v1) 直接废弃,从头开始(用户已明确接受)
|
||||||
|
if data.get("version", 1) < MANIFEST_VERSION:
|
||||||
|
log("检测到旧版 manifest.json (v1),将以 v2 schema 重新初始化 - 旧记录将被覆盖", "WARN")
|
||||||
|
return {"version": MANIFEST_VERSION, "files": {}}
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
def save_manifest(manifest_path: Path, manifest: dict):
|
def save_manifest(manifest_path: Path, manifest: dict):
|
||||||
@@ -72,34 +91,41 @@ def save_manifest(manifest_path: Path, manifest: dict):
|
|||||||
|
|
||||||
|
|
||||||
def get_file_entry(manifest: dict, video_path: str) -> dict:
|
def get_file_entry(manifest: dict, video_path: str) -> dict:
|
||||||
"""获取或初始化某个视频文件的 manifest 条目"""
|
"""获取或初始化某个视频文件的 manifest 条目 (v2 schema)"""
|
||||||
if video_path not in manifest["files"]:
|
if video_path not in manifest["files"]:
|
||||||
manifest["files"][video_path] = {
|
manifest["files"][video_path] = {
|
||||||
"audio_extracted": False,
|
"extraction": None,
|
||||||
"audio_path": None,
|
"transcriptions": {},
|
||||||
"split_segments": [], # 切分后的片段列表(若有)
|
|
||||||
"error": None,
|
|
||||||
"updated_at": None,
|
|
||||||
}
|
}
|
||||||
return manifest["files"][video_path]
|
entry = manifest["files"][video_path]
|
||||||
|
if "transcriptions" not in entry:
|
||||||
|
entry["transcriptions"] = {}
|
||||||
|
return entry
|
||||||
|
|
||||||
|
|
||||||
def mark_done(manifest: dict, manifest_path: Path, video_path: str,
|
def mark_done(manifest: dict, manifest_path: Path, video_path: str,
|
||||||
audio_path: str, segments: list = None):
|
audio_path: str, segments: list = None):
|
||||||
entry = get_file_entry(manifest, video_path)
|
entry = get_file_entry(manifest, video_path)
|
||||||
entry["audio_extracted"] = True
|
entry["extraction"] = {
|
||||||
entry["audio_path"] = audio_path
|
"extracted": True,
|
||||||
entry["split_segments"] = segments or []
|
"audio_path": audio_path,
|
||||||
entry["error"] = None
|
"split_segments": segments or [],
|
||||||
entry["updated_at"] = time.strftime("%Y-%m-%dT%H:%M:%S")
|
"error": None,
|
||||||
|
"updated_at": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||||
|
}
|
||||||
save_manifest(manifest_path, manifest)
|
save_manifest(manifest_path, manifest)
|
||||||
|
|
||||||
|
|
||||||
def mark_failed(manifest: dict, manifest_path: Path, video_path: str, error: str):
|
def mark_failed(manifest: dict, manifest_path: Path, video_path: str, error: str):
|
||||||
entry = get_file_entry(manifest, video_path)
|
entry = get_file_entry(manifest, video_path)
|
||||||
entry["audio_extracted"] = False
|
prev = entry.get("extraction") or {}
|
||||||
entry["error"] = error
|
entry["extraction"] = {
|
||||||
entry["updated_at"] = time.strftime("%Y-%m-%dT%H:%M:%S")
|
"extracted": False,
|
||||||
|
"audio_path": prev.get("audio_path"),
|
||||||
|
"split_segments": prev.get("split_segments") or [],
|
||||||
|
"error": error,
|
||||||
|
"updated_at": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||||
|
}
|
||||||
save_manifest(manifest_path, manifest)
|
save_manifest(manifest_path, manifest)
|
||||||
|
|
||||||
|
|
||||||
@@ -220,22 +246,25 @@ def show_status(manifest: dict, all_videos: list):
|
|||||||
print("\n" + "=" * 60)
|
print("\n" + "=" * 60)
|
||||||
print("📊 提取音频进度状态")
|
print("📊 提取音频进度状态")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
done = sum(1 for v in all_videos
|
done, failed = 0, 0
|
||||||
if manifest["files"].get(v, {}).get("audio_extracted"))
|
for v in all_videos:
|
||||||
failed = sum(1 for v in all_videos
|
ext = ((manifest["files"].get(v) or {}).get("extraction")) or {}
|
||||||
if manifest["files"].get(v, {}).get("error"))
|
if ext.get("extracted"):
|
||||||
pending = len(all_videos) - done
|
done += 1
|
||||||
|
elif ext.get("error"):
|
||||||
|
failed += 1
|
||||||
|
pending = len(all_videos) - done - failed
|
||||||
print(f" 总文件数 : {len(all_videos)}")
|
print(f" 总文件数 : {len(all_videos)}")
|
||||||
print(f" 已完成 : {done}")
|
print(f" 已完成 : {done}")
|
||||||
print(f" 失败 : {failed}")
|
print(f" 失败 : {failed}")
|
||||||
print(f" 待处理 : {pending}")
|
print(f" 待处理 : {pending}")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
for v in all_videos:
|
for v in all_videos:
|
||||||
entry = manifest["files"].get(v, {})
|
ext = ((manifest["files"].get(v) or {}).get("extraction")) or {}
|
||||||
if entry.get("audio_extracted"):
|
if ext.get("extracted"):
|
||||||
status = "✅ 完成"
|
status = "✅ 完成"
|
||||||
elif entry.get("error"):
|
elif ext.get("error"):
|
||||||
status = f"❌ 失败: {entry['error']}"
|
status = f"❌ 失败: {ext['error']}"
|
||||||
else:
|
else:
|
||||||
status = "⏳ 待处理"
|
status = "⏳ 待处理"
|
||||||
print(f" {status} {Path(v).name}")
|
print(f" {status} {Path(v).name}")
|
||||||
@@ -292,9 +321,10 @@ def main():
|
|||||||
pending = []
|
pending = []
|
||||||
for v in all_videos:
|
for v in all_videos:
|
||||||
entry = get_file_entry(manifest, v)
|
entry = get_file_entry(manifest, v)
|
||||||
already_done = entry.get("audio_extracted") and entry.get("audio_path") and \
|
ext = entry.get("extraction") or {}
|
||||||
mp3_exists(entry["audio_path"])
|
already_done = ext.get("extracted") and ext.get("audio_path") and \
|
||||||
is_failed = bool(entry.get("error"))
|
mp3_exists(ext["audio_path"])
|
||||||
|
is_failed = bool(ext.get("error"))
|
||||||
|
|
||||||
if already_done:
|
if already_done:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -66,13 +66,38 @@ def log(msg: str, level: str = "INFO"):
|
|||||||
print(f"[{ts}] {prefix} {msg}", flush=True)
|
print(f"[{ts}] {prefix} {msg}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
# ─── Manifest 操作 (key 包含 model,支持同一音频多模型共存) ────────────────────
|
# ─── Manifest 操作 (v2 schema) ────────────────────────────────────────────────
|
||||||
|
#
|
||||||
|
# v2 schema:每条记录以 视频路径 (extract_audio 流) 或 音频路径 (独立转录) 为 key,
|
||||||
|
# 包含 extraction + transcriptions 两部分。extract_audio 与 transcribe_audio
|
||||||
|
# 共写同一条记录,互不覆盖。
|
||||||
|
#
|
||||||
|
# {
|
||||||
|
# "<key>": {
|
||||||
|
# "extraction": { "extracted": bool, "audio_path": str, "split_segments": [],
|
||||||
|
# "error": str|None, "updated_at": str } | null,
|
||||||
|
# "transcriptions": {
|
||||||
|
# "<model>[:<suffix>]": { "transcribed": bool, "txt_path": str, "model": str,
|
||||||
|
# "language": str|None, "output_suffix": str|None,
|
||||||
|
# "split_segments": [], "error": str|None,
|
||||||
|
# "updated_at": str }
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
MANIFEST_VERSION = 2
|
||||||
|
|
||||||
|
|
||||||
def load_manifest(manifest_path: Path) -> dict:
|
def load_manifest(manifest_path: Path) -> dict:
|
||||||
if manifest_path.exists():
|
if not manifest_path.exists():
|
||||||
|
return {"version": MANIFEST_VERSION, "files": {}}
|
||||||
with open(manifest_path, encoding="utf-8") as f:
|
with open(manifest_path, encoding="utf-8") as f:
|
||||||
return json.load(f)
|
data = json.load(f)
|
||||||
return {"version": 1, "files": {}}
|
if data.get("version", 1) < MANIFEST_VERSION:
|
||||||
|
log("检测到旧版 manifest.json (v1),将以 v2 schema 重新初始化 - 旧记录将被覆盖", "WARN")
|
||||||
|
return {"version": MANIFEST_VERSION, "files": {}}
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
def save_manifest(manifest_path: Path, manifest: dict):
|
def save_manifest(manifest_path: Path, manifest: dict):
|
||||||
@@ -81,31 +106,56 @@ def save_manifest(manifest_path: Path, manifest: dict):
|
|||||||
json.dump(manifest, f, ensure_ascii=False, indent=2)
|
json.dump(manifest, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
|
||||||
def transcribe_key(model: str, audio_path: str, output_suffix: str = "") -> str:
|
def transcription_subkey(model: str, output_suffix: str = "") -> str:
|
||||||
"""
|
"""transcriptions 字典的 subkey:<model> 或 <model>:<suffix>"""
|
||||||
manifest key 由 model + output_suffix + 音频路径共同决定,
|
|
||||||
因此改变 --output-suffix 会形成新的 key(不会误跳过)。
|
|
||||||
"""
|
|
||||||
if output_suffix:
|
if output_suffix:
|
||||||
return f"transcribe:{model}:{output_suffix}:{audio_path}"
|
return f"{model}:{output_suffix}"
|
||||||
return f"transcribe:{model}:{audio_path}"
|
return model
|
||||||
|
|
||||||
|
|
||||||
|
def find_entry_key_for_audio(manifest: dict, audio_path: str) -> str:
|
||||||
|
"""
|
||||||
|
反向查找:audio 属于哪条 manifest 记录。
|
||||||
|
- 若某视频记录的 extraction.audio_path == audio → 归入该视频 key
|
||||||
|
- 否则以 audio_path 自身为 key(独立音频场景)
|
||||||
|
"""
|
||||||
|
for key, entry in manifest["files"].items():
|
||||||
|
extraction = entry.get("extraction")
|
||||||
|
if extraction and extraction.get("audio_path") == audio_path:
|
||||||
|
return key
|
||||||
|
return audio_path
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_entry(manifest: dict, key: str) -> dict:
|
||||||
|
if key not in manifest["files"]:
|
||||||
|
manifest["files"][key] = {
|
||||||
|
"extraction": None,
|
||||||
|
"transcriptions": {},
|
||||||
|
}
|
||||||
|
entry = manifest["files"][key]
|
||||||
|
if "transcriptions" not in entry:
|
||||||
|
entry["transcriptions"] = {}
|
||||||
|
return entry
|
||||||
|
|
||||||
|
|
||||||
def get_transcribe_entry(manifest: dict, audio_path: str, model: str,
|
def get_transcribe_entry(manifest: dict, audio_path: str, model: str,
|
||||||
output_suffix: str = "") -> dict:
|
output_suffix: str = "") -> dict:
|
||||||
key = transcribe_key(model, audio_path, output_suffix)
|
"""获取或初始化某个 (音频, 模型, suffix) 的转录记录"""
|
||||||
if key not in manifest["files"]:
|
key = find_entry_key_for_audio(manifest, audio_path)
|
||||||
manifest["files"][key] = {
|
parent = ensure_entry(manifest, key)
|
||||||
|
subkey = transcription_subkey(model, output_suffix)
|
||||||
|
if subkey not in parent["transcriptions"]:
|
||||||
|
parent["transcriptions"][subkey] = {
|
||||||
"transcribed": False,
|
"transcribed": False,
|
||||||
"txt_path": None,
|
"txt_path": None,
|
||||||
"split_segments": [],
|
"split_segments": [],
|
||||||
"error": None,
|
|
||||||
"model": model,
|
"model": model,
|
||||||
"language": None,
|
"language": None,
|
||||||
"output_suffix": output_suffix or None,
|
"output_suffix": output_suffix or None,
|
||||||
|
"error": None,
|
||||||
"updated_at": None,
|
"updated_at": None,
|
||||||
}
|
}
|
||||||
return manifest["files"][key]
|
return parent["transcriptions"][subkey]
|
||||||
|
|
||||||
|
|
||||||
def mark_transcribed(manifest: dict, manifest_path: Path, audio_path: str,
|
def mark_transcribed(manifest: dict, manifest_path: Path, audio_path: str,
|
||||||
@@ -305,31 +355,43 @@ def show_status(manifest: dict, all_audios: list):
|
|||||||
print("📊 音频转录进度状态(按 模型 x 音频 展示)")
|
print("📊 音频转录进度状态(按 模型 x 音频 展示)")
|
||||||
print("=" * 70)
|
print("=" * 70)
|
||||||
|
|
||||||
entries = {k: v for k, v in manifest["files"].items() if k.startswith("transcribe:")}
|
all_trans = []
|
||||||
done = sum(1 for v in entries.values() if v.get("transcribed"))
|
for parent_key, entry in manifest["files"].items():
|
||||||
failed = sum(1 for v in entries.values() if v.get("error"))
|
for subkey, tr in (entry.get("transcriptions") or {}).items():
|
||||||
print(f" Manifest 记录: {len(entries)} 已完成: {done} 失败: {failed}")
|
all_trans.append((parent_key, subkey, tr))
|
||||||
|
|
||||||
|
done = sum(1 for _, _, v in all_trans if v.get("transcribed"))
|
||||||
|
failed = sum(1 for _, _, v in all_trans if v.get("error"))
|
||||||
|
print(f" 转录记录: {len(all_trans)} 已完成: {done} 失败: {failed}")
|
||||||
print("=" * 70)
|
print("=" * 70)
|
||||||
|
|
||||||
for a in all_audios:
|
for a in all_audios:
|
||||||
print(f"\n 📄 {Path(a).name}")
|
print(f"\n 📄 {Path(a).name}")
|
||||||
matching = [(k, v) for k, v in entries.items() if k.endswith(f":{a}")]
|
matching = []
|
||||||
|
for parent_key, entry in manifest["files"].items():
|
||||||
|
extraction = entry.get("extraction") or {}
|
||||||
|
is_match = extraction.get("audio_path") == a or parent_key == a
|
||||||
|
if not is_match:
|
||||||
|
continue
|
||||||
|
for subkey, tr in (entry.get("transcriptions") or {}).items():
|
||||||
|
matching.append((subkey, tr))
|
||||||
|
|
||||||
if not matching:
|
if not matching:
|
||||||
print(f" ⏳ 无记录(未转录)")
|
print(f" ⏳ 无记录(未转录)")
|
||||||
continue
|
continue
|
||||||
for key, entry in matching:
|
for subkey, tr in matching:
|
||||||
m = entry.get("model", "?")
|
m = tr.get("model", "?")
|
||||||
suffix = entry.get("output_suffix") or ""
|
suffix = tr.get("output_suffix") or ""
|
||||||
suffix_tag = f" suffix={suffix}" if suffix else ""
|
suffix_tag = f" suffix={suffix}" if suffix else ""
|
||||||
if entry.get("transcribed"):
|
if tr.get("transcribed"):
|
||||||
size = ""
|
size = ""
|
||||||
if entry.get("txt_path") and os.path.exists(entry["txt_path"]):
|
if tr.get("txt_path") and os.path.exists(tr["txt_path"]):
|
||||||
size = f" {os.path.getsize(entry['txt_path']) // 1024}KB"
|
size = f" {os.path.getsize(tr['txt_path']) // 1024}KB"
|
||||||
lang = entry.get("language") or "auto"
|
lang = tr.get("language") or "auto"
|
||||||
txt_name = Path(entry['txt_path']).name if entry.get('txt_path') else '?'
|
txt_name = Path(tr['txt_path']).name if tr.get('txt_path') else '?'
|
||||||
print(f" ✅ [{m:6s}] lang={lang}{suffix_tag}{size} → {txt_name}")
|
print(f" ✅ [{m:6s}] lang={lang}{suffix_tag}{size} → {txt_name}")
|
||||||
elif entry.get("error"):
|
elif tr.get("error"):
|
||||||
print(f" ❌ [{m:6s}]{suffix_tag} {entry['error'][:60]}")
|
print(f" ❌ [{m:6s}]{suffix_tag} {tr['error'][:60]}")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user