feat(kb-workflow): 切换为 Whisper 语音识别
- 将步骤5从 OCR 改为 Whisper 语音识别
- 更新 kb-whisper 支持自动音频转换
- 默认使用中文语言 (zh) 和 base 模型
- 更新输出格式为 {platform}_{id}.txt/.srt/.json
- 添加 FunASR 作为备用方案
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
# FunASR 模型缓存目录
|
||||||
|
FUNASR_CACHE_DIR=~/.cache/modelscope/hub
|
||||||
@@ -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 许可证。
|
||||||
@@ -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 等常见格式
|
||||||
|
- **首次运行**: 首次运行时会自动下载模型文件,需要稳定的网络连接
|
||||||
Executable
+313
@@ -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
@@ -2,6 +2,11 @@
|
|||||||
"""
|
"""
|
||||||
KB Whisper - 本地语音识别工具
|
KB Whisper - 本地语音识别工具
|
||||||
基于 Whisper.cpp 的音视频转文字
|
基于 Whisper.cpp 的音视频转文字
|
||||||
|
|
||||||
|
功能:
|
||||||
|
- 自动将视频/音频转换为 16kHz WAV 格式
|
||||||
|
- 支持指定语言(默认中文)
|
||||||
|
- 输出标准 JSON 格式供 KB Workflow 使用
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -9,6 +14,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
|
|
||||||
@@ -22,7 +28,7 @@ def parse_arguments():
|
|||||||
示例:
|
示例:
|
||||||
python whisper.py --input video.mp4
|
python whisper.py --input video.mp4
|
||||||
python whisper.py --input video.mp4 --model base
|
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(
|
parser.add_argument(
|
||||||
@@ -32,9 +38,9 @@ def parse_arguments():
|
|||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--model',
|
'--model',
|
||||||
default='tiny',
|
default='base',
|
||||||
choices=['tiny', 'base', 'small', 'medium', 'large'],
|
choices=['tiny', 'base', 'small', 'medium', 'large'],
|
||||||
help='Whisper 模型类型 (默认: tiny)'
|
help='Whisper 模型类型 (默认: base)'
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--output',
|
'--output',
|
||||||
@@ -47,7 +53,8 @@ def parse_arguments():
|
|||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--language',
|
'--language',
|
||||||
help='指定语言 (默认: 自动检测),例如: en, zh, ja'
|
default='zh',
|
||||||
|
help='指定语言 (默认: zh),例如: en, zh, ja'
|
||||||
)
|
)
|
||||||
|
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
@@ -92,37 +99,96 @@ def check_model(whisper_path, model):
|
|||||||
return model_path
|
return model_path
|
||||||
|
|
||||||
|
|
||||||
def run_whisper_cpp(whisper_path, input_file, model, output_dir, language=None):
|
def convert_to_wav(input_path, output_dir):
|
||||||
"""运行 whisper.cpp 命令行工具"""
|
"""将音视频转换为 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 = [
|
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'),
|
'-m', os.path.join(whisper_path, 'models', f'ggml-{model}.bin'),
|
||||||
'-f', input_file,
|
'-f', audio_path,
|
||||||
'-of', os.path.join(output_dir, Path(input_file).stem),
|
'-of', os.path.join(output_dir, output_basename),
|
||||||
'-otxt', # 输出 txt
|
'-l', language,
|
||||||
'-osrt', # 输出 srt
|
'--output-txt',
|
||||||
'-oj', # 输出 json
|
'--output-srt',
|
||||||
|
'--output-json',
|
||||||
]
|
]
|
||||||
|
|
||||||
# 添加语言参数(如果指定)
|
|
||||||
if language:
|
|
||||||
cmd.extend(['-l', language])
|
|
||||||
|
|
||||||
print(f"执行命令: {' '.join(cmd)}")
|
print(f"执行命令: {' '.join(cmd)}")
|
||||||
|
|
||||||
# 运行命令
|
# 运行
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
cmd,
|
cmd,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
cwd=whisper_path
|
cwd=whisper_path,
|
||||||
|
timeout=3600 # 1小时超时
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.returncode != 0:
|
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(
|
raise RuntimeError(
|
||||||
f"Whisper.cpp 执行失败:\n"
|
f"Whisper 执行失败:\n"
|
||||||
f"标准输出: {result.stdout}\n"
|
f"标准输出: {result.stdout}\n"
|
||||||
f"错误输出: {result.stderr}"
|
f"错误输出: {result.stderr}"
|
||||||
)
|
)
|
||||||
@@ -130,81 +196,87 @@ def run_whisper_cpp(whisper_path, input_file, model, output_dir, language=None):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def parse_whisper_json(json_file):
|
def parse_srt_for_duration(srt_file):
|
||||||
"""解析 whisper.cpp 输出的 JSON 文件"""
|
"""从 SRT 文件解析时长"""
|
||||||
try:
|
if not os.path.exists(srt_file):
|
||||||
with open(json_file, 'r', encoding='utf-8') as f:
|
return 0
|
||||||
data = json.load(f)
|
|
||||||
return data
|
with open(srt_file, 'r', encoding='utf-8') as f:
|
||||||
except json.JSONDecodeError:
|
content = f.read()
|
||||||
# whisper.cpp 的 JSON 格式可能不同,尝试解析标准格式
|
|
||||||
return None
|
# 找到最后一个时间戳
|
||||||
|
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):
|
def create_workflow_json(input_file, output_dir, model, language, duration):
|
||||||
"""创建标准格式的 JSON 输出"""
|
"""创建标准格式的 JSON 输出,供 KB Workflow 使用"""
|
||||||
basename = Path(input_file).stem
|
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 = []
|
segments = []
|
||||||
if os.path.exists(srt_file):
|
if os.path.exists(srt_file):
|
||||||
with open(srt_file, 'r', encoding='utf-8') as f:
|
with open(srt_file, 'r', encoding='utf-8') as f:
|
||||||
content = f.read()
|
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)'
|
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)
|
matches = re.findall(pattern, content, re.DOTALL)
|
||||||
|
|
||||||
for idx, (num, start, end, text) in enumerate(matches):
|
|
||||||
# 转换时间戳
|
|
||||||
def parse_time(t):
|
def parse_time(t):
|
||||||
h, m, s_ms = t.split(':')
|
h, m, s_ms = t.split(':')
|
||||||
s, ms = s_ms.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({
|
segments.append({
|
||||||
"id": idx,
|
"id": idx,
|
||||||
"start": parse_time(start),
|
"start": round(parse_time(start), 2),
|
||||||
"end": parse_time(end),
|
"end": round(parse_time(end), 2),
|
||||||
"text": text.strip()
|
"text": text.strip()
|
||||||
})
|
})
|
||||||
|
|
||||||
# 读取纯文本
|
# 构建 JSON
|
||||||
txt_content = ""
|
workflow_json = {
|
||||||
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,
|
"success": True,
|
||||||
"input_file": os.path.basename(input_file),
|
"tool": "whisper",
|
||||||
"input_path": input_file,
|
|
||||||
"model": model,
|
"model": model,
|
||||||
|
"language": language,
|
||||||
|
"input_file": str(input_path),
|
||||||
"output_files": {
|
"output_files": {
|
||||||
"json": f"{basename}.json",
|
"txt": f"{basename}.txt",
|
||||||
"srt": f"{basename}.srt",
|
"srt": f"{basename}.srt",
|
||||||
"txt": f"{basename}.txt"
|
"json": f"{basename}.json"
|
||||||
},
|
},
|
||||||
"duration_seconds": duration,
|
"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():
|
def main():
|
||||||
@@ -228,24 +300,22 @@ def main():
|
|||||||
# 获取 whisper.cpp 路径
|
# 获取 whisper.cpp 路径
|
||||||
try:
|
try:
|
||||||
whisper_path = get_whisper_cpp_path()
|
whisper_path = get_whisper_cpp_path()
|
||||||
print(f"Whisper.cpp 路径: {whisper_path}")
|
print(f"✓ Whisper.cpp 路径: {whisper_path}")
|
||||||
except FileNotFoundError as e:
|
except FileNotFoundError as e:
|
||||||
print(f"错误: {e}", file=sys.stderr)
|
print(f"✗ 错误: {e}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# 检查模型
|
# 检查模型
|
||||||
try:
|
try:
|
||||||
check_model(whisper_path, args.model)
|
check_model(whisper_path, args.model)
|
||||||
print(f"使用模型: {args.model}")
|
print(f"✓ 使用模型: {args.model}")
|
||||||
except FileNotFoundError as e:
|
except FileNotFoundError as e:
|
||||||
print(f"错误: {e}", file=sys.stderr)
|
print(f"✗ 错误: {e}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# 后台执行
|
# 后台执行
|
||||||
if args.background:
|
if args.background:
|
||||||
print("启动后台处理...")
|
print("启动后台处理...")
|
||||||
|
|
||||||
# 使用 nohup 在后台运行
|
|
||||||
log_file = os.path.join(output_dir, f"{Path(input_file).stem}_whisper.log")
|
log_file = os.path.join(output_dir, f"{Path(input_file).stem}_whisper.log")
|
||||||
|
|
||||||
with open(log_file, 'w') as log:
|
with open(log_file, 'w') as log:
|
||||||
@@ -253,91 +323,76 @@ def main():
|
|||||||
[sys.executable, __file__,
|
[sys.executable, __file__,
|
||||||
'--input', input_file,
|
'--input', input_file,
|
||||||
'--model', args.model,
|
'--model', args.model,
|
||||||
'--output', output_dir] +
|
'--output', output_dir,
|
||||||
(['--language', args.language] if args.language else []),
|
'--language', args.language],
|
||||||
stdout=log,
|
stdout=log,
|
||||||
stderr=log,
|
stderr=log,
|
||||||
start_new_session=True
|
start_new_session=True
|
||||||
)
|
)
|
||||||
|
|
||||||
print(f"后台任务已启动,日志文件: {log_file}")
|
print(f"✓ 后台任务已启动")
|
||||||
print(f"输出文件将生成到: {output_dir}")
|
print(f" 日志: {log_file}")
|
||||||
|
print(f" 输出: {output_dir}")
|
||||||
|
|
||||||
# 返回任务信息
|
|
||||||
result = {
|
result = {
|
||||||
"success": True,
|
"success": True,
|
||||||
"background": True,
|
"background": True,
|
||||||
"status": "running",
|
"status": "running",
|
||||||
"log_file": log_file,
|
"log_file": log_file,
|
||||||
"output_dir": output_dir,
|
"output_dir": output_dir
|
||||||
"message": "后台处理中,请稍候检查输出文件"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||||
return
|
return
|
||||||
|
|
||||||
# 前台执行
|
# 前台执行
|
||||||
print(f"处理文件: {input_file}")
|
print(f"\n📁 处理文件: {input_file}")
|
||||||
print(f"输出目录: {output_dir}")
|
print(f"📁 输出目录: {output_dir}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 运行 whisper.cpp
|
# 1. 转换为 WAV
|
||||||
result = run_whisper_cpp(
|
print(f"\n🔄 步骤1: 转换音频格式...")
|
||||||
whisper_path,
|
audio_path = convert_to_wav(input_file, output_dir)
|
||||||
input_file,
|
|
||||||
args.model,
|
|
||||||
output_dir,
|
|
||||||
args.language
|
|
||||||
)
|
|
||||||
|
|
||||||
# 输出文件路径
|
# 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
|
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")
|
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)
|
||||||
|
|
||||||
# 检查输出文件
|
workflow_json = create_workflow_json(
|
||||||
output_files = {}
|
input_file, output_dir, args.model, args.language, duration
|
||||||
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 = {
|
print(f"\n✅ 处理完成!")
|
||||||
"success": True,
|
print(f" 时长: {duration} 秒")
|
||||||
"input_file": input_file,
|
print(f" 分段数: {len(workflow_json['segments'])}")
|
||||||
"model": args.model,
|
print(f"\n📄 输出文件:")
|
||||||
"output_files": {
|
for fmt in ['txt', 'srt', 'json']:
|
||||||
k: os.path.basename(v) for k, v in output_files.items()
|
fpath = os.path.join(output_dir, f"{basename}.{fmt}")
|
||||||
},
|
if os.path.exists(fpath):
|
||||||
"output_dir": output_dir,
|
size = os.path.getsize(fpath)
|
||||||
"total_segments": len(standard_json.get('segments', [])),
|
print(f" • {fmt.upper()}: {fpath} ({size:,} bytes)")
|
||||||
"duration": standard_json.get('duration_seconds', 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
print("\n✓ 处理完成!")
|
print(f"\n📋 Workflow JSON:")
|
||||||
print(f"\n输出文件:")
|
print(json.dumps({
|
||||||
for fmt, fname in output_files.items():
|
"success": workflow_json["success"],
|
||||||
print(f" - {fname.upper()}: {fname}")
|
"tool": "whisper",
|
||||||
|
"text_length": len(workflow_json["text"]),
|
||||||
print(f"\n完整结果:")
|
"segments_count": len(workflow_json["segments"])
|
||||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
}, ensure_ascii=False, indent=2))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"\n✗ 错误: {e}", file=sys.stderr)
|
print(f"\n✗ 错误: {e}", file=sys.stderr)
|
||||||
|
|||||||
+11
-12
@@ -108,28 +108,28 @@ python3 /path/to/bilibili_content.py --id "[ID]" --type subtitle --output [subti
|
|||||||
|
|
||||||
3. 解析输出 JSON,提取 video_filename 和 subtitle_filename(如有)
|
3. 解析输出 JSON,提取 video_filename 和 subtitle_filename(如有)
|
||||||
|
|
||||||
### 步骤 5: OCR 处理(禁止跳过)
|
### 步骤 5: 语音识别转文字(禁止跳过)
|
||||||
|
|
||||||
> ⚠️ **重要**:此步骤为耗时步骤,视频按帧截取和 OCR 识别需要较长时间。
|
> ⚠️ **重要**:此步骤为耗时步骤,使用 Whisper 进行语音识别。
|
||||||
> - **严禁跳过**:即使等待时间较长,也必须等待 OCR 完成
|
> - **严禁跳过**:即使等待时间较长,也必须等待识别完成
|
||||||
> - **定时汇报**:每2-3分钟向用户汇报进度(处理帧数/预计剩余时间)
|
> - **定时汇报**:每2-3分钟向用户汇报进度
|
||||||
> - **超时时间**:30分钟
|
> - **超时时间**:30分钟
|
||||||
> - **耐心等待**:不要中途启动其他步骤,必须串行执行
|
> - **耐心等待**:不要中途启动其他步骤,必须串行执行
|
||||||
|
|
||||||
如果步骤4没有获取到字幕,需要从视频截帧 OCR:
|
如果步骤4没有获取到字幕,需要使用 Whisper 进行语音识别:
|
||||||
|
|
||||||
1. 获取目录路径:
|
1. 获取目录路径:
|
||||||
```bash
|
```bash
|
||||||
python3 kb_workflow.py --content
|
python3 kb_workflow.py --content
|
||||||
```
|
```
|
||||||
|
|
||||||
2. 启动子代理调用 kb-ocr-processor(传入完整路径):
|
2. 启动子代理调用 kb-whisper(传入完整路径):
|
||||||
```bash
|
```bash
|
||||||
# 必须传入 --output 完整路径
|
# 必须传入 --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使用
|
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-bilibili-metadata | `--id "BVxxx" --type video` | `{p}_{id}.meta.json` |
|
||||||
| 3 | kb-zhihu-metadata | `--id "xxx" --type question` | `{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}` |
|
| 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` |
|
| 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/
|
├── content/
|
||||||
│ ├── downloads/
|
│ ├── downloads/
|
||||||
│ │ └── {platform}_{id}.mp4
|
│ │ └── {platform}_{id}.mp4
|
||||||
│ ├── subtitles/
|
│ ├── {platform}_{id}.txt # Whisper 识别的文本
|
||||||
│ │ └── {platform}_{id}.srt
|
│ └── {platform}_{id}.json # Whisper 完整结果
|
||||||
│ └── {platform}_{id}.txt
|
|
||||||
└── analyzed/
|
└── analyzed/
|
||||||
└── {platform}_{id}.md
|
└── {platform}_{id}.md
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ def ensure_directories(base_path: Path = None) -> Dict[str, Path]:
|
|||||||
"metadata": base_path / "metadata",
|
"metadata": base_path / "metadata",
|
||||||
"content": base_path / "content",
|
"content": base_path / "content",
|
||||||
"content_downloads": base_path / "content" / "downloads",
|
"content_downloads": base_path / "content" / "downloads",
|
||||||
"content_subtitles": base_path / "content" / "subtitles",
|
|
||||||
"analyzed": base_path / "analyzed",
|
"analyzed": base_path / "analyzed",
|
||||||
"archive": base_path / "archive",
|
"archive": base_path / "archive",
|
||||||
"archive_subscriptions": base_path / "archive" / "subscriptions",
|
"archive_subscriptions": base_path / "archive" / "subscriptions",
|
||||||
@@ -250,7 +249,6 @@ def main():
|
|||||||
parser.add_argument('--metadata', action='store_true', help='获取元数据目录')
|
parser.add_argument('--metadata', action='store_true', help='获取元数据目录')
|
||||||
parser.add_argument('--content', 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_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('--analyzed', action='store_true', help='获取深度分析目录')
|
||||||
parser.add_argument('--archive_dir', action='store_true', help='获取归档目录')
|
parser.add_argument('--archive_dir', action='store_true', help='获取归档目录')
|
||||||
parser.add_argument('--archive_subscriptions', action='store_true', help='获取订阅信息目录')
|
parser.add_argument('--archive_subscriptions', action='store_true', help='获取订阅信息目录')
|
||||||
@@ -280,9 +278,6 @@ def main():
|
|||||||
elif args.content_downloads:
|
elif args.content_downloads:
|
||||||
print(json.dumps({"path": str(dirs["content_downloads"])}, ensure_ascii=False, indent=2))
|
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:
|
elif args.analyzed:
|
||||||
print(json.dumps({"path": str(dirs["analyzed"])}, ensure_ascii=False, indent=2))
|
print(json.dumps({"path": str(dirs["analyzed"])}, ensure_ascii=False, indent=2))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user