feat(kb-whisper): 新增本地语音识别技能,基于 Whisper.cpp

This commit is contained in:
2026-02-13 01:20:27 +08:00
parent 2adc3c1e8d
commit 9102a973cd
6 changed files with 993 additions and 0 deletions
+32
View File
@@ -0,0 +1,32 @@
# 2026-02-13
## 项目状态
### GPG 与 Git 推送
- ~~GPG 签名~~ → **改用 SSH**
- **SSH 推送**: ✅ 成功!
- 最新提交: `2adc3c1 docs: 更新每日记录`
### Whisper 替代 OCR 方案
- **决策**: 新增 `kb-whisper` 技能,异步执行
- **原因**: 语音处理耗时,不阻塞主代理,支持并行讨论
- **输出格式**: JSON(时间戳) + SRT + TXT
- **状态**: ✅ 安装完成,技能创建完成
### 待清理文件
- `bilibili_BV1Z9nXzUEhv_720p_frames/`
- `whispercpp.zip`
## 今日完成
| 任务 | 状态 |
|------|------|
| whisper.cpp 安装 | ✅ 完成 |
| kb-whisper 技能创建 | ✅ 完成 |
| SSH 推送 | ✅ 成功 |
## 待办
- [ ] 清理临时文件
- [ ] 下载中文模型 (需代理)
- [ ] 测试 Whisper 转写效果
- [ ] kb-workflow 集成 kb-whisper
+5
View File
@@ -0,0 +1,5 @@
# KB Whisper 环境变量配置示例
# 复制此文件为 .env 并填写实际值
# Whisper.cpp 安装路径
WHISPER_CPP_PATH=~/whisper.cpp
+211
View File
@@ -0,0 +1,211 @@
# KB Whisper - 本地语音识别
基于 Whisper.cpp 的本地语音识别工具,支持将音视频文件转换为文本、字幕和 JSON 格式。
## 功能特性
- 🎯 **本地执行** - 完全离线运行,保护隐私
- 🚀 **多模型支持** - 从 tiny 到 large,根据需求选择速度/精度
- 📝 **多格式输出** - JSON、SRT、TXT 三种格式
- ⚡ **异步执行** - 支持后台运行,不阻塞主代理
- 🎬 **广泛兼容** - 支持常见音视频格式(MP3、MP4、WAV 等)
## 安装前提
### 1. 安装 whisper.cpp
```bash
# 克隆仓库
git clone https://github.com/ggerganov/whisper.cpp.git ~/whisper.cpp
# 编译
cd ~/whisper.cpp
make
# 下载模型(至少需要一个模型)
./models/download-ggml-model.sh tiny
./models/download-ggml-model.sh base # 可选
```
### 2. 配置环境变量
在 `~/.clawdbot/clawdbot.json` 中添加:
```json5
{
skills: {
entries: {
"kb-whisper": {
env: {
WHISPER_CPP_PATH: "~/whisper.cpp"
}
}
}
}
}
```
或通过命令行配置:
```bash
clawdbot skills config kb-whisper WHISPER_CPP_PATH "~/whisper.cpp"
```
## 使用方法
### 基本用法
```bash
# 使用默认 tiny 模型处理
python whisper.py --input video.mp4
# 使用 base 模型(推荐,平衡速度和精度)
python whisper.py --input video.mp4 --model base
# 后台异步执行
python whisper.py --input video.mp4 --background
```
### 指定输出目录
```bash
python whisper.py --input video.mp4 --output ./transcripts
```
### 处理音频文件
```bash
python whisper.py --input podcast.mp3 --model base
```
## 输出文件
对于输入 `video.mp4`,将生成以下文件:
```
output/
├── video.json # 详细结果(含时间戳)
├── video.srt # 字幕格式
└── video.txt # 纯文本
```
### JSON 格式
```json
{
"success": true,
"input_file": "video.mp4",
"model": "base",
"output_files": {
"json": "video.json",
"srt": "video.srt",
"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": "这是第二段字幕"
}
]
}
```
## 模型对比
| 模型 | 速度 | 精度 | 适用场景 |
|------|------|------|----------|
| tiny | ⚡⚡⚡ | ⭐ | 快速预览、实时转录 |
| base | ⚡⚡ | ⭐⭐⭐ | 日常使用(推荐) |
| small | ⚡ | ⭐⭐⭐⭐ | 高质量需求 |
| medium | ⭐ | ⭐⭐⭐⭐⭐ | 专业用途 |
| large | 🐢 | ⭐⭐⭐⭐⭐ | 最高精度要求 |
## 完整示例
### 示例 1: 转录播客
```bash
python whisper.py --input podcast.mp3 --model base --background
```
### 示例 2: 生成视频字幕
```bash
python whisper.py --input tutorial.mp4 --model small
```
### 示例 3: 批量处理(配合脚本)
```bash
#!/bin/bash
for video in videos/*.mp4; do
python whisper.py --input "$video" --model base
done
```
## 在 Workflow 中使用
在知识库处理流程中,如果视频没有字幕:
```bash
# 步骤4: 下载视频后,如果没有字幕
# 步骤5: 使用 Whisper 生成字幕
python whisper.py --input ${video_path} --model base --background
# 等待 Whisper 完成,然后继续后续步骤
```
## 技术细节
- **引擎**: Whisper.cpp(C++ 实现,性能优异)
- **音频采样率**: 自动转换为 16kHz
- **语言**: 自动检测(支持 99 种语言)
- **GPU 支持**: whisper.cpp 支持 CUDA 加速(需编译时启用)
## 故障排查
### 问题: 找不到 whisper.cpp
```bash
# 检查环境变量
echo $WHISPER_CPP_PATH
# 手动测试
~/whisper.cpp/main --help
```
### 问题: 模型未下载
```bash
cd ~/whisper.cpp
./models/download-ggml-model.sh tiny
```
### 问题: 音频格式不支持
```bash
# 转换音频格式
ffmpeg -i input.avi -acodec libmp3lame output.mp3
python whisper.py --input output.mp3
```
## 性能参考
在 CPU 环境下(Intel i7):
- **tiny 模型**: ~1x 实时速度(1分钟音频 ~1分钟处理)
- **base 模型**: ~0.5x 实时速度(1分钟音频 ~2分钟处理)
- **small 模型**: ~0.25x 实时速度(1分钟音频 ~4分钟处理)
## 许可证
本技能使用 MIT 许可证,Whisper.cpp 使用 MIT 许可证。
+124
View File
@@ -0,0 +1,124 @@
---
name: kb-whisper
description: 本地语音识别,支持音视频转文字(基于 Whisper.cpp)
---
# KB Whisper Skill
**kb-whisper** - 本地语音识别,使用 Whisper.cpp 将音视频文件转换为文本、字幕和 JSON 格式输出。
## 什么时候使用
当用户需要:
- 将视频/音频文件转换为文字
- 生成视频字幕(SRT 格式)
- 获取带时间戳的详细识别结果
## 环境变量
在使用之前,必须检查环境变量是否已经设置。
| 变量名 | 用途 | 示例 |
|--------|------|------|
| `WHISPER_CPP_PATH` | whisper.cpp 安装路径 | `~/whisper.cpp` |
## 使用方法
```bash
# 基本用法
python whisper.py --input video.mp4
# 指定模型(默认 tiny)
python whisper.py --input video.mp4 --model base
# 指定输出目录
python whisper.py --input video.mp4 --output ./output
# 异步执行(推荐)
python whisper.py --input video.mp4 --background
```
## 命令参数
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--input` | 输入音视频文件路径 | 必需 |
| `--model` | Whisper 模型类型 | `tiny` |
| `--output` | 输出目录 | 文件所在目录 |
| `--background` | 后台异步执行 | False |
## 模型选项
- `tiny` - 最小模型,速度快,精度较低(推荐快速识别)
- `base` - 基础模型,平衡速度和精度(推荐)
- `small` - 小型模型,精度更高,速度较慢
- `medium` - 中型模型,高精度
- `large` - 大型模型,最高精度,速度慢
## 输出文件
对于输入 `video.mp4`,将生成:
- `video.json` - 详细结果(包含时间戳、分段信息)
- `video.srt` - 字幕格式文件
- `video.txt` - 纯文本内容
## 输出格式示例
### JSON 格式
```json
{
"success": true,
"input_file": "video.mp4",
"model": "tiny",
"output_files": {
"json": "video.json",
"srt": "video.srt",
"txt": "video.txt"
},
"duration_seconds": 120.5,
"segments": [
{
"id": 0,
"start": 0.0,
"end": 3.2,
"text": "这是第一段字幕"
}
]
}
```
## Workflow 中的使用
```bash
# 步骤4之后:如果已有音频但无字幕,可以使用 whisper
python whisper.py --input ${video_path} --model base --background
```
## 安装
```bash
# 1. 克隆 whisper.cpp
git clone https://github.com/ggerganov/whisper.cpp.git ~/whisper.cpp
# 2. 编译
cd ~/whisper.cpp
make
# 3. 下载模型(tiny)
./models/download-ggml-model.sh tiny
# 4. 下载更多模型(可选)
# ./models/download-ggml-model.sh base
# ./models/download-ggml-model.sh small
# 5. 复制技能到 skills 目录
cp -r kb-whisper/ ~/skills/
```
## 注意事项
- **异步执行**: 使用 `--background` 参数时,命令会在后台运行,不阻塞主代理
- **模型选择**: tiny 模型适合快速识别,base 模型适合一般用途,large 模型适合高精度需求
- **文件格式**: 支持 MP3、MP4、WAV、FLAC、OGG、M4A 等常见格式
+273
View File
@@ -0,0 +1,273 @@
# KB Whisper 使用示例
## 快速开始
### 1. 基础使用
```bash
# 使用默认 tiny 模型转录视频
python whisper.py --input video.mp4
# 使用 base 模型(推荐)
python whisper.py --input video.mp4 --model base
```
### 2. 异步执行
```bash
# 后台处理,不阻塞主代理
python whisper.py --input video.mp4 --model base --background
```
### 3. 指定输出目录
```bash
python whisper.py --input video.mp4 --output ./transcripts
```
### 4. 指定语言
```bash
# 指定中文
python whisper.py --input video.mp4 --language zh
# 指定英文
python whisper.py --input video.mp4 --language en
# 指定日语
python whisper.py --input video.mp4 --language ja
```
## 实际应用场景
### 场景 1: 转录播客
```bash
python whisper.py --input podcast.mp3 --model base --background
```
**输出**:
- `podcast.json` - 带时间戳的详细结果
- `podcast.srt` - 字幕格式
- `podcast.txt` - 纯文本
### 场景 2: 生成视频字幕
```bash
python whisper.py --input tutorial.mp4 --model small
```
**用途**:
- 为教程视频生成字幕
- 导出 SRT 文件到视频编辑软件
- 生成多语言字幕版本
### 场景 3: 会议录音转文字
```bash
python whisper.py --input meeting.mp3 --model base --output ./meetings
```
**输出示例**:
```
meetings/
├── meeting.json # 完整 JSON(含时间戳)
├── meeting.srt # 字幕格式
└── meeting.txt # 纯文本
```
### 场景 4: 批量处理
创建脚本 `batch_transcribe.sh`:
```bash
#!/bin/bash
OUTPUT_DIR="./transcripts"
mkdir -p "$OUTPUT_DIR"
for video in videos/*.mp4; do
echo "处理: $video"
python whisper.py --input "$video" --output "$OUTPUT_DIR" --model base
done
echo "批量处理完成!"
```
```bash
chmod +x batch_transcribe.sh
./batch_transcribe.sh
```
## 在 Workflow 中使用
### 知识库处理流程
在 `kb-workflow` 技能中,如果视频没有字幕:
```bash
# 步骤 4: 下载视频后
# 检查是否有字幕文件
if [ ! -f "${subtitle_file}" ]; then
echo "无字幕文件,使用 Whisper 生成..."
# 使用 Whisper 生成字幕(后台执行)
python /path/to/whisper.py \
--input "${video_path}" \
--model base \
--output "${content_dir}" \
--background
# 等待 Whisper 完成
sleep 5 # 等待后台任务启动
while ! [ -f "${content_dir}/$(basename ${video_path} .mp4).srt" ]; do
sleep 2
done
echo "Whisper 处理完成"
fi
```
## 输出文件使用
### JSON 格式
```python
import json
# 读取 JSON 结果
with open('video.json', 'r', encoding='utf-8') as f:
data = json.load(f)
# 访问分段
for segment in data['segments']:
print(f"[{segment['start']:.2f}-{segment['end']:.2f}] {segment['text']}")
```
### SRT 格式
```bash
# 直接在视频播放器中使用
mpv video.mp4 --sub-file=video.srt
# 或转换字幕格式
ffmpeg -i video.srt video.ass
```
### TXT 格式
```bash
# 搜索关键词
grep "重要信息" video.txt
# 统计字数
wc -m video.txt
```
## 性能优化建议
### 1. 根据内容选择模型
```bash
# 快速预览(短视频、测试)
python whisper.py --input video.mp4 --model tiny
# 日常使用(播客、会议)
python whisper.py --input video.mp4 --model base
# 高质量需求(正式内容、多语言)
python whisper.py --input video.mp4 --model small
```
### 2. 使用后台执行
```bash
# 处理长文件时使用后台模式
python whisper.py --input long_video.mp4 --model base --background
```
### 3. 指定语言提升准确度
```bash
# 如果知道内容语言,指定后准确度更高
python whisper.py --input video.mp4 --language zh
```
## 故障排查
### 问题 1: whisper.cpp 未找到
```bash
# 检查环境变量
echo $WHISPER_CPP_PATH
# 或配置
clawdbot skills config kb-whisper WHISPER_CPP_PATH "~/whisper.cpp"
```
### 问题 2: 模型未下载
```bash
cd ~/whisper.cpp
./models/download-ggml-model.sh base
```
### 问题 3: 音频格式不支持
```bash
# 转换格式
ffmpeg -i input.avi -acodec libmp3lame output.mp3
python whisper.py --input output.mp3
```
### 问题 4: 后台任务未完成
```bash
# 查看日志
cat video_whisper.log
# 检查进程
ps aux | grep whisper
```
## 集成到其他工具
### 集成到 Python 脚本
```python
import subprocess
import json
def transcribe_audio(audio_path, model='base'):
"""调用 whisper.py 转录音频"""
cmd = [
'python3', '/path/to/whisper.py',
'--input', audio_path,
'--model', model
]
result = subprocess.run(cmd, capture_output=True, text=True)
return json.loads(result.stdout)
# 使用
result = transcribe_audio('podcast.mp3', 'base')
print(f"识别到 {result['total_segments']} 个片段")
```
### 集成到 Shell 脚本
```bash
#!/bin/bash
transcribe() {
local file=$1
local model=${2:-base}
echo "转录: $file"
python3 whisper.py --input "$file" --model "$model" --background
}
# 批量处理
transcribe "video1.mp4"
transcribe "video2.mp4" "tiny"
```
+348
View File
@@ -0,0 +1,348 @@
#!/usr/bin/env python3
"""
KB Whisper - 本地语音识别工具
基于 Whisper.cpp 的音视频转文字
"""
import argparse
import json
import os
import subprocess
import sys
from pathlib import Path
import re
def parse_arguments():
"""解析命令行参数"""
parser = argparse.ArgumentParser(
description='KB Whisper - 本地语音识别工具',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
python whisper.py --input video.mp4
python whisper.py --input video.mp4 --model base
python whisper.py --input audio.mp3 --output ./output --background
"""
)
parser.add_argument(
'--input',
required=True,
help='输入音视频文件路径'
)
parser.add_argument(
'--model',
default='tiny',
choices=['tiny', 'base', 'small', 'medium', 'large'],
help='Whisper 模型类型 (默认: tiny)'
)
parser.add_argument(
'--output',
help='输出目录 (默认: 输入文件所在目录)'
)
parser.add_argument(
'--background',
action='store_true',
help='后台异步执行'
)
parser.add_argument(
'--language',
help='指定语言 (默认: 自动检测),例如: en, zh, ja'
)
return parser.parse_args()
def get_whisper_cpp_path():
"""获取 whisper.cpp 路径"""
# 优先从环境变量获取
path = os.environ.get('WHISPER_CPP_PATH')
if path:
path = os.path.expanduser(path)
if os.path.exists(path):
return path
# 尝试常见位置
common_paths = [
os.path.expanduser('~/whisper.cpp'),
'/usr/local/bin/whisper',
'/opt/whisper.cpp',
]
for cp in common_paths:
if os.path.exists(cp):
return cp
raise FileNotFoundError(
"未找到 whisper.cpp。请设置环境变量 WHISPER_CPP_PATH "
"或安装到 ~/whisper.cpp\n"
"安装方法: git clone https://github.com/ggerganov/whisper.cpp.git ~/whisper.cpp && cd ~/whisper.cpp && make"
)
def check_model(whisper_path, model):
"""检查模型是否存在"""
model_path = os.path.join(whisper_path, 'models', f'ggml-{model}.bin')
if not os.path.exists(model_path):
raise FileNotFoundError(
f"模型文件不存在: {model_path}\n"
f"请运行以下命令下载模型:\n"
f"cd {whisper_path} && ./models/download-ggml-model.sh {model}"
)
return model_path
def run_whisper_cpp(whisper_path, input_file, model, output_dir, language=None):
"""运行 whisper.cpp 命令行工具"""
# 构建命令
cmd = [
os.path.join(whisper_path, 'main'),
'-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
]
# 添加语言参数(如果指定)
if language:
cmd.extend(['-l', language])
print(f"执行命令: {' '.join(cmd)}")
# 运行命令
result = subprocess.run(
cmd,
capture_output=True,
text=True,
cwd=whisper_path
)
if result.returncode != 0:
raise RuntimeError(
f"Whisper.cpp 执行失败:\n"
f"标准输出: {result.stdout}\n"
f"错误输出: {result.stderr}"
)
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 create_standard_json(input_file, output_dir, srt_file, txt_file, model):
"""创建标准格式的 JSON 输出"""
basename = Path(input_file).stem
# 解析 SRT 文件获取时间戳
segments = []
if os.path.exists(srt_file):
with open(srt_file, 'r', encoding='utf-8') as f:
content = f.read()
# 解析 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
segments.append({
"id": idx,
"start": parse_time(start),
"end": parse_time(end),
"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 = {
"success": True,
"input_file": os.path.basename(input_file),
"input_path": input_file,
"model": model,
"output_files": {
"json": f"{basename}.json",
"srt": f"{basename}.srt",
"txt": f"{basename}.txt"
},
"duration_seconds": duration,
"segments": segments
}
return json_data
def main():
"""主函数"""
args = parse_arguments()
# 检查输入文件
input_file = os.path.abspath(args.input)
if not os.path.exists(input_file):
print(f"错误: 输入文件不存在: {input_file}", file=sys.stderr)
sys.exit(1)
# 获取输出目录
if args.output:
output_dir = os.path.abspath(args.output)
else:
output_dir = os.path.dirname(input_file)
os.makedirs(output_dir, exist_ok=True)
# 获取 whisper.cpp 路径
try:
whisper_path = get_whisper_cpp_path()
print(f"Whisper.cpp 路径: {whisper_path}")
except FileNotFoundError as e:
print(f"错误: {e}", file=sys.stderr)
sys.exit(1)
# 检查模型
try:
check_model(whisper_path, args.model)
print(f"使用模型: {args.model}")
except FileNotFoundError as e:
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:
subprocess.Popen(
[sys.executable, __file__,
'--input', input_file,
'--model', args.model,
'--output', output_dir] +
(['--language', args.language] if args.language else []),
stdout=log,
stderr=log,
start_new_session=True
)
print(f"后台任务已启动,日志文件: {log_file}")
print(f"输出文件将生成到: {output_dir}")
# 返回任务信息
result = {
"success": True,
"background": True,
"status": "running",
"log_file": log_file,
"output_dir": output_dir,
"message": "后台处理中,请稍候检查输出文件"
}
print(json.dumps(result, ensure_ascii=False, indent=2))
return
# 前台执行
print(f"处理文件: {input_file}")
print(f"输出目录: {output_dir}")
try:
# 运行 whisper.cpp
result = run_whisper_cpp(
whisper_path,
input_file,
args.model,
output_dir,
args.language
)
# 输出文件路径
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")
# 检查输出文件
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
)
# 写入标准 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("\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))
except Exception as e:
print(f"\n✗ 错误: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()