Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 15 additions & 11 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ AI_PUBLISH_PROVIDER=codex
AI_CODEX_PATH=codex
AI_CODEX_HOME=
AI_CODEX_MODEL=gpt-5.6-sol
AI_CODEX_TIMEOUT_SECONDS=300
AI_CODEX_TIMEOUT_SECONDS=600
AI_REQUEST_TIMEOUT_SECONDS=120

# 远程 OpenAI-compatible / DeepSeek:文字稿分析
Expand Down Expand Up @@ -68,10 +68,22 @@ AI_LOCAL_HEALTH_TIMEOUT_SECONDS=30
# 语音转写
# ========================

# volcengine:远程火山引擎;local:本地 faster-whisper
TRANSCRIPTION_PROVIDER=volcengine
# 默认完全离线,本地音频不会上传;火山配置仅保留用于手动回滚
TRANSCRIPTION_PROVIDER=local
TRANSCRIPTION_FALLBACK_PROVIDER=
TRANSCRIPTION_OFFLINE_ONLY=true
TRANSCRIPTION_MODEL=large-v3
TRANSCRIPTION_MODEL_REVISION=edaa852ec7e145841d8ffdb056a99866b5f0a478
TRANSCRIPTION_MODEL_CACHE_DIR=E:\直播间切片工作流存储\_模型\faster-whisper
TRANSCRIPTION_LOCAL_FILES_ONLY=true
TRANSCRIPTION_LANGUAGE=zh
TRANSCRIPTION_DEVICE=cuda
TRANSCRIPTION_COMPUTE_TYPE=float16
TRANSCRIPTION_CPU_FALLBACK_MODEL=medium
TRANSCRIPTION_CHUNK_SECONDS=120
TRANSCRIPTION_CHUNK_OVERLAP_SECONDS=5

# 以下值不会在完全离线模式下使用,也不会被自动删除。
VOLCENGINE_ASR_API_URL=https://openspeech.bytedance.com/api/v3/auc/bigmodel/recognize/flash
VOLCENGINE_ASR_API_KEY=
VOLCENGINE_ASR_APP_KEY=
Expand All @@ -80,14 +92,6 @@ VOLCENGINE_ASR_RESOURCE_ID=volc.bigasr.auc_turbo
VOLCENGINE_ASR_TIMEOUT_SECONDS=300
VOLCENGINE_ASR_AUDIO_FORMAT=mp3

TRANSCRIPTION_MODEL=medium
TRANSCRIPTION_LANGUAGE=zh
TRANSCRIPTION_DEVICE=cpu
TRANSCRIPTION_COMPUTE_TYPE=int8
TRANSCRIPTION_CPU_FALLBACK_MODEL=medium
TRANSCRIPTION_CHUNK_SECONDS=120
TRANSCRIPTION_CHUNK_OVERLAP_SECONDS=5

# ========================
# 排期与 Windows Chrome Worker
# ========================
Expand Down
28 changes: 28 additions & 0 deletions DEVELOPMENT_LOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,33 @@
# Development Log

## 2026-09-01 最新任务恢复与本地转写简体化

- 本地 `faster-whisper` 的段落和逐词文本在写入分段 checkpoint 前统一经过 OpenCC `t2s.json`;只转换繁简字形,继续保留“计程车、软体”等台湾用词,英文、数字和时间戳不变。
- 固定新增 `opencc==1.4.2`;本地转写 checkpoint 模型身份加入 `opencc-t2s-v1`,旧繁体分块不会被新任务静默复用。转写进度 JSON 同步记录实际文本标准化版本。
- AI 完整性门禁前移到 `AI_ANALYZING`:覆盖率或单元不完整会直接进入 `FAILED_AI_ANALYZING`;`CLIP_SELECTING` 保留二次防护,不会继续生成切片或发布记录。
- `/api/tasks/{task_id}/process/auto-retry` 在发现计费结果不确定的 AI 单元时先返回结构化 409。任务详情会明确说明风险并要求二次确认;取消时旧 Job 和全部证据保持不变。
- 输入未变化时,确认后原位重置不确定单元并复用成功 AI checkpoint;transcript 已变化时,从 `AI_ANALYZING` 创建独立新 Job,旧失败 Job 不修改。有任何下游切片或发布记录时继续 fail-closed。
- Codex 单窗口默认超时由 300 秒调整为 600 秒;新增 transcript 原位转换工具,转换前写入 `transcripts/backups/`,并校验时间戳、行数和 Markdown 结构。
- 专项回归 `66 passed`、最终完整回归 `825 passed`;Ruff、Python compileall、JavaScript 语法和 `git diff --check` 均通过。`release_gate.ps1` 因当前分支没有正式版 `acceptance-results/latest.json` 按设计拒绝放行;正式服务部署和任务 `614fb38401e0` 的现场恢复继续单独验收。
- 已把两项代码提交同步到运行工作树 `codex/runtime-offline-transcription-cutover`,项目环境确认安装 `opencc==1.4.2`;运行 `.env` 先备份,再只把 `AI_CODEX_TIMEOUT_SECONDS` 从 300 调整为 600。
- 活动 SQLite 已通过 Online Backup 保存到 `data/backups/workflow-before-latest-ai-retry-20260901-165534-858701-d0d0492a.sqlite3`;备份及活动库均为 `quick_check=ok`、外键违规 0。恢复前任务无活动 Job、无切片、无发布记录。
- 任务 `614fb38401e0` 的原 transcript 已备份到任务目录 `transcripts/backups/transcript.before-t2s-20260901-165555.md`,原 SHA-256 为 `d4f17ff7...d09f9d`;原位转换后的 SHA-256 为 `c666a5dc...43f18d`,1407 行、2781 个时间戳和 Markdown 结构保持不变。
- 只通过 Alter 重启了 `Niuma-Studio` Web:8001 Listener 更新为 PID `151000`,祖先进程落在运行工作树;8765 发布 Worker Listener 仍为 PID `39056`。`/health`、深度 readiness、Scheduler 和 Worker 均健康。
- 未确认重试先按设计返回 `409 / ai_retry_confirmation_required`;显式确认后创建独立 Job `dec329256a0b`,旧失败 Job `434da2a9bc97` 及证据保持不变。新 AI Run `6c497a6ac3f7` 完成 `18/18` 单元、覆盖率 100%,随后生成 5 个切片并停在 `PENDING_SUBTITLE_REVIEW`;发布记录仍为 0,未触发抖音或 B站投稿。

## 2026-08-31 完全离线长音频转写

- 默认转写 Provider 改为本地 `faster-whisper`,固定 `large-v3@edaa852e / cuda / float16`;`TRANSCRIPTION_OFFLINE_ONLY=true` 时,任务页、自动流水线、重试入口和显式火山请求都会在远程 HTTP 请求前拒绝执行。
- 新增固定版本模型缓存和 Windows CUDA 运行时管理:模型只从 `E:\直播间切片工作流存储\_模型\faster-whisper` 读取,任务运行中不会偷偷联网下载;当前进程会加载项目 `.venv` 内的 cuBLAS 12 DLL,并复用 CTranslate2 自带的 cuDNN 9。
- 新增一次性初始化脚本 `scripts/setup_local_transcription.ps1`,锁定下载 `large-v3` GPU 主模型和 `medium` CPU/int8 兜底模型,写入原子模型清单,并支持真实 20 秒音频的 GPU 推理冒烟。
- 新增 `scripts/benchmark_local_transcription.py`,使用隔离数据库和任务目录执行 10/40 分钟本地验收;支持在指定分块保存 checkpoint 后主动停止,并用同一目录原地续跑,不接触正式任务数据。
- 保留原 120 秒分块、5 秒重叠和 SQLite checkpoint;checkpoint 在实际 GPU/CPU 模型加载完成后才创建,`transcription_runs.model` 使用带 revision 的模型身份,音频指纹改为完整 SHA-256。
- `/api/tasks/{id}/transcript-status` 增加离线锁、模型缓存、GPU 和 revision 状态;系统状态页展示完全离线、模型/DLL 就绪度、实际配置设备和外部转写费用 `¥0`,火山配置保留作人工回滚且不删除。
- 新增离线边界专项回归,覆盖默认 Provider、409 阻断、HTTP 前置拦截、模型缺失、DLL 路径、CPU 兜底、模型 revision 与完整音频指纹;正式 `.env` 和运行中 Web/Worker 尚未切换或重启,等待真实素材验收后再启用。
- 真实验收已验证主模型 `model.bin` 大小 `3,087,284,237` 字节及官方 SHA-256 `69f74147e3334731bc3a76048724833325d2ec74642fb52620eda87352e3d4f1`;20 秒中文音频 GPU 推理成功,没有退回 CPU。`medium` 兜底模型的 `1,527,906,378` 字节大文件也通过官方 SHA-256 `9b45e1009dcc4ab601eff815b61d80e60ce3fd8c74c1a14f4a282258286b51ae`,并实际以 `cpu / int8` 加载成功;就绪检查同时兼容官方模型使用的 `vocabulary.json` 或 `vocabulary.txt`。
- 同一份 41 分 18 秒含多人对白和音乐素材,10 分钟样本用时 83.2 秒并输出 336 条;完整素材先在第 1/22 分块后主动中断,再复用该 checkpoint,用时 291.85 秒完成其余分块并输出 1085 条。SQLite 记录为 `large-v3@edaa852e / cuda / float16`、22/22 完成、完整 64 位 SHA-256;运行中转写进程 TCP 连接数为 0,GPU 采样为 65% 且约占 8.1 GB 显存。
- 已在 `E:\直播间切片工作流存储\_验收\offline-transcription-20260831\41min\spot-checks` 均匀生成 10 个 15 秒试听片段和人工清单;人名、关键对白和剪辑时间点仍待用户试听确认,因此没有切换正式 `.env`,也没有重启 Web/Worker。

## 2026-08-28 2.1 集成 PR 与 Docker 冒烟修复

- 将当前线性领先 `master` 的 26 个提交完整保留到 `codex/integrate-v2.1-stable`,新增独立空白清理提交并创建顶层集成 PR #60;不 rebase、不 squash,也不自动合并。
Expand Down
15 changes: 15 additions & 0 deletions NEXT_STEPS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Next Steps

## 2026-09-01 最新任务恢复验收

1. [x] 运行工作树已同步代码并固定安装 OpenCC;运行 `.env` 只把 Codex 单窗口超时调整为 600 秒,原配置已备份。
2. [x] 活动 SQLite、当前 transcript 均已先备份;数据库完整性正常,transcript 已转为仅字形简体且结构保持不变。
3. [x] 只通过 Alter 重启 `Niuma-Studio` Web;8001 新进程、祖先链、深度 readiness 和 Scheduler 正常,8765 发布 Worker 未重启且健康。
4. [x] 独立 Job `dec329256a0b` 已完成,AI 覆盖率 `18/18(100%)`,生成 5 个切片并停在 `PENDING_SUBTITLE_REVIEW`;旧失败 Job 保留,发布记录为 0。
5. 下一步由用户在任务详情页人工审核字幕和切片。不要点击立即发送,也不要创建或修改抖音/B站排期;PR #72 等 CI 通过后仍需用户确认才可合并。

## 2026-08-31 完全离线转写验收与启用

1. 固定版本 cuBLAS 12、`large-v3` 主模型和 `medium / CPU / int8` 兜底模型均已初始化并通过加载校验;20 秒、10 分钟和完整 41 分钟 GPU 验收均通过,完整素材续跑约 4 分 52 秒完成并成功复用第一个 checkpoint。
2. 打开 `E:\直播间切片工作流存储\_验收\offline-transcription-20260831\41min\spot-checks\README.md`,依次试听 10 个 15 秒片段,核对人名、关键对白和剪辑起止点;第 6、10 段已标出需要重点确认的疑似人名/背景声识别。
3. 只有人工试听确认后,才把正式 `.env` 切换为本地离线配置,并在记录进程、端口和健康状态后受控重启 Web/Worker。当前正式服务仍保持原状。
4. 回滚时只需恢复旧 `TRANSCRIPTION_PROVIDER` 并设置 `TRANSCRIPTION_OFFLINE_ONLY=false`;不要删除火山配置、E 盘模型缓存、历史 transcript 或 SQLite checkpoint。

## 2026-08-28 2.1 集成收口

1. 等待 PR #60 的 Linux、Windows 与 Docker 三组 CI 全部通过;Docker 页面冒烟必须确认 `/`、`/tasks`、`/clips`、`/publish` 均返回成功。
Expand Down
6 changes: 6 additions & 0 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@
- 许可证:MIT License。
- 本项目锁定运行时版本 `pysubs2==1.9.0`;完整许可证保存在 `third_party_licenses/pysubs2-LICENSE.txt`。

## OpenCC 1.4.2

- 项目:https://github.com/BYVoid/OpenCC
- 用途:把本地 faster-whisper 的繁体字形输出转换为简体字形;本项目使用 `t2s.json`,不做词汇本地化。
- 许可证:Apache License 2.0;本项目通过 `opencc==1.4.2` 安装包使用,安装包内包含完整许可证。

## wavesurfer.js 7.12.11

- 项目:https://github.com/katspaugh/wavesurfer.js
Expand Down
31 changes: 25 additions & 6 deletions app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
import os
from pathlib import Path

from app.core.transcription_defaults import (
CPU_FALLBACK_TRANSCRIPTION_MODEL,
PRIMARY_TRANSCRIPTION_MODEL,
PRIMARY_TRANSCRIPTION_MODEL_REVISION,
)


PROJECT_ROOT = Path(__file__).resolve().parents[2]
EXTERNAL_STORAGE_ROOT = Path(r"E:\直播间切片工作流存储")
Expand Down Expand Up @@ -88,7 +94,7 @@ class Settings:
ai_codex_path: str = _env("AI_CODEX_PATH", "codex")
ai_codex_home: str = _env("AI_CODEX_HOME", "")
ai_codex_model: str = _env("AI_CODEX_MODEL", "gpt-5.6-sol")
ai_codex_timeout_seconds: int = int(_env("AI_CODEX_TIMEOUT_SECONDS", "300"))
ai_codex_timeout_seconds: int = int(_env("AI_CODEX_TIMEOUT_SECONDS", "600"))

ai_analysis_remote_base_url: str = _env_first(
("AI_ANALYSIS_REMOTE_BASE_URL", "AI_REMOTE_BASE_URL"),
Expand Down Expand Up @@ -221,13 +227,26 @@ class Settings:
ai_windows_wsl_setup_acknowledged: str = _env("AI_WINDOWS_WSL_SETUP_ACKNOWLEDGED", "true")
ai_model_context_window: int = int(_env("AI_MODEL_CONTEXT_WINDOW", "1000000"))
ai_model_auto_compact_token_limit: int = int(_env("AI_MODEL_AUTO_COMPACT_TOKEN_LIMIT", "900000"))
transcription_provider: str = _env("TRANSCRIPTION_PROVIDER", "volcengine")
transcription_provider: str = _env("TRANSCRIPTION_PROVIDER", "local")
transcription_fallback_provider: str = _env("TRANSCRIPTION_FALLBACK_PROVIDER", "")
transcription_model: str = _env("TRANSCRIPTION_MODEL", "medium")
transcription_offline_only: bool = _env_bool("TRANSCRIPTION_OFFLINE_ONLY", True)
transcription_model: str = _env("TRANSCRIPTION_MODEL", PRIMARY_TRANSCRIPTION_MODEL)
transcription_model_revision: str = _env(
"TRANSCRIPTION_MODEL_REVISION",
PRIMARY_TRANSCRIPTION_MODEL_REVISION,
)
transcription_model_cache_dir: Path = _env_path(
"TRANSCRIPTION_MODEL_CACHE_DIR",
_env_path("STORAGE_ROOT", EXTERNAL_STORAGE_ROOT) / "_模型" / "faster-whisper",
)
transcription_local_files_only: bool = _env_bool("TRANSCRIPTION_LOCAL_FILES_ONLY", True)
transcription_language: str = _env("TRANSCRIPTION_LANGUAGE", "zh")
transcription_device: str = _env("TRANSCRIPTION_DEVICE", "cpu")
transcription_compute_type: str = _env("TRANSCRIPTION_COMPUTE_TYPE", "int8")
transcription_cpu_fallback_model: str = _env("TRANSCRIPTION_CPU_FALLBACK_MODEL", "medium")
transcription_device: str = _env("TRANSCRIPTION_DEVICE", "cuda")
transcription_compute_type: str = _env("TRANSCRIPTION_COMPUTE_TYPE", "float16")
transcription_cpu_fallback_model: str = _env(
"TRANSCRIPTION_CPU_FALLBACK_MODEL",
CPU_FALLBACK_TRANSCRIPTION_MODEL,
)
transcription_chunk_seconds: int = int(_env("TRANSCRIPTION_CHUNK_SECONDS", "120"))
transcription_chunk_overlap_seconds: int = int(_env("TRANSCRIPTION_CHUNK_OVERLAP_SECONDS", "5"))
volcengine_asr_api_url: str = _env(
Expand Down
15 changes: 15 additions & 0 deletions app/core/transcription_defaults.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""离线语音转写的固定模型与版本。"""

PRIMARY_TRANSCRIPTION_MODEL = "large-v3"
PRIMARY_TRANSCRIPTION_MODEL_REPOSITORY = "Systran/faster-whisper-large-v3"
PRIMARY_TRANSCRIPTION_MODEL_REVISION = "edaa852ec7e145841d8ffdb056a99866b5f0a478"
PRIMARY_TRANSCRIPTION_MODEL_BIN_SIZE = 3_087_284_237
PRIMARY_TRANSCRIPTION_MODEL_BIN_SHA256 = "69f74147e3334731bc3a76048724833325d2ec74642fb52620eda87352e3d4f1"

CPU_FALLBACK_TRANSCRIPTION_MODEL = "medium"
CPU_FALLBACK_TRANSCRIPTION_MODEL_REPOSITORY = "Systran/faster-whisper-medium"
CPU_FALLBACK_TRANSCRIPTION_MODEL_REVISION = "08e178d48790749d25932bbc082711ddcfdfbc4f"
CPU_FALLBACK_TRANSCRIPTION_MODEL_BIN_SIZE = 1_527_906_378
CPU_FALLBACK_TRANSCRIPTION_MODEL_BIN_SHA256 = "9b45e1009dcc4ab601eff815b61d80e60ce3fd8c74c1a14f4a282258286b51ae"

WINDOWS_CUBLAS_PACKAGE = "nvidia-cublas-cu12==12.9.2.10"
54 changes: 51 additions & 3 deletions app/models/settings.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
from urllib.parse import urlsplit

from pydantic import BaseModel, Field, field_validator
from pathlib import Path, PureWindowsPath

from pydantic import BaseModel, Field, field_validator, model_validator

from app.core.transcription_defaults import (
PRIMARY_TRANSCRIPTION_MODEL,
PRIMARY_TRANSCRIPTION_MODEL_REVISION,
)


_LOCAL_AI_HOSTS = {"localhost", "127.0.0.1", "::1", "host.docker.internal", "host.containers.internal"}
Expand Down Expand Up @@ -46,10 +53,32 @@ class AIConfigUpdate(BaseModel):
ai_codex_path: str = Field(default="codex", max_length=1000)
ai_codex_home: str = Field(default="", max_length=1000)
ai_codex_model: str = Field(default="gpt-5.6-sol", max_length=200)
ai_codex_timeout_seconds: int = Field(default=300, ge=10, le=1800)
ai_codex_timeout_seconds: int = Field(default=600, ge=10, le=1800)

transcription_provider: str = Field(default="volcengine", pattern="^(volcengine|local)$", max_length=20)
transcription_provider: str = Field(default="local", pattern="^(volcengine|local)$", max_length=20)
transcription_fallback_provider: str = Field(default="", pattern="^(|volcengine|local)$", max_length=20)
transcription_offline_only: bool = Field(default=True)
transcription_model: str = Field(
default=PRIMARY_TRANSCRIPTION_MODEL,
pattern=r"^[A-Za-z0-9._/-]+$",
max_length=200,
)
transcription_model_revision: str = Field(
default=PRIMARY_TRANSCRIPTION_MODEL_REVISION,
pattern=r"^[0-9a-f]{40}$",
max_length=40,
)
transcription_model_cache_dir: str = Field(
default=r"E:\直播间切片工作流存储\_模型\faster-whisper",
max_length=2000,
)
transcription_local_files_only: bool = Field(default=True)
transcription_device: str = Field(default="cuda", pattern="^(cuda|cpu|auto)$", max_length=20)
transcription_compute_type: str = Field(
default="float16",
pattern="^(float16|int8_float16|int8|float32|auto)$",
max_length=30,
)
volcengine_asr_api_url: str = Field(default="", max_length=2000)
volcengine_asr_api_key: str = Field(default="", max_length=4096)
volcengine_asr_app_key: str = Field(default="", max_length=4096)
Expand Down Expand Up @@ -113,6 +142,25 @@ def reject_control_characters(cls, value):
def validate_volcengine_url(cls, value: str) -> str:
return _validate_http_url(value, https_only=True)

@field_validator("transcription_model_cache_dir")
@classmethod
def validate_transcription_model_cache_dir(cls, value: str) -> str:
text = value.strip()
if not text or not (Path(text).is_absolute() or PureWindowsPath(text).is_absolute()):
raise ValueError("本地转写模型缓存目录必须是绝对路径")
return text

@model_validator(mode="after")
def validate_offline_transcription_policy(self):
if self.transcription_offline_only:
if self.transcription_provider != "local":
raise ValueError("完全离线转写开启时,转写方式必须为 local")
if self.transcription_fallback_provider not in {"", "local"}:
raise ValueError("完全离线转写开启时,不能配置远程转写兜底")
if not self.transcription_local_files_only:
raise ValueError("完全离线转写开启时,必须只读取本地模型文件")
return self

@field_validator(*_REMOTE_URL_FIELDS)
@classmethod
def validate_remote_url(cls, value: str) -> str:
Expand Down
Loading
Loading