mirror of
https://github.com/d0zingcat/MoviePilot-Plugins.git
synced 2026-05-13 15:09:12 +00:00
新增AI字幕自动生成插件
This commit is contained in:
12
package.json
12
package.json
@@ -22,6 +22,18 @@
|
||||
"v1.9": "支持馒头新架构自动签到"
|
||||
}
|
||||
},
|
||||
"AutoSubv2": {
|
||||
"name": "AI字幕自动生成(v2)",
|
||||
"description": "使用whisper自动生成视频文件字幕,使用大模型翻译字幕成中文。",
|
||||
"version": "1.0",
|
||||
"icon": "autosubtitles.jpeg",
|
||||
"author": "TimoYoung",
|
||||
"level": 1,
|
||||
"v2": true,
|
||||
"history": {
|
||||
"v1.0": "first stable version"
|
||||
}
|
||||
},
|
||||
"CustomSites": {
|
||||
"name": "自定义站点",
|
||||
"description": "增加自定义站点为签到和统计使用。",
|
||||
|
||||
88
plugins/autosubv2/README.md
Normal file
88
plugins/autosubv2/README.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# AI字幕自动生成(v2)
|
||||
|
||||
自动生成视频字幕,并使用大模型将字幕翻译成中文。
|
||||
|
||||
基于 [autosub](https://github.com/lightolly/MoviePilot-Plugins) 修改并适配v2,感谢原作者
|
||||
|
||||
## 功能特点
|
||||
|
||||
- 支持从视频音轨中提取字幕(使用faster-whisper)
|
||||
- 支持从视频内嵌字幕中提取字幕
|
||||
- 支持从外挂字幕文件中提取字幕
|
||||
- 支持使用大模型(OpenAI)将字幕翻译成中文
|
||||
- 支持批量翻译以提高效率
|
||||
- 支持使用滑动窗口配置上下文提高翻译连贯性
|
||||
- 支持多种字幕提取语言偏好设置
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 基础配置
|
||||
|
||||
| 配置项 | 说明 | 默认值 |
|
||||
|--------|------|--------|
|
||||
| 立即运行一次 | 保存配置后是否立即执行一次任务 | 否 |
|
||||
| 本地字幕提取策略 | 设置字幕提取的优先级策略 | 优先原音字幕 |
|
||||
| 翻译为中文 | 是否在需要时使用大模型将字幕翻译成中文 | 是 |
|
||||
| 发送通知 | 是否发送任务执行通知 | 否 |
|
||||
|
||||
### ASR配置
|
||||
|
||||
| 配置项 | 说明 | 默认值 |
|
||||
|--------|------|--------|
|
||||
| 允许从音轨提取字幕 | 是否允许从视频音轨中提取字幕 | 是 |
|
||||
| ASR引擎 | 语音识别引擎 | faster-whisper |
|
||||
| 模型 | 使用的模型大小 | base |
|
||||
| 使用代理下载模型 | 是否使用代理下载模型 | 是 |
|
||||
|
||||
### 翻译配置
|
||||
|
||||
| 配置项 | 说明 | 默认值 |
|
||||
|--------|------|--------|
|
||||
| 启用批量翻译 | 是否启用批量翻译以提高效率 | 是 |
|
||||
| 每批翻译行数 | 每批处理的字幕行数 | 20 |
|
||||
| 上下文窗口大小 | 翻译时考虑的上下文行数 | 5 |
|
||||
| llm请求重试次数 | 翻译失败时的重试次数 | 3 |
|
||||
|
||||
### 其他配置
|
||||
|
||||
| 配置项 | 说明 | 默认值 |
|
||||
|--------|------|--------|
|
||||
| 媒体路径 | 要处理的媒体文件或文件夹绝对路径,每行一个 | 空 |
|
||||
| 文件大小(MB) | 最小处理文件大小 | 10 |
|
||||
|
||||
|
||||
## 字幕提取策略说明
|
||||
字幕提取优先级:外挂字幕 > 内嵌字幕 > 音轨识别
|
||||
|
||||
字幕提取策略的选择主要取决于视频源语言和大模型的翻译能力。对于包含多语言字幕的非英语视频,建议根据以下原则选择策略:
|
||||
|
||||
1. 仅英文字幕
|
||||
- 仅使用英文字幕作为翻译源
|
||||
- 当视频无英文字幕时,使用ASR提取
|
||||
- 适用于大模型仅支持中英互译的场景
|
||||
|
||||
2. 优先英文字幕
|
||||
- 优先使用英文字幕作为翻译源
|
||||
- 无英文字幕时,使用其他语言字幕
|
||||
- 当所有字幕都不存在时,使用ASR提取
|
||||
- 适用于大模型在英译中任务上表现更好的场景
|
||||
|
||||
3. 优先原音字幕
|
||||
- 优先使用视频原始语言的字幕
|
||||
- 无原音字幕时,使用英文字幕
|
||||
- 当所有字幕都不存在时,使用ASR提取
|
||||
- 适用于大模型支持多语言翻译且翻译质量较好的场景
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 翻译功能依赖OpenAI插件配置,使用前请确保已正确配置
|
||||
2. 首次使用音轨识别功能时,会自动从HuggingFace下载模型。开启"使用代理下载模型"选项会使用MP配置的代理。
|
||||
3. 媒体路径支持单个文件或文件夹的绝对路径。选择文件夹时会递归处理其中的所有视频文件,外挂字幕将从媒体文件同级目录中查找
|
||||
4. 批量翻译通过一次处理多行字幕来减少API调用次数,提高效率。如果翻译结果与原文行数不匹配,系统会自动降级为逐行翻译
|
||||
5. 上下文窗口大小和批量翻译行数需要根据大模型的推理能力来调整。当模型能力不足时,过大的批量或上下文窗口可能会影响翻译质量
|
||||
6. 翻译后的中文字幕会打上“机翻”标签。
|
||||
|
||||
## todo
|
||||
- 监听媒体入库事件自动调用字幕生成
|
||||
- 任务完成后调用媒体库刷新
|
||||
- 历史任务管理与展示
|
||||
1336
plugins/autosubv2/__init__.py
Normal file
1336
plugins/autosubv2/__init__.py
Normal file
File diff suppressed because it is too large
Load Diff
63
plugins/autosubv2/ffmpeg/__init__.py
Normal file
63
plugins/autosubv2/ffmpeg/__init__.py
Normal file
@@ -0,0 +1,63 @@
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
|
||||
class Ffmpeg:
|
||||
|
||||
@staticmethod
|
||||
def extract_wav_from_video(video_path, audio_path, audio_index=None):
|
||||
"""
|
||||
使用ffmpeg从视频文件中提取16000hz, 16-bit的wav格式音频
|
||||
"""
|
||||
if not video_path or not audio_path:
|
||||
return False
|
||||
|
||||
# 提取指定音频流
|
||||
if audio_index:
|
||||
command = ['ffmpeg', "-hide_banner", "-loglevel", "warning", '-y', '-i', video_path,
|
||||
'-map', f'0:a:{audio_index}',
|
||||
'-acodec', 'pcm_s16le', '-ac', '1', '-ar', '16000', audio_path]
|
||||
else:
|
||||
command = ['ffmpeg', "-hide_banner", "-loglevel", "warning", '-y', '-i', video_path,
|
||||
'-acodec', 'pcm_s16le', '-ac', '1', '-ar', '16000', audio_path]
|
||||
|
||||
ret = subprocess.run(command).returncode
|
||||
if ret == 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_video_metadata(video_path):
|
||||
"""
|
||||
获取视频元数据
|
||||
"""
|
||||
if not video_path:
|
||||
return False
|
||||
|
||||
try:
|
||||
command = ['ffprobe', '-v', 'quiet', '-print_format', 'json', '-show_format', '-show_streams', video_path]
|
||||
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
if result.returncode == 0:
|
||||
return json.loads(result.stdout.decode("utf-8"))
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def extract_subtitle_from_video(video_path, subtitle_path, subtitle_index=None):
|
||||
"""
|
||||
从视频中提取字幕
|
||||
"""
|
||||
if not video_path or not subtitle_path:
|
||||
return False
|
||||
|
||||
if subtitle_index:
|
||||
command = ['ffmpeg', "-hide_banner", "-loglevel", "warning", '-y', '-i', video_path,
|
||||
'-map', f'0:s:{subtitle_index}',
|
||||
subtitle_path]
|
||||
else:
|
||||
command = ['ffmpeg', "-hide_banner", "-loglevel", "warning", '-y', '-i', video_path, subtitle_path]
|
||||
ret = subprocess.run(command).returncode
|
||||
if ret == 0:
|
||||
return True
|
||||
return False
|
||||
4
plugins/autosubv2/requirements.txt
Normal file
4
plugins/autosubv2/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
iso639~=0.1.4
|
||||
srt~=3.5.3
|
||||
python-dotenv~=1.0.1
|
||||
faster-whisper~=1.0.1
|
||||
0
plugins/autosubv2/translate/__init__.py
Normal file
0
plugins/autosubv2/translate/__init__.py
Normal file
133
plugins/autosubv2/translate/openai.py
Normal file
133
plugins/autosubv2/translate/openai.py
Normal file
@@ -0,0 +1,133 @@
|
||||
import time
|
||||
from typing import List, Union
|
||||
|
||||
import openai
|
||||
from cacheout import Cache
|
||||
|
||||
OpenAISessionCache = Cache(maxsize=100, ttl=3600, timer=time.time, default=None)
|
||||
|
||||
|
||||
class OpenAi:
|
||||
_api_key: str = None
|
||||
_api_url: str = None
|
||||
_model: str = "gpt-3.5-turbo"
|
||||
|
||||
def __init__(self, api_key: str = None, api_url: str = None, proxy: dict = None, model: str = None):
|
||||
self._api_key = api_key
|
||||
self._api_url = api_url
|
||||
openai.api_base = self._api_url + "/v1"
|
||||
openai.api_key = self._api_key
|
||||
if proxy and proxy.get("https"):
|
||||
openai.proxy = proxy.get("https")
|
||||
if model:
|
||||
self._model = model
|
||||
|
||||
@staticmethod
|
||||
def __save_session(session_id: str, message: str):
|
||||
"""
|
||||
保存会话
|
||||
:param session_id: 会话ID
|
||||
:param message: 消息
|
||||
:return:
|
||||
"""
|
||||
seasion = OpenAISessionCache.get(session_id)
|
||||
if seasion:
|
||||
seasion.append({
|
||||
"role": "assistant",
|
||||
"content": message
|
||||
})
|
||||
OpenAISessionCache.set(session_id, seasion)
|
||||
|
||||
@staticmethod
|
||||
def __get_session(session_id: str, message: str) -> List[dict]:
|
||||
"""
|
||||
获取会话
|
||||
:param session_id: 会话ID
|
||||
:return: 会话上下文
|
||||
"""
|
||||
seasion = OpenAISessionCache.get(session_id)
|
||||
if seasion:
|
||||
seasion.append({
|
||||
"role": "user",
|
||||
"content": message
|
||||
})
|
||||
else:
|
||||
seasion = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "请在接下来的对话中请使用中文回复,并且内容尽可能详细。"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": message
|
||||
}]
|
||||
OpenAISessionCache.set(session_id, seasion)
|
||||
return seasion
|
||||
|
||||
def __get_model(self, message: Union[str, List[dict]],
|
||||
prompt: str = None,
|
||||
user: str = "MoviePilot",
|
||||
**kwargs):
|
||||
"""
|
||||
获取模型
|
||||
"""
|
||||
if not isinstance(message, list):
|
||||
if prompt:
|
||||
message = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": prompt
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": message
|
||||
}
|
||||
]
|
||||
else:
|
||||
message = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": message
|
||||
}
|
||||
]
|
||||
return openai.ChatCompletion.create(
|
||||
model=self._model,
|
||||
user=user,
|
||||
messages=message,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def __clear_session(session_id: str):
|
||||
"""
|
||||
清除会话
|
||||
:param session_id: 会话ID
|
||||
:return:
|
||||
"""
|
||||
if OpenAISessionCache.get(session_id):
|
||||
OpenAISessionCache.delete(session_id)
|
||||
|
||||
def translate_to_zh(self, text: str, context: str = None):
|
||||
"""
|
||||
翻译为中文
|
||||
:param text: 输入文本
|
||||
:param context: 翻译上下文
|
||||
"""
|
||||
system_prompt = """您是一位专业字幕翻译专家,请严格遵循以下规则:
|
||||
1. 将原文精准翻译为简体中文,保持原文本意
|
||||
2. 使用自然的口语化表达,符合中文观影习惯
|
||||
3. 结合上下文语境,人物称谓、专业术语、情感语气在上下文中保持连贯
|
||||
4. 按行翻译待译内容。翻译结果不要包括上下文。
|
||||
5. 输出内容必须仅包括译文。不要输出任何开场白,解释说明或总结"""
|
||||
user_prompt = f"翻译上下文:\n{context}\n\n需要翻译的内容:\n{text}" if context else f"请翻译:\n{text}"
|
||||
result = ""
|
||||
try:
|
||||
completion = self.__get_model(prompt=system_prompt,
|
||||
message=user_prompt,
|
||||
temperature=0.2,
|
||||
top_p=0.9)
|
||||
result = completion.choices[0].message.content.strip()
|
||||
return True, result
|
||||
except Exception as e:
|
||||
print(f"{str(e)}:{result}")
|
||||
return False, f"{str(e)}:{result}"
|
||||
Reference in New Issue
Block a user