feat(kb-workflow): 切换为 Whisper 语音识别

- 将步骤5从 OCR 改为 Whisper 语音识别
- 更新 kb-whisper 支持自动音频转换
- 默认使用中文语言 (zh) 和 base 模型
- 更新输出格式为 {platform}_{id}.txt/.srt/.json
- 添加 FunASR 作为备用方案
This commit is contained in:
2026-02-13 09:04:27 +08:00
parent 9102a973cd
commit 29dd236bb9
7 changed files with 844 additions and 155 deletions
+2
View File
@@ -0,0 +1,2 @@
# FunASR 模型缓存目录
FUNASR_CACHE_DIR=~/.cache/modelscope/hub
+212
View File
@@ -0,0 +1,212 @@
# KB FunASR - 本地语音识别
基于阿里 FunASR 的本地语音识别工具,支持将音视频文件转换为文本和 JSON 格式。
## 功能特性
- 🎯 **本地执行** - 完全离线运行,保护隐私
- 🚀 **多模型支持** - Paraformer、SenseVoice 等模型可选
- 📝 **多格式输出** - JSON、TXT 两种格式
- ⚡ **异步执行** - 支持后台运行,不阻塞主代理
- 🎬 **广泛兼容** - 支持常见音视频格式(MP3、MP4、WAV 等)
- 🇨🇳 **中文优化** - Paraformer 模型针对中文语音优化
## 安装前提
### 1. 安装依赖
```bash
pip install funasr modelscope
```
### 2. 配置环境变量(可选)
在 `~/.clawdbot/clawdbot.json` 中添加:
```json5
{
skills: {
entries: {
"kb-funasr": {
env: {
FUNASR_CACHE_DIR: "~/.cache/modelscope/hub"
}
}
}
}
}
```
或通过命令行配置:
```bash
clawdbot skills config kb-funasr FUNASR_CACHE_DIR "~/.cache/modelscope/hub"
```
## 使用方法
### 基本用法
```bash
# 使用默认 Paraformer 中文模型处理
python funasr.py --input video.mp4
# 后台异步执行
python funasr.py --input video.mp4 --background
```
### 指定模型
```bash
# Paraformer 中文模型(推荐)
python funasr.py --input video.mp4 --model paraformer-zh
# Paraformer 多语言模型
python funasr.py --input video.mp4 --model paraformer
# SenseVoice 多语言模型
python funasr.py --input video.mp4 --model sensevoice
```
### 指定输出目录
```bash
python funasr.py --input video.mp4 --output ./transcripts
```
## 输出文件
对于输入 `video.mp4`,将生成以下文件:
```
output/
├── video.json # 详细结果(含时间戳)
└── video.txt # 纯文本
```
### JSON 格式
```json
{
"success": true,
"input_file": "video.mp4",
"model": "paraformer-zh",
"output_files": {
"json": "video.json",
"txt": "video.txt"
},
"duration_seconds": 120.5,
"segments": [
{
"id": 0,
"start": 0.0,
"end": 3.2,
"text": "这是第一段识别结果"
},
{
"id": 1,
"start": 3.2,
"end": 6.8,
"text": "这是第二段识别结果"
}
]
}
```
## 模型对比
| 模型 | 速度 | 精度 | 适用场景 |
|------|------|------|----------|
| paraformer-zh | ⚡⚡⚡ | ⭐⭐⭐⭐ | 中文语音(推荐) |
| paraformer | ⚡⚡ | ⭐⭐⭐⭐ | 多语言识别 |
| sensevoice | ⚡ | ⭐⭐⭐⭐⭐ | 高精度多语言 |
## 完整示例
### 示例 1: 转录播客
```bash
python funasr.py --input podcast.mp3 --model paraformer-zh --background
```
### 示例 2: 生成视频识别结果
```bash
python funasr.py --input tutorial.mp4 --model paraformer
```
### 示例 3: 批量处理(配合脚本)
```bash
#!/bin/bash
for video in videos/*.mp4; do
python funasr.py --input "$video" --model paraformer-zh
done
```
## 在 Workflow 中使用
在知识库处理流程中,如果视频没有识别结果:
```bash
# 步骤4: 下载视频后
# 步骤5: 使用 FunASR 生成识别结果
python funasr.py --input ${video_path} --model paraformer-zh --background
# 等待 FunASR 完成,然后继续后续步骤
```
## 技术细节
- **引擎**: 阿里 FunASR
- **模型框架**: ModelScope
- **音频采样率**: 自动转换为 16kHz
- **语言**: 根据模型支持不同语言
## 故障排查
### 问题: 模型下载失败
```bash
# 首次运行会自动下载,确保网络稳定
python funasr.py --input test.mp4
# 手动检查缓存目录
ls ~/.cache/modelscope/hub/
```
### 问题: 音频格式不支持
```bash
# 转换音频格式
ffmpeg -i input.avi -acodec libmp3lame output.mp3
python funasr.py --input output.mp3
```
### 问题: 依赖安装失败
```bash
# 使用清华镜像源
pip install funasr modelscope -i https://pypi.tuna.tsinghua.edu.cn/simple
```
## 性能参考
在 CPU 环境下(Intel i7):
- **paraformer-zh 模型**: ~2x 实时速度(1分钟音频 ~30秒处理)
- **paraformer 模型**: ~1.5x 实时速度(1分钟音频 ~40秒处理)
- **sensevoice 模型**: ~1x 实时速度(1分钟音频 ~1分钟处理)
## 与 Whisper 对比
| 特性 | FunASR | Whisper |
|------|--------|---------|
| 中文识别 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 速度 | ⚡⚡⚡ | ⚡⚡ |
| 模型大小 | 较小 | 较大 |
| 多语言支持 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
## 许可证
本技能使用 MIT 许可证,FunASR 使用 Apache 2.0 许可证。
+113
View File
@@ -0,0 +1,113 @@
---
name: kb-funasr
description: 本地语音识别,支持音视频转文字(基于阿里 FunASR)
---
# KB FunASR Skill
**kb-funasr** - 本地语音识别,使用阿里 FunASR 将音视频文件转换为文本和 JSON 格式输出。
## 什么时候使用
当用户需要:
- 将视频/音频文件转换为文字
- 获取带时间戳的详细识别结果
- 需要中文优化的语音识别(Paraformer 模型)
## 环境变量
在使用之前,必须检查环境变量是否已经设置。
| 变量名 | 用途 | 示例 |
|--------|------|------|
| `FUNASR_CACHE_DIR` | FunASR 模型缓存目录 | `~/.cache/modelscope/hub` |
## 使用方法
```bash
# 基本用法(使用 Paraformer 模型)
python funasr.py --input video.mp4
# 指定模型
python funasr.py --input video.mp4 --model paraformer-zh
# 指定输出目录
python funasr.py --input video.mp4 --output ./output
# 异步执行(推荐)
python funasr.py --input video.mp4 --background
```
## 命令参数
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--input` | 输入音视频文件路径 | 必需 |
| `--model` | FunASR 模型类型 | `paraformer-zh` |
| `--output` | 输出目录 | 文件所在目录 |
| `--background` | 后台异步执行 | False |
## 模型选项
- `paraformer-zh` - Paraformer 中文模型(轻量,中文优化,推荐)
- `paraformer` - Paraformer 多语言模型
- `sensevoice` - SenseVoice 多语言模型
## 输出文件
对于输入 `video.mp4`,将生成:
- `video.json` - 详细结果(包含时间戳、分段信息)
- `video.txt` - 纯文本内容
## 输出格式示例
### JSON 格式
```json
{
"success": true,
"input_file": "video.mp4",
"model": "paraformer-zh",
"output_files": {
"json": "video.json",
"txt": "video.txt"
},
"duration_seconds": 120.5,
"segments": [
{
"id": 0,
"start": 0.0,
"end": 3.2,
"text": "这是第一段识别结果"
}
]
}
```
## Workflow 中的使用
```bash
# 步骤4之后:如果已有音频但无字幕,可以使用 FunASR
python funasr.py --input ${video_path} --model paraformer-zh --background
```
## 安装
```bash
# 1. 安装依赖
pip install funasr modelscope
# 2. 首次运行时会自动下载模型到缓存目录
python funasr.py --input test.mp4
# 3. 复制技能到 skills 目录
cp -r kb-funasr/ ~/skills/
```
## 注意事项
- **异步执行**: 使用 `--background` 参数时,命令会在后台运行,不阻塞主代理
- **模型选择**: paraformer-zh 模型适合中文语音识别,速度较快,精度较高
- **文件格式**: 支持 MP3、MP4、WAV、FLAC、OGG、M4A 等常见格式
- **首次运行**: 首次运行时会自动下载模型文件,需要稳定的网络连接
+313
View File
@@ -0,0 +1,313 @@
#!/usr/bin/env python3
"""
KB FunASR - 本地语音识别工具
基于阿里 FunASR 实现音视频转文字
"""
import argparse
import json
import os
import subprocess
import sys
import tempfile
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Optional
class FunASRTranscriber:
"""FunASR 语音识别器"""
# 模型映射
MODEL_MAP = {
"paraformer-zh": "damo/speech_paraformer-large-vad_punc_asr_nat-zh-cn-16k-common-vocab8404-pytorch",
"paraformer": "damo/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-pytorch",
"sensevoice": "FunASR/SenseVoiceSmall"
}
def __init__(self, model: str = "paraformer-zh", cache_dir: Optional[str] = None):
"""初始化 FunASR 识别器
Args:
model: 模型名称
cache_dir: 模型缓存目录
"""
self.model_name = model
self.model_id = self.MODEL_MAP.get(model, self.MODEL_MAP["paraformer-zh"])
self.cache_dir = cache_dir or os.path.expanduser("~/.cache/modelscope/hub")
self._model = None
def _load_model(self):
"""延迟加载模型"""
if self._model is None:
try:
from funasr import AutoModel
print(f"正在加载模型: {self.model_name} ({self.model_id})")
print(f"缓存目录: {self.cache_dir}")
self._model = AutoModel(
model=self.model_id,
disable_pbar=False,
disable_log=False,
)
print("模型加载完成")
except ImportError:
print("错误: 未安装 funasr 库")
print("请运行: pip install funasr modelscope")
sys.exit(1)
def _convert_audio(self, input_path: str) -> str:
"""转换音频格式为 16kHz WAV
Args:
input_path: 输入文件路径
Returns:
转换后的文件路径(如果不需要转换则返回原路径)
"""
# 检查是否已经是 WAV 格式
if input_path.lower().endswith('.wav'):
# 检查采样率
try:
result = subprocess.run(
['ffprobe', '-v', 'error', '-show_entries',
'stream=sample_rate', '-of', 'default=noprint_wrappers=1:nokey=1',
input_path],
capture_output=True, text=True
)
if result.returncode == 0:
sample_rate = int(result.stdout.strip())
if sample_rate == 16000:
return input_path
except Exception:
pass
# 需要转换
output_path = input_path.rsplit('.', 1)[0] + '_converted.wav'
print(f"正在转换音频格式: {input_path} -> {output_path}")
try:
subprocess.run([
'ffmpeg', '-y', '-i', input_path,
'-acodec', 'pcm_s16le',
'-ar', '16000',
'-ac', '1',
output_path
], check=True, capture_output=True)
print("音频转换完成")
return output_path
except subprocess.CalledProcessError as e:
print(f"音频转换失败: {e}")
# 如果转换失败,尝试使用原文件
print("尝试使用原文件继续处理")
return input_path
except FileNotFoundError:
print("警告: 未找到 ffmpeg,跳过音频转换")
return input_path
def transcribe(self, input_path: str) -> Dict:
"""转录音频文件
Args:
input_path: 输入文件路径
Returns:
识别结果字典
"""
input_path = os.path.abspath(input_path)
if not os.path.exists(input_path):
return {
"success": False,
"error": f"文件不存在: {input_path}"
}
print(f"\n开始处理: {input_path}")
print(f"使用模型: {self.model_name}")
# 转换音频格式
audio_path = self._convert_audio(input_path)
# 加载模型
self._load_model()
# 执行识别
print("正在进行语音识别...")
try:
result = self._model.generate(
input=audio_path,
batch_size_s=300,
)
except Exception as e:
return {
"success": False,
"error": f"识别失败: {str(e)}"
}
# 解析结果
if not result or not isinstance(result, list):
return {
"success": False,
"error": "识别结果格式错误"
}
# FunASR 返回格式处理
segments = []
full_text = ""
for seg in result:
if isinstance(seg, dict):
text = seg.get("text", "").strip()
if "timestamp" in seg and seg["timestamp"]:
# 包含时间戳
timestamp = seg["timestamp"]
if len(timestamp) >= 2:
start_time = timestamp[0][0] if timestamp[0] else 0
end_time = timestamp[-1][1] if timestamp[-1] else start_time
else:
start_time = 0
end_time = 0
else:
start_time = 0
end_time = 0
if text:
segments.append({
"id": len(segments),
"start": round(start_time, 2),
"end": round(end_time, 2),
"text": text
})
full_text += text + "\n"
# 清理临时文件
if audio_path != input_path and "_converted.wav" in audio_path:
try:
os.remove(audio_path)
except Exception:
pass
# 计算时长
duration = segments[-1]["end"] if segments else 0
return {
"success": True,
"input_file": os.path.basename(input_path),
"model": self.model_name,
"output_files": {},
"duration_seconds": round(duration, 2),
"segments": segments,
"text": full_text.strip()
}
def save_outputs(self, result: Dict, output_dir: str):
"""保存输出文件
Args:
result: 识别结果
output_dir: 输出目录
"""
if not result.get("success"):
return
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
basename = Path(result["input_file"]).stem
# 保存 JSON
json_path = output_dir / f"{basename}.json"
with open(json_path, "w", encoding="utf-8") as f:
json.dump(result, f, ensure_ascii=False, indent=2)
print(f"已保存: {json_path}")
result["output_files"]["json"] = str(json_path)
# 保存 TXT
txt_path = output_dir / f"{basename}.txt"
with open(txt_path, "w", encoding="utf-8") as f:
f.write(result.get("text", ""))
print(f"已保存: {txt_path}")
result["output_files"]["txt"] = str(txt_path)
def run_as_background(input_path: str, model: str, output_dir: str):
"""后台运行识别
Args:
input_path: 输入文件路径
model: 模型名称
output_dir: 输出目录
"""
# 创建日志文件
log_file = os.path.join(output_dir, f"funasr_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log")
# 重新执行命令(后台模式)
cmd = [
sys.executable, __file__,
"--input", input_path,
"--model", model,
"--output", output_dir
]
with open(log_file, "w") as f:
process = subprocess.Popen(
cmd,
stdout=f,
stderr=subprocess.STDOUT,
start_new_session=True
)
print(f"\n✓ 后台任务已启动 (PID: {process.pid})")
print(f"日志文件: {log_file}")
print(f"输出目录: {output_dir}")
print("\n使用以下命令查看进度:")
print(f" tail -f {log_file}")
def main():
parser = argparse.ArgumentParser(
description="KB FunASR - 本地语音识别工具",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
python funasr.py --input video.mp4
python funasr.py --input podcast.mp3 --model paraformer-zh
python funasr.py --input video.mp4 --output ./transcripts --background
"""
)
parser.add_argument("--input", "-i", required=True, help="输入音视频文件路径")
parser.add_argument("--model", "-m", default="paraformer-zh",
choices=["paraformer-zh", "paraformer", "sensevoice"],
help="模型名称 (默认: paraformer-zh)")
parser.add_argument("--output", "-o", default=None, help="输出目录 (默认: 文件所在目录)")
parser.add_argument("--background", "-b", action="store_true", help="后台异步执行")
args = parser.parse_args()
# 确定输出目录
if args.output:
output_dir = args.output
else:
output_dir = os.path.dirname(args.input) or "."
# 后台模式
if args.background:
run_as_background(args.input, args.model, output_dir)
return
# 前台模式
transcriber = FunASRTranscriber(model=args.model)
result = transcriber.transcribe(args.input)
if result.get("success"):
transcriber.save_outputs(result, output_dir)
print(f"\n✓ 处理完成!")
print(f" 时长: {result['duration_seconds']} 秒")
print(f" 分段数: {len(result['segments'])}")
else:
print(f"\n✗ 处理失败: {result.get('error')}")
sys.exit(1)
if __name__ == "__main__":
main()
+186 -131
View File
@@ -2,6 +2,11 @@
"""
KB Whisper - 本地语音识别工具
基于 Whisper.cpp 的音视频转文字
功能:
- 自动将视频/音频转换为 16kHz WAV 格式
- 支持指定语言(默认中文)
- 输出标准 JSON 格式供 KB Workflow 使用
"""
import argparse
@@ -9,6 +14,7 @@ import json
import os
import subprocess
import sys
import tempfile
from pathlib import Path
import re
@@ -22,7 +28,7 @@ def parse_arguments():
示例:
python whisper.py --input video.mp4
python whisper.py --input video.mp4 --model base
python whisper.py --input audio.mp3 --output ./output --background
python whisper.py --input video.mp4 --output ./output --background
"""
)
parser.add_argument(
@@ -32,9 +38,9 @@ def parse_arguments():
)
parser.add_argument(
'--model',
default='tiny',
default='base',
choices=['tiny', 'base', 'small', 'medium', 'large'],
help='Whisper 模型类型 (默认: tiny)'
help='Whisper 模型类型 (默认: base)'
)
parser.add_argument(
'--output',
@@ -47,7 +53,8 @@ def parse_arguments():
)
parser.add_argument(
'--language',
help='指定语言 (默认: 自动检测),例如: en, zh, ja'
default='zh',
help='指定语言 (默认: zh),例如: en, zh, ja'
)
return parser.parse_args()
@@ -92,37 +99,96 @@ def check_model(whisper_path, model):
return model_path
def run_whisper_cpp(whisper_path, input_file, model, output_dir, language=None):
"""运行 whisper.cpp 命令行工具"""
def convert_to_wav(input_path, output_dir):
"""将音视频转换为 16kHz WAV 格式
Whisper 需要 WAV 格式的输入文件。
"""
input_path = os.path.abspath(input_path)
# 如果已经是 WAV 且采样率正确,直接返回
if input_path.lower().endswith('.wav'):
try:
result = subprocess.run(
['ffprobe', '-v', 'error', '-show_entries',
'stream=sample_rate', '-of', 'default=noprint_wrappers=1:nokey=1',
input_path],
capture_output=True, text=True
)
if result.returncode == 0 and int(result.stdout.strip()) == 16000:
print(f"输入文件已是 16kHz WAV 格式,直接使用")
return input_path
except Exception:
pass
# 转换
output_path = os.path.join(output_dir, f"{Path(input_path).stem}_audio.wav")
print(f"转换音频格式: {input_path} -> {output_path}")
try:
subprocess.run([
'ffmpeg', '-y', '-i', input_path,
'-acodec', 'pcm_s16le',
'-ar', '16000',
'-ac', '1',
'-loglevel', 'error',
output_path
], check=True, capture_output=True)
print("音频转换完成")
return output_path
except subprocess.CalledProcessError as e:
print(f"音频转换失败: {e}")
raise RuntimeError(f"无法转换音频格式: {e}")
except FileNotFoundError:
raise RuntimeError("未找到 ffmpeg,请安装: sudo apt-get install ffmpeg")
def run_whisper(whisper_path, audio_path, model, output_dir, language='zh'):
"""运行 Whisper 语音识别
Args:
whisper_path: whisper.cpp 目录
audio_path: WAV 音频文件路径
model: 模型名称
output_dir: 输出目录
language: 语言代码
"""
# 输出文件名(使用原始输入文件名,不是 audio.wav)
# 从 output_dir 和 audio_path 推断原始文件名
original_stem = Path(audio_path).stem.replace('_audio', '')
output_basename = original_stem
# 构建命令
cmd = [
os.path.join(whisper_path, 'main'),
os.path.join(whisper_path, 'whisper-cli'),
'-m', os.path.join(whisper_path, 'models', f'ggml-{model}.bin'),
'-f', input_file,
'-of', os.path.join(output_dir, Path(input_file).stem),
'-otxt', # 输出 txt
'-osrt', # 输出 srt
'-oj', # 输出 json
'-f', audio_path,
'-of', os.path.join(output_dir, output_basename),
'-l', language,
'--output-txt',
'--output-srt',
'--output-json',
]
# 添加语言参数(如果指定)
if language:
cmd.extend(['-l', language])
print(f"执行命令: {' '.join(cmd)}")
# 运行命令
# 运行
result = subprocess.run(
cmd,
capture_output=True,
text=True,
cwd=whisper_path
cwd=whisper_path,
timeout=3600 # 1小时超时
)
if result.returncode != 0:
# 检查是否有警告但仍成功
if "error" in result.stderr.lower() and not os.path.exists(
os.path.join(output_dir, f"{output_basename}.txt")
):
raise RuntimeError(
f"Whisper.cpp 执行失败:\n"
f"Whisper 执行失败:\n"
f"标准输出: {result.stdout}\n"
f"错误输出: {result.stderr}"
)
@@ -130,81 +196,87 @@ def run_whisper_cpp(whisper_path, input_file, model, output_dir, language=None):
return result
def parse_whisper_json(json_file):
"""解析 whisper.cpp 输出的 JSON 文件"""
try:
with open(json_file, 'r', encoding='utf-8') as f:
data = json.load(f)
return data
except json.JSONDecodeError:
# whisper.cpp 的 JSON 格式可能不同,尝试解析标准格式
return None
def parse_srt_for_duration(srt_file):
"""从 SRT 文件解析时长"""
if not os.path.exists(srt_file):
return 0
with open(srt_file, 'r', encoding='utf-8') as f:
content = f.read()
# 找到最后一个时间戳
pattern = r'(\d{2}:\d{2}:\d{2}),\d{3} --> \d{2}:\d{2}:\d{2},\d{3}'
matches = re.findall(pattern, content)
if not matches:
return 0
# 解析最后一个时间戳
last_time = matches[-1]
h, m, s = last_time.split(':')
return int(h) * 3600 + int(m) * 60 + int(s)
def create_standard_json(input_file, output_dir, srt_file, txt_file, model):
"""创建标准格式的 JSON 输出"""
basename = Path(input_file).stem
def create_workflow_json(input_file, output_dir, model, language, duration):
"""创建标准格式的 JSON 输出,供 KB Workflow 使用"""
input_path = Path(input_file)
basename = input_path.stem
# 解析 SRT 文件获取时间戳
txt_file = os.path.join(output_dir, f"{basename}.txt")
srt_file = os.path.join(output_dir, f"{basename}.srt")
json_file = os.path.join(output_dir, f"{basename}.json")
# 读取文本内容
text_content = ""
if os.path.exists(txt_file):
with open(txt_file, 'r', encoding='utf-8') as f:
text_content = f.read().strip()
# 解析 SRT 获取分段
segments = []
if os.path.exists(srt_file):
with open(srt_file, 'r', encoding='utf-8') as f:
content = f.read()
# 解析 SRT 格式
# SRT 格式: 序号 -> 时间 -> 文本
pattern = r'(\d+)\n(\d{2}:\d{2}:\d{2},\d{3}) --> (\d{2}:\d{2}:\d{2},\d{3})\n(.+?)(?=\n\n|\Z)'
matches = re.findall(pattern, content, re.DOTALL)
for idx, (num, start, end, text) in enumerate(matches):
# 转换时间戳
def parse_time(t):
h, m, s_ms = t.split(':')
s, ms = s_ms.split(',')
return int(h) * 3600 + int(m) * 60 + int(s) + int(ms) / 1000
return float(h) * 3600 + float(m) * 60 + float(s) + float(ms) / 1000
for idx, (num, start, end, text) in enumerate(matches):
segments.append({
"id": idx,
"start": parse_time(start),
"end": parse_time(end),
"start": round(parse_time(start), 2),
"end": round(parse_time(end), 2),
"text": text.strip()
})
# 读取纯文本
txt_content = ""
if os.path.exists(txt_file):
with open(txt_file, 'r', encoding='utf-8') as f:
txt_content = f.read().strip()
# 获取音频时长(使用 ffprobe)
duration = 0
try:
result = subprocess.run(
['ffprobe', '-v', 'error', '-show_entries', 'format=duration',
'-of', 'default=noprint_wrappers=1:nokey=1', input_file],
capture_output=True,
text=True
)
if result.returncode == 0:
duration = float(result.stdout.strip())
except FileNotFoundError:
pass
# 构建标准 JSON
json_data = {
# 构建 JSON
workflow_json = {
"success": True,
"input_file": os.path.basename(input_file),
"input_path": input_file,
"tool": "whisper",
"model": model,
"language": language,
"input_file": str(input_path),
"output_files": {
"json": f"{basename}.json",
"txt": f"{basename}.txt",
"srt": f"{basename}.srt",
"txt": f"{basename}.txt"
"json": f"{basename}.json"
},
"duration_seconds": duration,
"segments": segments
"segments": segments,
"text": text_content
}
return json_data
# 写入 JSON
with open(json_file, 'w', encoding='utf-8') as f:
json.dump(workflow_json, f, ensure_ascii=False, indent=2)
return workflow_json
def main():
@@ -228,24 +300,22 @@ def main():
# 获取 whisper.cpp 路径
try:
whisper_path = get_whisper_cpp_path()
print(f"Whisper.cpp 路径: {whisper_path}")
print(f"✓ Whisper.cpp 路径: {whisper_path}")
except FileNotFoundError as e:
print(f"错误: {e}", file=sys.stderr)
print(f"✗ 错误: {e}", file=sys.stderr)
sys.exit(1)
# 检查模型
try:
check_model(whisper_path, args.model)
print(f"使用模型: {args.model}")
print(f"✓ 使用模型: {args.model}")
except FileNotFoundError as e:
print(f"错误: {e}", file=sys.stderr)
print(f"✗ 错误: {e}", file=sys.stderr)
sys.exit(1)
# 后台执行
if args.background:
print("启动后台处理...")
# 使用 nohup 在后台运行
log_file = os.path.join(output_dir, f"{Path(input_file).stem}_whisper.log")
with open(log_file, 'w') as log:
@@ -253,91 +323,76 @@ def main():
[sys.executable, __file__,
'--input', input_file,
'--model', args.model,
'--output', output_dir] +
(['--language', args.language] if args.language else []),
'--output', output_dir,
'--language', args.language],
stdout=log,
stderr=log,
start_new_session=True
)
print(f"后台任务已启动,日志文件: {log_file}")
print(f"输出文件将生成到: {output_dir}")
print(f"✓ 后台任务已启动")
print(f" 日志: {log_file}")
print(f" 输出: {output_dir}")
# 返回任务信息
result = {
"success": True,
"background": True,
"status": "running",
"log_file": log_file,
"output_dir": output_dir,
"message": "后台处理中,请稍候检查输出文件"
"output_dir": output_dir
}
print(json.dumps(result, ensure_ascii=False, indent=2))
return
# 前台执行
print(f"处理文件: {input_file}")
print(f"输出目录: {output_dir}")
print(f"\n📁 处理文件: {input_file}")
print(f"📁 输出目录: {output_dir}")
try:
# 运行 whisper.cpp
result = run_whisper_cpp(
whisper_path,
input_file,
args.model,
output_dir,
args.language
)
# 1. 转换为 WAV
print(f"\n🔄 步骤1: 转换音频格式...")
audio_path = convert_to_wav(input_file, output_dir)
# 输出文件路径
# 2. 运行 Whisper
print(f"\n🎙️ 步骤2: 语音识别 (语言: {args.language})...")
run_whisper(whisper_path, audio_path, args.model, output_dir, args.language)
# 3. 清理临时 WAV 文件
if audio_path != input_file and audio_path.endswith('_audio.wav'):
try:
os.remove(audio_path)
print("✓ 清理临时文件")
except Exception:
pass
# 4. 获取时长并创建 JSON
print(f"\n📝 步骤3: 生成输出文件...")
basename = Path(input_file).stem
json_file = os.path.join(output_dir, f"{basename}.json")
srt_file = os.path.join(output_dir, f"{basename}.srt")
txt_file = os.path.join(output_dir, f"{basename}.txt")
duration = parse_srt_for_duration(srt_file)
# 检查输出文件
output_files = {}
if os.path.exists(srt_file):
output_files['srt'] = srt_file
if os.path.exists(txt_file):
output_files['txt'] = txt_file
# 创建标准 JSON 格式
standard_json = create_standard_json(
input_file,
output_dir,
srt_file,
txt_file,
args.model
workflow_json = create_workflow_json(
input_file, output_dir, args.model, args.language, duration
)
# 写入标准 JSON
with open(json_file, 'w', encoding='utf-8') as f:
json.dump(standard_json, f, ensure_ascii=False, indent=2)
output_files['json'] = json_file
# 输出结果
result = {
"success": True,
"input_file": input_file,
"model": args.model,
"output_files": {
k: os.path.basename(v) for k, v in output_files.items()
},
"output_dir": output_dir,
"total_segments": len(standard_json.get('segments', [])),
"duration": standard_json.get('duration_seconds', 0)
}
print(f"\n✅ 处理完成!")
print(f" 时长: {duration} 秒")
print(f" 分段数: {len(workflow_json['segments'])}")
print(f"\n📄 输出文件:")
for fmt in ['txt', 'srt', 'json']:
fpath = os.path.join(output_dir, f"{basename}.{fmt}")
if os.path.exists(fpath):
size = os.path.getsize(fpath)
print(f" • {fmt.upper()}: {fpath} ({size:,} bytes)")
print("\n✓ 处理完成!")
print(f"\n输出文件:")
for fmt, fname in output_files.items():
print(f" - {fname.upper()}: {fname}")
print(f"\n完整结果:")
print(json.dumps(result, ensure_ascii=False, indent=2))
print(f"\n📋 Workflow JSON:")
print(json.dumps({
"success": workflow_json["success"],
"tool": "whisper",
"text_length": len(workflow_json["text"]),
"segments_count": len(workflow_json["segments"])
}, ensure_ascii=False, indent=2))
except Exception as e:
print(f"\n✗ 错误: {e}", file=sys.stderr)
+11 -12
View File
@@ -108,28 +108,28 @@ python3 /path/to/bilibili_content.py --id "[ID]" --type subtitle --output [subti
3. 解析输出 JSON,提取 video_filename 和 subtitle_filename(如有)
### 步骤 5: OCR 处理(禁止跳过)
### 步骤 5: 语音识别转文字(禁止跳过)
> ⚠️ **重要**:此步骤为耗时步骤,视频按帧截取和 OCR 识别需要较长时间。
> - **严禁跳过**:即使等待时间较长,也必须等待 OCR 完成
> - **定时汇报**:每2-3分钟向用户汇报进度(处理帧数/预计剩余时间)
> ⚠️ **重要**:此步骤为耗时步骤,使用 Whisper 进行语音识别。
> - **严禁跳过**:即使等待时间较长,也必须等待识别完成
> - **定时汇报**:每2-3分钟向用户汇报进度
> - **超时时间**:30分钟
> - **耐心等待**:不要中途启动其他步骤,必须串行执行
如果步骤4没有获取到字幕,需要从视频截帧 OCR:
如果步骤4没有获取到字幕,需要使用 Whisper 进行语音识别:
1. 获取目录路径:
```bash
python3 kb_workflow.py --content
```
2. 启动子代理调用 kb-ocr-processor(传入完整路径):
2. 启动子代理调用 kb-whisper(传入完整路径):
```bash
# 必须传入 --output 完整路径
python3 /path/to/ocr_processor.py --video [video_path] --output [content_dir]/ocr.json --merge
python3 /path/to/whisper.py --input [video_path] --model base --output [content_dir]
```
3. 等待 OCR 完成(定时汇报进度)
3. 等待 Whisper 完成(定时汇报进度)
4. 解析输出 JSON,生成 content 文件供步骤6使用
@@ -173,7 +173,7 @@ python3 kb_workflow.py --archive-file "[分析报告文件路径]"
| 3 | kb-bilibili-metadata | `--id "BVxxx" --type video` | `{p}_{id}.meta.json` |
| 3 | kb-zhihu-metadata | `--id "xxx" --type question` | `{p}_{id}.meta.json` |
| 4 | kb-bilibili-content | `--id "BVxxx" --type video` | `{p}_{id}.{ext}` |
| 5 | kb-ocr-processor | `--video xxx.flv --output ocr.json` | `ocr.json`, `ocr.txt` |
| 5 | kb-whisper | `--input xxx.mp4 --model base` | `{p}_{id}.json`, `{p}_{id}.txt` |
| 6 | kb-deep-analyzer | `--metadata x.meta.json --content x.json --output x.md` | `{p}_{id}.md` |
## 目录结构
@@ -189,9 +189,8 @@ python3 kb_workflow.py --archive-file "[分析报告文件路径]"
├── content/
│ ├── downloads/
│ │ └── {platform}_{id}.mp4
│ ├── subtitles/
│ │ └── {platform}_{id}.srt
│ └── {platform}_{id}.txt
│ ├── {platform}_{id}.txt # Whisper 识别的文本
│ └── {platform}_{id}.json # Whisper 完整结果
└── analyzed/
└── {platform}_{id}.md
```
-5
View File
@@ -67,7 +67,6 @@ def ensure_directories(base_path: Path = None) -> Dict[str, Path]:
"metadata": base_path / "metadata",
"content": base_path / "content",
"content_downloads": base_path / "content" / "downloads",
"content_subtitles": base_path / "content" / "subtitles",
"analyzed": base_path / "analyzed",
"archive": base_path / "archive",
"archive_subscriptions": base_path / "archive" / "subscriptions",
@@ -250,7 +249,6 @@ def main():
parser.add_argument('--metadata', action='store_true', help='获取元数据目录')
parser.add_argument('--content', action='store_true', help='获取内容目录')
parser.add_argument('--content_downloads', action='store_true', help='获取主内容下载目录')
parser.add_argument('--content_subtitles', action='store_true', help='获取字幕目录')
parser.add_argument('--analyzed', action='store_true', help='获取深度分析目录')
parser.add_argument('--archive_dir', action='store_true', help='获取归档目录')
parser.add_argument('--archive_subscriptions', action='store_true', help='获取订阅信息目录')
@@ -280,9 +278,6 @@ def main():
elif args.content_downloads:
print(json.dumps({"path": str(dirs["content_downloads"])}, ensure_ascii=False, indent=2))
elif args.content_subtitles:
print(json.dumps({"path": str(dirs["content_subtitles"])}, ensure_ascii=False, indent=2))
elif args.analyzed:
print(json.dumps({"path": str(dirs["analyzed"])}, ensure_ascii=False, indent=2))