File size: 5,870 Bytes
ae8b600 e5e756a ae8b600 cc826a1 ae8b600 e5e756a ae8b600 e5e756a ae8b600 e5e756a ae8b600 e5e756a ae8b600 e5e756a ae8b600 e5e756a ae8b600 e5e756a ae8b600 e5e756a ae8b600 e5e756a ae8b600 e5e756a ae8b600 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | import base64
import logging
import os
import shutil
import subprocess
import tempfile
from pathlib import Path
from plugins.audio.core import get_transcription_service
logger = logging.getLogger(__name__)
class VideoAudioExtractorService:
"""视频音频提取和转录服务。"""
# 常见视频格式后缀
SUPPORTED_VIDEO_SUFFIXES = {".mp4", ".avi", ".mov", ".mkv", ".webm", ".flv", ".wmv", ".m4v", ".mpg", ".mpeg"}
def extract_audio(self, video_path: str, output_path: str | None = None) -> dict:
"""从视频文件中提取 WAV 音频(16kHz 单声道)。"""
if shutil.which("ffmpeg") is None:
return {
"success": False,
"audio_path": None,
"error": "未找到 ffmpeg,请在运行环境中安装 ffmpeg",
}
source = Path(video_path)
if not source.exists():
return {
"success": False,
"audio_path": None,
"error": f"视频文件不存在: {video_path}",
}
# 获取视频信息(时长等)用于更好的错误提示
video_info = self._probe_video(source)
target = Path(output_path) if output_path else Path(tempfile.mkstemp(suffix=".wav")[1])
command = [
"ffmpeg",
"-y",
"-i",
str(source),
"-vn", # 丢弃视频流
"-acodec",
"pcm_s16le", # 16-bit PCM
"-ar",
"16000", # 16kHz 采样率(Whisper 推荐)
"-ac",
"1", # 单声道
str(target),
]
logger.info(f"开始提取音频: {source.name} (时长: {video_info.get('duration', '未知')})")
try:
result = subprocess.run(
command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=300,
)
if result.returncode != 0:
target.unlink(missing_ok=True)
# 提取关键错误信息
error_lines = [l for l in result.stderr.split("\n") if "Error" in l or "Invalid" in l]
error_msg = "\n".join(error_lines) if error_lines else result.stderr[-500:]
logger.error(f"ffmpeg 提取音频失败: {error_msg}")
return {
"success": False,
"audio_path": None,
"error": f"音频提取失败: {error_msg}",
}
logger.info(f"音频提取完成: {target.stat().st_size / 1024:.1f}KB")
return {
"success": True,
"audio_path": str(target),
"error": None,
}
except subprocess.TimeoutExpired:
target.unlink(missing_ok=True)
return {
"success": False,
"audio_path": None,
"error": "音频提取超时(超过5分钟),请检查视频文件大小",
}
def _probe_video(self, source: Path) -> dict:
"""获取视频基本信息。"""
try:
result = subprocess.run(
["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", str(source)],
capture_output=True,
text=True,
timeout=30,
)
if result.returncode == 0:
import json
info = json.loads(result.stdout)
fmt = info.get("format", {})
return {
"duration": fmt.get("duration", "未知"),
"size_mb": round(int(fmt.get("size", 0)) / (1024 * 1024), 1),
}
except Exception:
pass
return {}
def transcribe_video_file(self, video_path: str, language: str | None = None) -> dict:
"""从视频提取音频并转录文本。"""
extraction = self.extract_audio(video_path)
if not extraction["success"]:
return {
"success": False,
"text": "",
"audio_path": extraction.get("audio_path"),
"error": extraction["error"],
}
audio_path = extraction["audio_path"]
try:
transcription = get_transcription_service().transcribe_file(audio_path, language)
return {
"success": transcription["success"],
"text": transcription.get("text", ""),
"audio_path": audio_path,
"error": transcription.get("error"),
}
finally:
Path(audio_path).unlink(missing_ok=True)
def transcribe_video_base64(
self,
video_base64: str,
suffix: str = ".mp4",
language: str | None = None,
) -> dict:
"""从 Base64 视频提取音频并转录文本。"""
try:
video_bytes = base64.b64decode(video_base64)
except Exception as e:
return {
"success": False,
"text": "",
"audio_path": None,
"error": f"Base64 解码失败: {e}",
}
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp_file:
tmp_file.write(video_bytes)
tmp_path = tmp_file.name
try:
return self.transcribe_video_file(tmp_path, language)
finally:
Path(tmp_path).unlink(missing_ok=True)
_video_audio_service: VideoAudioExtractorService | None = None
def get_video_audio_service() -> VideoAudioExtractorService:
global _video_audio_service
if _video_audio_service is None:
_video_audio_service = VideoAudioExtractorService()
return _video_audio_service
|