mirror of
https://github.com/linyqh/NarratoAI.git
synced 2026-07-22 22:18:07 +00:00
feat: 新增本地FunASR支持并优化网页端视频与字幕工作流
- 更新示例配置文件,添加本地FunASR后端配置项 - 重构fun_asr_subtitle服务,完整支持本地FunASR-Pack API调用 - 优化多语言翻译文件,更新界面相关译文 - 重写网页端视频选择组件,支持本地资源目录选择和直接上传 - 重构字幕转写UI,支持本地/在线百炼/直接上传三种模式 - 新增本地FunASR相关单元测试
This commit is contained in:
parent
89eebb8b41
commit
e744960ac1
@ -1,15 +1,17 @@
|
||||
"""Aliyun Bailian Fun-ASR subtitle transcription helpers.
|
||||
"""Fun-ASR subtitle transcription helpers.
|
||||
|
||||
This module intentionally uses the REST API because the official Fun-ASR
|
||||
The Bailian path intentionally uses the REST API because the official Fun-ASR
|
||||
recorded-file API supports temporary `oss://` resources only through REST.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional
|
||||
from urllib.parse import urljoin, urlparse, urlunparse
|
||||
|
||||
import requests
|
||||
from loguru import logger
|
||||
@ -21,6 +23,7 @@ UPLOAD_POLICY_URL = f"{DASHSCOPE_BASE_URL}/api/v1/uploads"
|
||||
TRANSCRIPTION_URL = f"{DASHSCOPE_BASE_URL}/api/v1/services/audio/asr/transcription"
|
||||
TASK_URL_TEMPLATE = f"{DASHSCOPE_BASE_URL}/api/v1/tasks/{{task_id}}"
|
||||
MODEL_NAME = "fun-asr"
|
||||
LOCAL_FUN_ASR_API_URL = "http://127.0.0.1:7860"
|
||||
TERMINAL_FAILED_STATUSES = {"FAILED", "CANCELED", "UNKNOWN"}
|
||||
PUNCTUATION_BREAKS = set(",。!?;,.!?;")
|
||||
|
||||
@ -89,6 +92,85 @@ def _session_post(session, url: str, **kwargs):
|
||||
return session.post(url, **kwargs)
|
||||
|
||||
|
||||
def _require_local_file(local_file: str) -> None:
|
||||
if not os.path.isfile(local_file):
|
||||
raise FunAsrError(f"待转写文件不存在: {local_file}")
|
||||
|
||||
|
||||
def _normalize_local_api_url(api_url: str = "") -> str:
|
||||
api_url = (api_url or LOCAL_FUN_ASR_API_URL).strip().rstrip("/")
|
||||
if not api_url:
|
||||
raise FunAsrError("请先填写本地 FunASR-Pack API 地址")
|
||||
if "://" not in api_url:
|
||||
api_url = f"http://{api_url}"
|
||||
return api_url
|
||||
|
||||
|
||||
def _local_base_url(api_url: str = "") -> str:
|
||||
api_url = _normalize_local_api_url(api_url)
|
||||
parsed = urlparse(api_url)
|
||||
path = parsed.path.rstrip("/")
|
||||
if path.endswith("/asr"):
|
||||
path = path[:-4].rstrip("/")
|
||||
return urlunparse(parsed._replace(path=path, params="", query="", fragment="")).rstrip("/")
|
||||
|
||||
|
||||
def _local_asr_url(api_url: str = "") -> str:
|
||||
api_url = _normalize_local_api_url(api_url)
|
||||
if urlparse(api_url).path.rstrip("/").endswith("/asr"):
|
||||
return api_url
|
||||
return f"{api_url}/asr"
|
||||
|
||||
|
||||
def _absolute_local_download_url(api_url: str, download_url: str) -> str:
|
||||
download_url = (download_url or "").strip()
|
||||
if not download_url:
|
||||
return ""
|
||||
if urlparse(download_url).scheme:
|
||||
return download_url
|
||||
return urljoin(f"{_local_base_url(api_url)}/", download_url)
|
||||
|
||||
|
||||
def _raise_for_local_http(response: requests.Response, action: str) -> None:
|
||||
status_code = getattr(response, "status_code", 200)
|
||||
if status_code and status_code >= 400:
|
||||
detail = ""
|
||||
try:
|
||||
data = response.json()
|
||||
if isinstance(data, dict):
|
||||
detail = str(data.get("detail") or "")
|
||||
except Exception:
|
||||
detail = ""
|
||||
suffix = f": {detail}" if detail else ""
|
||||
raise FunAsrError(f"{action}失败{suffix},请确认本地 FunASR-Pack 服务可用")
|
||||
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except Exception as exc:
|
||||
raise FunAsrError(f"{action}失败,请确认本地 FunASR-Pack 服务可用") from exc
|
||||
|
||||
|
||||
def _local_json(response: requests.Response, action: str) -> dict[str, Any]:
|
||||
_raise_for_local_http(response, action)
|
||||
try:
|
||||
data = response.json()
|
||||
except Exception as exc:
|
||||
raise FunAsrError(f"{action}返回了无效 JSON") from exc
|
||||
if not isinstance(data, dict):
|
||||
raise FunAsrError(f"{action}返回格式无效")
|
||||
return data
|
||||
|
||||
|
||||
def _response_text(response: requests.Response) -> str:
|
||||
text = getattr(response, "text", None)
|
||||
if isinstance(text, str):
|
||||
return text
|
||||
content = getattr(response, "content", b"")
|
||||
if isinstance(content, bytes):
|
||||
return content.decode("utf-8")
|
||||
return str(content)
|
||||
|
||||
|
||||
def request_upload_policy(api_key: str, model: str = MODEL_NAME, session=requests) -> UploadPolicy:
|
||||
"""Request Bailian temporary-storage upload policy for the target model."""
|
||||
api_key = _require_api_key(api_key)
|
||||
@ -418,6 +500,216 @@ def write_srt_file(srt_content: str, subtitle_file: str = "") -> str:
|
||||
return subtitle_file
|
||||
|
||||
|
||||
def copy_srt_file(source_file: str, subtitle_file: str = "") -> str:
|
||||
"""Copy an existing SRT file into NarratoAI's subtitle directory."""
|
||||
if not os.path.isfile(source_file):
|
||||
raise FunAsrError(f"本地 FunASR-Pack 返回的字幕文件不存在: {source_file}")
|
||||
if not subtitle_file:
|
||||
subtitle_file = os.path.join(utils.subtitle_dir(), f"fun_asr_local_{int(time.time())}.srt")
|
||||
parent = os.path.dirname(subtitle_file)
|
||||
if parent:
|
||||
os.makedirs(parent, exist_ok=True)
|
||||
if os.path.abspath(source_file) != os.path.abspath(subtitle_file):
|
||||
shutil.copyfile(source_file, subtitle_file)
|
||||
return subtitle_file
|
||||
|
||||
|
||||
def request_local_fun_asr_health(api_url: str = LOCAL_FUN_ASR_API_URL, session=requests) -> dict[str, Any]:
|
||||
"""Fetch FunASR-Pack health metadata from the local service."""
|
||||
response = _session_get(session, f"{_local_base_url(api_url)}/health", timeout=10)
|
||||
return _local_json(response, "检查本地 FunASR-Pack 服务")
|
||||
|
||||
|
||||
def request_local_fun_asr(
|
||||
local_file: str,
|
||||
api_url: str = LOCAL_FUN_ASR_API_URL,
|
||||
hotword: str = "",
|
||||
enable_spk: Optional[bool] = None,
|
||||
timeout: float = 600.0,
|
||||
session=requests,
|
||||
) -> dict[str, Any]:
|
||||
"""Call the local FunASR-Pack `/asr` API and return its JSON result."""
|
||||
_require_local_file(local_file)
|
||||
data: dict[str, str] = {}
|
||||
if hotword.strip():
|
||||
data["hotword"] = hotword.strip()
|
||||
if enable_spk is not None:
|
||||
data["enable_spk"] = "true" if enable_spk else "false"
|
||||
|
||||
with open(local_file, "rb") as file_obj:
|
||||
files = {"file": (_safe_upload_name(local_file), file_obj)}
|
||||
response = _session_post(
|
||||
session,
|
||||
_local_asr_url(api_url),
|
||||
data=data,
|
||||
files=files,
|
||||
timeout=timeout,
|
||||
)
|
||||
return _local_json(response, "调用本地 FunASR-Pack ASR API")
|
||||
|
||||
|
||||
def download_local_srt(
|
||||
download_url: str,
|
||||
api_url: str = LOCAL_FUN_ASR_API_URL,
|
||||
subtitle_file: str = "",
|
||||
session=requests,
|
||||
) -> str:
|
||||
"""Download an SRT exposed by FunASR-Pack and save it as a NarratoAI subtitle."""
|
||||
absolute_url = _absolute_local_download_url(api_url, download_url)
|
||||
if not absolute_url:
|
||||
raise FunAsrError("本地 FunASR-Pack 结果缺少 SRT 下载地址")
|
||||
response = _session_get(session, absolute_url, timeout=60)
|
||||
_raise_for_local_http(response, "下载本地 FunASR-Pack SRT")
|
||||
srt_content = _response_text(response)
|
||||
if not srt_content.strip():
|
||||
raise FunAsrError("本地 FunASR-Pack 返回了空 SRT")
|
||||
return write_srt_file(srt_content, subtitle_file)
|
||||
|
||||
|
||||
def _local_result_items(result_json: dict[str, Any]):
|
||||
raw = result_json.get("raw")
|
||||
if isinstance(raw, dict):
|
||||
yield raw
|
||||
elif isinstance(raw, list):
|
||||
for item in raw:
|
||||
if isinstance(item, dict):
|
||||
yield item
|
||||
elif result_json.get("text"):
|
||||
yield result_json
|
||||
|
||||
|
||||
def _blocks_from_local_timestamp(item: dict[str, Any], max_chars: int, max_duration: float) -> list[dict[str, Any]]:
|
||||
text = str(item.get("text") or "").strip()
|
||||
timestamps = item.get("timestamp") or []
|
||||
if not text or not isinstance(timestamps, list):
|
||||
return []
|
||||
|
||||
non_space_chars = [char for char in text if char.strip()]
|
||||
consume_punctuation = len(timestamps) >= len(non_space_chars)
|
||||
blocks: list[dict[str, Any]] = []
|
||||
current: Optional[dict[str, Any]] = None
|
||||
timestamp_index = 0
|
||||
last_end = 0.0
|
||||
max_duration_ms = max_duration * 1000
|
||||
|
||||
for char in text:
|
||||
if not char.strip():
|
||||
continue
|
||||
|
||||
is_punctuation = char in PUNCTUATION_BREAKS
|
||||
consume_timestamp = consume_punctuation or not is_punctuation
|
||||
if consume_timestamp and timestamp_index < len(timestamps):
|
||||
pair = timestamps[timestamp_index]
|
||||
timestamp_index += 1
|
||||
if not isinstance(pair, (list, tuple)) or len(pair) < 2:
|
||||
continue
|
||||
start_ms = _timestamp_ms(pair[0], "local.timestamp.start")
|
||||
end_ms = _timestamp_ms(pair[1], "local.timestamp.end")
|
||||
last_end = end_ms
|
||||
else:
|
||||
start_ms = last_end
|
||||
end_ms = last_end if is_punctuation else last_end + 200
|
||||
last_end = end_ms
|
||||
|
||||
if current is None:
|
||||
current = {"start": start_ms, "end": end_ms, "text": char}
|
||||
else:
|
||||
should_split_before = (
|
||||
len(current["text"] + char) > max_chars
|
||||
or (end_ms - current["start"]) > max_duration_ms
|
||||
)
|
||||
if should_split_before:
|
||||
_flush_block(blocks, current)
|
||||
current = {"start": start_ms, "end": end_ms, "text": char}
|
||||
else:
|
||||
current["text"] += char
|
||||
current["end"] = end_ms
|
||||
|
||||
if current and is_punctuation:
|
||||
_flush_block(blocks, current)
|
||||
current = None
|
||||
|
||||
if current:
|
||||
_flush_block(blocks, current)
|
||||
return blocks
|
||||
|
||||
|
||||
def local_fun_asr_result_to_srt(
|
||||
result_json: dict[str, Any],
|
||||
max_chars: int = 20,
|
||||
max_duration: float = 3.5,
|
||||
) -> str:
|
||||
"""Convert a FunASR-Pack JSON response into SRT when the API SRT is unavailable."""
|
||||
blocks: list[dict[str, Any]] = []
|
||||
for item in _local_result_items(result_json):
|
||||
item_blocks = _blocks_from_local_timestamp(item, max_chars, max_duration)
|
||||
if not item_blocks:
|
||||
text = str(item.get("text") or "").strip()
|
||||
if text:
|
||||
item_blocks = _blocks_from_sentence(
|
||||
{
|
||||
"begin_time": 0,
|
||||
"end_time": max(1500, len(text) * 180),
|
||||
"text": text,
|
||||
},
|
||||
max_chars=max_chars,
|
||||
)
|
||||
blocks.extend(item_blocks)
|
||||
|
||||
if not blocks:
|
||||
raise FunAsrError("本地 FunASR-Pack 转写结果为空:未找到可用字幕内容")
|
||||
|
||||
lines = []
|
||||
for index, block in enumerate(blocks, start=1):
|
||||
lines.append(_srt_block(index, block["start"], block["end"], block["text"]))
|
||||
return "\n".join(lines).rstrip() + "\n"
|
||||
|
||||
|
||||
def create_with_local_fun_asr(
|
||||
local_file: str,
|
||||
subtitle_file: str = "",
|
||||
api_url: str = LOCAL_FUN_ASR_API_URL,
|
||||
hotword: str = "",
|
||||
enable_spk: Optional[bool] = None,
|
||||
timeout: float = 600.0,
|
||||
session=requests,
|
||||
) -> Optional[str]:
|
||||
"""Create an SRT file through a locally running FunASR-Pack API."""
|
||||
try:
|
||||
result_json = request_local_fun_asr(
|
||||
local_file=local_file,
|
||||
api_url=api_url,
|
||||
hotword=hotword,
|
||||
enable_spk=enable_spk,
|
||||
timeout=timeout,
|
||||
session=session,
|
||||
)
|
||||
|
||||
srt_file = result_json.get("srt_file")
|
||||
if isinstance(srt_file, str) and srt_file and os.path.isfile(srt_file):
|
||||
output_file = copy_srt_file(srt_file, subtitle_file)
|
||||
else:
|
||||
downloads = result_json.get("downloads") or {}
|
||||
download_url = downloads.get("srt") if isinstance(downloads, dict) else ""
|
||||
if download_url:
|
||||
output_file = download_local_srt(
|
||||
download_url,
|
||||
api_url=api_url,
|
||||
subtitle_file=subtitle_file,
|
||||
session=session,
|
||||
)
|
||||
else:
|
||||
srt_content = local_fun_asr_result_to_srt(result_json)
|
||||
output_file = write_srt_file(srt_content, subtitle_file)
|
||||
|
||||
logger.info(f"本地 FunASR-Pack 字幕文件已生成: {output_file}")
|
||||
return output_file
|
||||
except FunAsrError:
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise FunAsrError("本地 FunASR-Pack 字幕转写失败,请检查服务地址、文件或模型状态") from exc
|
||||
|
||||
|
||||
def create_with_fun_asr(
|
||||
local_file: str,
|
||||
subtitle_file: str = "",
|
||||
|
||||
@ -12,9 +12,11 @@ from app.services import fun_asr_subtitle as fasr
|
||||
|
||||
|
||||
class FakeResponse:
|
||||
def __init__(self, payload=None, status_code=200):
|
||||
def __init__(self, payload=None, status_code=200, text=None):
|
||||
self.payload = payload or {}
|
||||
self.status_code = status_code
|
||||
self.text = text
|
||||
self.content = text.encode("utf-8") if isinstance(text, str) else b""
|
||||
|
||||
def json(self):
|
||||
return self.payload
|
||||
@ -375,6 +377,110 @@ class FunAsrServiceTests(unittest.TestCase):
|
||||
fasr.download_transcription_result("https://result.example/bad.json", session=MalformedDownloadSession({}))
|
||||
|
||||
|
||||
class LocalFunAsrServiceTests(unittest.TestCase):
|
||||
def test_request_local_fun_asr_posts_file_and_options(self):
|
||||
class LocalSession:
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
|
||||
def post(self, url, **kwargs):
|
||||
self.calls.append(("POST", url, kwargs))
|
||||
return FakeResponse({"text": "你好", "srt_file": "/tmp/out.srt"})
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
local_file = Path(tmp_dir) / "audio.wav"
|
||||
local_file.write_bytes(b"audio")
|
||||
session = LocalSession()
|
||||
|
||||
result = fasr.request_local_fun_asr(
|
||||
str(local_file),
|
||||
api_url="127.0.0.1:7860",
|
||||
hotword="NarratoAI",
|
||||
enable_spk=True,
|
||||
timeout=123,
|
||||
session=session,
|
||||
)
|
||||
|
||||
self.assertEqual("你好", result["text"])
|
||||
self.assertEqual("POST", session.calls[0][0])
|
||||
self.assertEqual("http://127.0.0.1:7860/asr", session.calls[0][1])
|
||||
self.assertEqual({"hotword": "NarratoAI", "enable_spk": "true"}, session.calls[0][2]["data"])
|
||||
self.assertEqual(123, session.calls[0][2]["timeout"])
|
||||
self.assertIn("file", session.calls[0][2]["files"])
|
||||
|
||||
def test_create_with_local_fun_asr_copies_pack_srt_file(self):
|
||||
class LocalSession:
|
||||
def __init__(self, srt_file):
|
||||
self.srt_file = srt_file
|
||||
self.calls = []
|
||||
|
||||
def post(self, url, **kwargs):
|
||||
self.calls.append(("POST", url, kwargs))
|
||||
return FakeResponse({"text": "你好", "srt_file": str(self.srt_file)})
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
local_file = Path(tmp_dir) / "audio.wav"
|
||||
local_file.write_bytes(b"audio")
|
||||
pack_srt = Path(tmp_dir) / "pack.srt"
|
||||
pack_srt.write_text("1\n00:00:00,000 --> 00:00:01,000\n你好\n", encoding="utf-8")
|
||||
subtitle_file = Path(tmp_dir) / "out.srt"
|
||||
|
||||
result_path = fasr.create_with_local_fun_asr(
|
||||
str(local_file),
|
||||
subtitle_file=str(subtitle_file),
|
||||
api_url="http://127.0.0.1:7860",
|
||||
session=LocalSession(pack_srt),
|
||||
)
|
||||
|
||||
self.assertEqual(str(subtitle_file), result_path)
|
||||
self.assertEqual(pack_srt.read_text(encoding="utf-8"), subtitle_file.read_text(encoding="utf-8"))
|
||||
|
||||
def test_create_with_local_fun_asr_downloads_relative_srt(self):
|
||||
class LocalSession:
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
|
||||
def post(self, url, **kwargs):
|
||||
self.calls.append(("POST", url, kwargs))
|
||||
return FakeResponse({"text": "你好", "downloads": {"srt": "/download/result.srt"}})
|
||||
|
||||
def get(self, url, **kwargs):
|
||||
self.calls.append(("GET", url, kwargs))
|
||||
return FakeResponse(text="1\n00:00:00,000 --> 00:00:01,000\n你好\n")
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
local_file = Path(tmp_dir) / "audio.wav"
|
||||
local_file.write_bytes(b"audio")
|
||||
subtitle_file = Path(tmp_dir) / "out.srt"
|
||||
session = LocalSession()
|
||||
|
||||
result_path = fasr.create_with_local_fun_asr(
|
||||
str(local_file),
|
||||
subtitle_file=str(subtitle_file),
|
||||
api_url="http://127.0.0.1:7860/asr",
|
||||
session=session,
|
||||
)
|
||||
|
||||
self.assertEqual(str(subtitle_file), result_path)
|
||||
self.assertEqual("http://127.0.0.1:7860/download/result.srt", session.calls[1][1])
|
||||
self.assertIn("你好", subtitle_file.read_text(encoding="utf-8"))
|
||||
|
||||
def test_local_fun_asr_result_to_srt_uses_raw_timestamps(self):
|
||||
result = {
|
||||
"raw": [
|
||||
{
|
||||
"text": "你好,世界。",
|
||||
"timestamp": [[0, 300], [300, 600], [600, 900], [900, 1200]],
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
srt = fasr.local_fun_asr_result_to_srt(result, max_chars=20)
|
||||
|
||||
self.assertIn("00:00:00,000 --> 00:00:00,600\n你好,", srt)
|
||||
self.assertIn("世界。", srt)
|
||||
|
||||
|
||||
class FunAsrConfigTests(unittest.TestCase):
|
||||
def test_save_config_persists_fun_asr_section(self):
|
||||
original_config_file = cfg.config_file
|
||||
@ -395,6 +501,8 @@ class FunAsrConfigTests(unittest.TestCase):
|
||||
|
||||
def test_config_example_fun_asr_section_parses(self):
|
||||
config_data = tomllib.loads(Path("config.example.toml").read_text(encoding="utf-8"))
|
||||
self.assertEqual("local", config_data["fun_asr"]["backend"])
|
||||
self.assertEqual("http://127.0.0.1:7860", config_data["fun_asr"]["api_url"])
|
||||
self.assertEqual("fun-asr", config_data["fun_asr"]["model"])
|
||||
self.assertIn("api_key", config_data["fun_asr"])
|
||||
|
||||
|
||||
@ -95,8 +95,13 @@
|
||||
model_name = "qwen3-tts-flash"
|
||||
|
||||
[fun_asr]
|
||||
# 阿里百炼 Fun-ASR 字幕转录配置
|
||||
# 访问 https://bailian.console.aliyun.com/?tab=model#/api-key 获取你的 API 密钥
|
||||
# Fun-ASR 字幕转录配置
|
||||
# backend = "local" 使用本地 FunASR-Pack API;backend = "bailian" 使用阿里百炼在线 fun-asr
|
||||
backend = "local"
|
||||
api_url = "http://127.0.0.1:7860"
|
||||
hotword = ""
|
||||
enable_spk = false
|
||||
# 使用阿里百炼在线 fun-asr 时,访问 https://bailian.console.aliyun.com/?tab=model#/api-key 获取 API Key
|
||||
api_key = ""
|
||||
model = "fun-asr"
|
||||
|
||||
|
||||
@ -56,12 +56,12 @@ def render_script_file(tr, params):
|
||||
MODE_SHORT = "short"
|
||||
MODE_SUMMARY = "summary"
|
||||
|
||||
# 模式选项映射
|
||||
# 模式选项映射,按工作流优先级展示
|
||||
mode_options = {
|
||||
tr("Select/Upload Script"): MODE_FILE,
|
||||
tr("Short Drama Summary"): MODE_SUMMARY,
|
||||
tr("Auto Generate"): MODE_AUTO,
|
||||
tr("Short Generate"): MODE_SHORT,
|
||||
tr("Short Drama Summary"): MODE_SUMMARY,
|
||||
tr("Select/Upload Script"): MODE_FILE,
|
||||
}
|
||||
|
||||
# 获取当前状态
|
||||
@ -80,8 +80,7 @@ def render_script_file(tr, params):
|
||||
else:
|
||||
default_index = mode_keys.index(tr("Select/Upload Script"))
|
||||
|
||||
# 1. 渲染功能选择下拉框
|
||||
# 使用 segmented_control 替代 selectbox,提供更好的视觉体验
|
||||
# 1. 渲染功能选择组件
|
||||
default_mode_label = mode_keys[default_index]
|
||||
default_mode = mode_options[default_mode_label]
|
||||
|
||||
@ -106,17 +105,16 @@ def render_script_file(tr, params):
|
||||
st.session_state.video_clip_json_path = new_mode
|
||||
params.video_clip_json_path = new_mode
|
||||
else:
|
||||
# 如果用户取消选择(segmented_control 允许取消),恢复到默认或上一个状态
|
||||
# 这里我们强制保持当前状态,或者重置为默认
|
||||
st.session_state.script_mode_selection = default_mode_label
|
||||
st.session_state.video_clip_json_path = default_mode
|
||||
params.video_clip_json_path = default_mode
|
||||
|
||||
# 渲染组件
|
||||
selected_mode_label = st.segmented_control(
|
||||
selected_mode_label = st.selectbox(
|
||||
tr("Video Type"),
|
||||
options=mode_keys,
|
||||
index=None,
|
||||
key="script_mode_selection",
|
||||
on_change=update_script_mode,
|
||||
required=True
|
||||
)
|
||||
|
||||
# 处理旧状态为空的兜底情况
|
||||
@ -231,50 +229,115 @@ def render_script_file(tr, params):
|
||||
|
||||
def render_video_file(tr, params):
|
||||
"""渲染视频文件选择"""
|
||||
video_list = [(tr("None"), ""), (tr("Upload Local Files"), "upload_local")]
|
||||
source_options = {
|
||||
tr("Select from resource directory"): "resource",
|
||||
tr("Upload Local Files"): "upload",
|
||||
}
|
||||
source_labels = list(source_options.keys())
|
||||
default_source_label = source_labels[0]
|
||||
|
||||
# 获取已有视频文件
|
||||
for suffix in ["*.mp4", "*.mov", "*.avi", "*.mkv"]:
|
||||
video_files = glob.glob(os.path.join(utils.video_dir(), suffix))
|
||||
for file in video_files:
|
||||
display_name = file.replace(config.root_dir, "")
|
||||
video_list.append((display_name, file))
|
||||
if (
|
||||
'video_source_selection' not in st.session_state
|
||||
or st.session_state['video_source_selection'] not in source_options
|
||||
):
|
||||
st.session_state['video_source_selection'] = default_source_label
|
||||
|
||||
selected_video_index = st.selectbox(
|
||||
tr("Video File"),
|
||||
index=0,
|
||||
options=range(len(video_list)),
|
||||
format_func=lambda x: video_list[x][0]
|
||||
current_source = st.session_state['video_source_selection']
|
||||
source_caption = (
|
||||
tr("Select a video from resource videos directory")
|
||||
if source_options[current_source] == "resource"
|
||||
else tr("Upload a new video file up to 2GB")
|
||||
)
|
||||
st.markdown(f"**{tr('Video Source')}** :gray[{source_caption}]")
|
||||
|
||||
video_path = video_list[selected_video_index][1]
|
||||
st.session_state['video_origin_path'] = video_path
|
||||
params.video_origin_path = video_path
|
||||
source = st.selectbox(
|
||||
tr("Video Source"),
|
||||
options=source_labels,
|
||||
index=None,
|
||||
key="video_source_selection",
|
||||
label_visibility="collapsed",
|
||||
)
|
||||
if not source:
|
||||
source = default_source_label
|
||||
|
||||
if video_path == "upload_local":
|
||||
uploaded_file = st.file_uploader(
|
||||
tr("Upload Local Files"),
|
||||
type=["mp4", "mov", "avi", "flv", "mkv"],
|
||||
accept_multiple_files=False,
|
||||
if source_options[source] == "resource":
|
||||
video_files = []
|
||||
for suffix in ["*.mp4", "*.mov", "*.avi", "*.flv", "*.mkv", "*.mpeg4"]:
|
||||
video_files.extend(glob.glob(os.path.join(utils.video_dir(), suffix)))
|
||||
|
||||
video_files = sorted(video_files, key=os.path.getctime, reverse=True)
|
||||
saved_video_path = st.session_state.get('video_origin_path', '')
|
||||
selected_video_path = st.session_state.get('resource_video_selection')
|
||||
if selected_video_path not in video_files:
|
||||
st.session_state['resource_video_selection'] = (
|
||||
saved_video_path if saved_video_path in video_files else None
|
||||
)
|
||||
|
||||
def format_video_name(path):
|
||||
return path.replace(config.root_dir, "")
|
||||
|
||||
video_path = st.selectbox(
|
||||
tr("Select Video"),
|
||||
options=video_files,
|
||||
index=None,
|
||||
placeholder=tr("Choose a video file"),
|
||||
format_func=format_video_name,
|
||||
key="resource_video_selection",
|
||||
)
|
||||
|
||||
if uploaded_file is not None:
|
||||
safe_filename = os.path.basename(uploaded_file.name)
|
||||
video_file_path = os.path.join(utils.video_dir(), safe_filename)
|
||||
file_name, file_extension = os.path.splitext(safe_filename)
|
||||
if video_path:
|
||||
st.session_state['video_origin_path'] = video_path
|
||||
params.video_origin_path = video_path
|
||||
else:
|
||||
st.session_state['video_origin_path'] = ""
|
||||
params.video_origin_path = ""
|
||||
if not video_files:
|
||||
st.info(tr("No video files found in resource videos directory"))
|
||||
return
|
||||
|
||||
if os.path.exists(video_file_path):
|
||||
timestamp = time.strftime("%Y%m%d%H%M%S")
|
||||
file_name_with_timestamp = f"{file_name}_{timestamp}"
|
||||
video_file_path = os.path.join(utils.video_dir(), file_name_with_timestamp + file_extension)
|
||||
if source_options[source] == "upload":
|
||||
uploaded_file = st.file_uploader(
|
||||
tr("Upload Video"),
|
||||
type=["mp4", "mov", "avi", "flv", "mkv", "mpeg4"],
|
||||
accept_multiple_files=False,
|
||||
key="video_file_uploader",
|
||||
)
|
||||
|
||||
with open(video_file_path, "wb") as f:
|
||||
f.write(uploaded_file.read())
|
||||
st.success(tr("File Uploaded Successfully"))
|
||||
if uploaded_file is None:
|
||||
st.session_state['video_origin_path'] = ""
|
||||
params.video_origin_path = ""
|
||||
st.session_state['video_file_processed'] = False
|
||||
st.session_state['uploaded_video_path'] = ""
|
||||
st.session_state['uploaded_video_signature'] = ""
|
||||
else:
|
||||
uploaded_signature = f"{uploaded_file.name}:{uploaded_file.size}"
|
||||
uploaded_video_path = st.session_state.get('uploaded_video_path', '')
|
||||
is_processed = (
|
||||
st.session_state.get('video_file_processed', False)
|
||||
and st.session_state.get('uploaded_video_signature') == uploaded_signature
|
||||
and uploaded_video_path
|
||||
)
|
||||
|
||||
if is_processed:
|
||||
st.session_state['video_origin_path'] = uploaded_video_path
|
||||
params.video_origin_path = uploaded_video_path
|
||||
else:
|
||||
safe_filename = os.path.basename(uploaded_file.name)
|
||||
video_file_path = os.path.join(utils.video_dir(), safe_filename)
|
||||
file_name, file_extension = os.path.splitext(safe_filename)
|
||||
|
||||
if os.path.exists(video_file_path):
|
||||
timestamp = time.strftime("%Y%m%d%H%M%S")
|
||||
file_name_with_timestamp = f"{file_name}_{timestamp}"
|
||||
video_file_path = os.path.join(utils.video_dir(), file_name_with_timestamp + file_extension)
|
||||
|
||||
with open(video_file_path, "wb") as f:
|
||||
f.write(uploaded_file.read())
|
||||
st.session_state['video_origin_path'] = video_file_path
|
||||
params.video_origin_path = video_file_path
|
||||
time.sleep(1)
|
||||
st.rerun()
|
||||
st.session_state['uploaded_video_path'] = video_file_path
|
||||
st.session_state['uploaded_video_signature'] = uploaded_signature
|
||||
st.session_state['video_file_processed'] = True
|
||||
|
||||
|
||||
def render_short_generate_options(tr):
|
||||
@ -336,7 +399,18 @@ def short_drama_summary(tr):
|
||||
st.session_state['subtitle_file_processed'] = False
|
||||
|
||||
render_fun_asr_transcription(tr)
|
||||
|
||||
|
||||
# 名称输入框
|
||||
video_theme = st.text_input(tr("短剧名称"))
|
||||
st.session_state['video_theme'] = video_theme
|
||||
# 数字输入框
|
||||
temperature = st.slider("temperature", 0.0, 2.0, 0.7)
|
||||
st.session_state['temperature'] = temperature
|
||||
return video_theme
|
||||
|
||||
|
||||
def render_subtitle_upload(tr):
|
||||
"""上传并保存用户提供的 SRT 字幕文件。"""
|
||||
subtitle_file = st.file_uploader(
|
||||
tr("上传字幕文件"),
|
||||
type=["srt"],
|
||||
@ -401,102 +475,180 @@ def short_drama_summary(tr):
|
||||
except Exception as e:
|
||||
st.error(f"{tr('Upload failed')}: {str(e)}")
|
||||
|
||||
# 名称输入框
|
||||
video_theme = st.text_input(tr("短剧名称"))
|
||||
st.session_state['video_theme'] = video_theme
|
||||
# 数字输入框
|
||||
temperature = st.slider("temperature", 0.0, 2.0, 0.7)
|
||||
st.session_state['temperature'] = temperature
|
||||
return video_theme
|
||||
|
||||
|
||||
def render_fun_asr_transcription(tr):
|
||||
"""使用阿里百炼 Fun-ASR 从本地音视频转写生成字幕。"""
|
||||
"""使用 Fun-ASR 从本地音视频转写生成字幕。"""
|
||||
def clear_fun_asr_subtitle_state():
|
||||
st.session_state['subtitle_path'] = None
|
||||
st.session_state['subtitle_content'] = None
|
||||
st.session_state['subtitle_file_processed'] = False
|
||||
|
||||
with st.expander(tr("Ali Bailian Fun-ASR Subtitle Transcription"), expanded=False):
|
||||
st.caption(tr("Fun-ASR upload caption"))
|
||||
st.markdown(
|
||||
f"{tr('API Key URL')}: "
|
||||
"[https://bailian.console.aliyun.com/?tab=model#/api-key]"
|
||||
"(https://bailian.console.aliyun.com/?tab=model#/api-key)"
|
||||
from app.services import fun_asr_subtitle
|
||||
|
||||
backend_options = {
|
||||
tr("Local FunASR-Pack API"): "local",
|
||||
tr("Ali Bailian Online Fun-ASR"): "bailian",
|
||||
tr("上传字幕文件"): "upload",
|
||||
}
|
||||
saved_backend = str(config.fun_asr.get("backend", "")).strip().lower()
|
||||
if saved_backend not in {"local", "bailian", "upload"}:
|
||||
saved_backend = (
|
||||
"bailian"
|
||||
if config.fun_asr.get("api_key") and not config.fun_asr.get("api_url")
|
||||
else "local"
|
||||
)
|
||||
|
||||
api_key = st.text_input(
|
||||
tr("Ali Bailian API Key"),
|
||||
value=config.fun_asr.get("api_key", ""),
|
||||
type="password",
|
||||
help=tr("Ali Bailian API Key Help"),
|
||||
key="fun_asr_api_key",
|
||||
)
|
||||
uploaded_media = st.file_uploader(
|
||||
tr("Upload media to transcribe"),
|
||||
type=[
|
||||
"aac", "amr", "avi", "flac", "flv", "m4a", "mkv", "mov",
|
||||
"mp3", "mp4", "mpeg", "ogg", "opus", "wav", "webm", "wma", "wmv",
|
||||
],
|
||||
accept_multiple_files=False,
|
||||
key="fun_asr_media_uploader",
|
||||
)
|
||||
backend_values = list(backend_options.values())
|
||||
backend_labels = list(backend_options.keys())
|
||||
backend = saved_backend
|
||||
api_key = ""
|
||||
api_url = config.fun_asr.get("api_url", fun_asr_subtitle.LOCAL_FUN_ASR_API_URL)
|
||||
hotword = config.fun_asr.get("hotword", "")
|
||||
enable_spk = bool(config.fun_asr.get("enable_spk", False))
|
||||
media_path = st.session_state.get('video_origin_path', '')
|
||||
|
||||
if st.button(tr("Transcribe subtitles"), key="fun_asr_transcribe"):
|
||||
if not api_key.strip():
|
||||
clear_fun_asr_subtitle_state()
|
||||
st.error(tr("Please enter Ali Bailian API Key"))
|
||||
return
|
||||
if uploaded_media is None:
|
||||
clear_fun_asr_subtitle_state()
|
||||
st.error(tr("Please upload media to transcribe"))
|
||||
return
|
||||
subtitle_cols = st.columns([3, 2], vertical_alignment="top")
|
||||
|
||||
try:
|
||||
clear_fun_asr_subtitle_state()
|
||||
from app.services import fun_asr_subtitle
|
||||
with subtitle_cols[0]:
|
||||
with st.expander(tr("Ali Bailian Fun-ASR Subtitle Transcription"), expanded=False):
|
||||
backend_label = st.radio(
|
||||
tr("Subtitle Processing Method"),
|
||||
options=backend_labels,
|
||||
index=backend_values.index(saved_backend),
|
||||
horizontal=True,
|
||||
key="fun_asr_backend",
|
||||
)
|
||||
backend = backend_options[backend_label]
|
||||
|
||||
config.fun_asr["api_key"] = api_key.strip()
|
||||
config.fun_asr["model"] = "fun-asr"
|
||||
config.save_config()
|
||||
if backend == "upload":
|
||||
render_subtitle_upload(tr)
|
||||
elif backend == "local":
|
||||
st.caption(tr("Local Fun-ASR upload caption"))
|
||||
api_url = st.text_input(
|
||||
tr("Local FunASR-Pack API URL"),
|
||||
value=api_url,
|
||||
help=tr("Local FunASR-Pack API URL Help"),
|
||||
key="fun_asr_api_url",
|
||||
)
|
||||
hotword = st.text_input(
|
||||
tr("Fun-ASR Hotword"),
|
||||
value=hotword,
|
||||
help=tr("Fun-ASR Hotword Help"),
|
||||
key="fun_asr_hotword",
|
||||
)
|
||||
enable_spk = st.checkbox(
|
||||
tr("Enable speaker diarization"),
|
||||
value=enable_spk,
|
||||
help=tr("Enable speaker diarization Help"),
|
||||
key="fun_asr_enable_spk",
|
||||
)
|
||||
else:
|
||||
st.caption(tr("Fun-ASR upload caption"))
|
||||
st.markdown(
|
||||
f"{tr('API Key URL')}: "
|
||||
"[https://bailian.console.aliyun.com/?tab=model#/api-key]"
|
||||
"(https://bailian.console.aliyun.com/?tab=model#/api-key)"
|
||||
)
|
||||
|
||||
temp_dir = utils.temp_dir("fun_asr")
|
||||
safe_filename = os.path.basename(uploaded_media.name)
|
||||
media_path = os.path.join(temp_dir, safe_filename)
|
||||
file_name, file_extension = os.path.splitext(safe_filename)
|
||||
if os.path.exists(media_path):
|
||||
timestamp = time.strftime("%Y%m%d%H%M%S")
|
||||
media_path = os.path.join(temp_dir, f"{file_name}_{timestamp}{file_extension}")
|
||||
api_key = st.text_input(
|
||||
tr("Ali Bailian API Key"),
|
||||
value=config.fun_asr.get("api_key", ""),
|
||||
type="password",
|
||||
help=tr("Ali Bailian API Key Help"),
|
||||
key="fun_asr_api_key",
|
||||
)
|
||||
|
||||
with open(media_path, "wb") as f:
|
||||
f.write(uploaded_media.getbuffer())
|
||||
|
||||
subtitle_name = f"{os.path.splitext(os.path.basename(media_path))[0]}_fun_asr.srt"
|
||||
subtitle_path = os.path.join(utils.subtitle_dir(), subtitle_name)
|
||||
|
||||
with st.spinner(tr("Transcribing with Fun-ASR...")):
|
||||
generated_path = fun_asr_subtitle.create_with_fun_asr(
|
||||
local_file=media_path,
|
||||
subtitle_file=subtitle_path,
|
||||
api_key=api_key.strip(),
|
||||
if backend != "upload":
|
||||
if media_path:
|
||||
st.info(
|
||||
tr("Using selected video for subtitle transcription").format(
|
||||
file=os.path.basename(media_path)
|
||||
)
|
||||
)
|
||||
else:
|
||||
st.warning(tr("Please select or upload a video first"))
|
||||
|
||||
if not generated_path or not os.path.exists(generated_path):
|
||||
clear_fun_asr_subtitle_state()
|
||||
st.error(tr("Fun-ASR failed without subtitle file"))
|
||||
return
|
||||
can_transcribe = backend != "upload" and bool(media_path)
|
||||
with subtitle_cols[1]:
|
||||
transcribe_clicked = st.button(
|
||||
tr("Transcribe subtitles"),
|
||||
key="fun_asr_transcribe",
|
||||
disabled=not can_transcribe,
|
||||
use_container_width=True,
|
||||
)
|
||||
|
||||
with open(generated_path, "r", encoding="utf-8") as f:
|
||||
subtitle_content = f.read()
|
||||
if not transcribe_clicked:
|
||||
return
|
||||
|
||||
st.session_state['subtitle_path'] = generated_path
|
||||
st.session_state['subtitle_content'] = subtitle_content
|
||||
st.session_state['subtitle_file_processed'] = True
|
||||
st.success(tr("Subtitle transcription succeeded").format(file=os.path.basename(generated_path)))
|
||||
except Exception as e:
|
||||
clear_fun_asr_subtitle_state()
|
||||
logger.error(f"Fun-ASR 字幕转写失败: {traceback.format_exc()}")
|
||||
st.error(f"{tr('Fun-ASR transcription failed')}: {str(e)}")
|
||||
if backend == "bailian" and not api_key.strip():
|
||||
clear_fun_asr_subtitle_state()
|
||||
st.error(tr("Please enter Ali Bailian API Key"))
|
||||
return
|
||||
if backend == "local" and not str(api_url).strip():
|
||||
clear_fun_asr_subtitle_state()
|
||||
st.error(tr("Please enter local FunASR-Pack API URL"))
|
||||
return
|
||||
if not media_path or not os.path.exists(media_path):
|
||||
clear_fun_asr_subtitle_state()
|
||||
st.error(tr("Selected video file does not exist"))
|
||||
return
|
||||
|
||||
try:
|
||||
clear_fun_asr_subtitle_state()
|
||||
|
||||
config.fun_asr["backend"] = backend
|
||||
config.fun_asr["api_url"] = str(api_url).strip()
|
||||
config.fun_asr["api_key"] = api_key.strip()
|
||||
config.fun_asr["hotword"] = str(hotword).strip()
|
||||
config.fun_asr["enable_spk"] = bool(enable_spk)
|
||||
config.fun_asr["model"] = "fun-asr"
|
||||
config.save_config()
|
||||
|
||||
subtitle_name = f"{os.path.splitext(os.path.basename(media_path))[0]}_fun_asr.srt"
|
||||
subtitle_path = os.path.join(utils.subtitle_dir(), subtitle_name)
|
||||
|
||||
spinner_text = (
|
||||
tr("Transcribing with local FunASR-Pack...")
|
||||
if backend == "local"
|
||||
else tr("Transcribing with Fun-ASR...")
|
||||
)
|
||||
with st.spinner(spinner_text):
|
||||
if backend == "local":
|
||||
generated_path = fun_asr_subtitle.create_with_local_fun_asr(
|
||||
local_file=media_path,
|
||||
subtitle_file=subtitle_path,
|
||||
api_url=str(api_url).strip(),
|
||||
hotword=str(hotword).strip(),
|
||||
enable_spk=bool(enable_spk),
|
||||
)
|
||||
else:
|
||||
generated_path = fun_asr_subtitle.create_with_fun_asr(
|
||||
local_file=media_path,
|
||||
subtitle_file=subtitle_path,
|
||||
api_key=api_key.strip(),
|
||||
)
|
||||
|
||||
if not generated_path or not os.path.exists(generated_path):
|
||||
clear_fun_asr_subtitle_state()
|
||||
st.error(tr("Fun-ASR failed without subtitle file"))
|
||||
return
|
||||
|
||||
with open(generated_path, "r", encoding="utf-8") as f:
|
||||
subtitle_content = f.read()
|
||||
|
||||
st.session_state['subtitle_path'] = generated_path
|
||||
st.session_state['subtitle_content'] = subtitle_content
|
||||
st.session_state['subtitle_file_processed'] = True
|
||||
success_placeholder = st.empty()
|
||||
success_placeholder.success(
|
||||
tr("Subtitle transcription succeeded").format(file=os.path.basename(generated_path))
|
||||
)
|
||||
time.sleep(3)
|
||||
success_placeholder.empty()
|
||||
except Exception as e:
|
||||
clear_fun_asr_subtitle_state()
|
||||
logger.error(f"Fun-ASR 字幕转写失败: {traceback.format_exc()}")
|
||||
st.error(f"{tr('Fun-ASR transcription failed')}: {str(e)}")
|
||||
|
||||
|
||||
def render_script_buttons(tr, params):
|
||||
|
||||
@ -8,11 +8,11 @@
|
||||
"Script Files": "Script Files",
|
||||
"Generate Video Script and Keywords": "Click to use AI to generate **Video Script** and **Video Keywords** based on the **subject**",
|
||||
"Auto Detect": "Auto Detect",
|
||||
"Auto Generate": "Auto Generate",
|
||||
"Auto Generate": "Frame Analysis",
|
||||
"Video Script": "Video Script (:blue[①Optional, use AI to generate ②Proper punctuation helps in generating subtitles])",
|
||||
"Save Script": "Save Script",
|
||||
"Crop Video": "Crop Video",
|
||||
"Video File": "Video File (:blue[1️⃣Supports uploading video files (limit 2G) 2️⃣For large files, it is recommended to directly import them into the ./resource/videos directory])",
|
||||
"Video File": "Video File",
|
||||
"Plot Description": "Plot Description (:blue[Can be obtained from https://www.tvmao.com/])",
|
||||
"Generate Video Keywords": "Click to use AI to generate **Video Keywords** based on the **script**",
|
||||
"Please Enter the Video Subject": "Please enter the video script first",
|
||||
@ -84,6 +84,13 @@
|
||||
"Synthesizing Voice": "Synthesizing voice, please wait...",
|
||||
"TTS Provider": "TTS Provider",
|
||||
"Hide Log": "Hide Log",
|
||||
"Select from resource directory": "Select from resource directory",
|
||||
"Select a video from resource videos directory": "Select a video from the ./resource/videos directory",
|
||||
"Upload a new video file up to 2GB": "Upload a new video file, up to 2GB",
|
||||
"Select Video": "Select Video",
|
||||
"Choose a video file": "Choose a video file",
|
||||
"Upload Video": "Upload Video",
|
||||
"No video files found in resource videos directory": "No video files found in the ./resource/videos directory",
|
||||
"Upload Local Files": "Upload Local Files",
|
||||
"File Uploaded Successfully": "File Uploaded Successfully",
|
||||
"Frame Interval (seconds)": "Frame Interval (seconds)",
|
||||
@ -172,8 +179,8 @@
|
||||
"Batch Size": "Batch Size",
|
||||
"Batch Size (More keyframes consume more tokens)": "Batch Size (smaller batches consume more tokens)",
|
||||
"Short Drama Summary": "Short Drama Summary",
|
||||
"Video Type": "Video Type",
|
||||
"Select/Upload Script": "Select/Upload Script",
|
||||
"Video Type": "Creation Type",
|
||||
"Select/Upload Script": "Custom Script",
|
||||
"原生Gemini模型连接成功": "Native Gemini model connection succeeded",
|
||||
"原生Gemini模型连接失败": "Native Gemini model connection failed",
|
||||
"OpenAI兼容Gemini代理连接成功": "OpenAI-compatible Gemini proxy connection succeeded",
|
||||
@ -181,7 +188,7 @@
|
||||
"Connection failed": "Connection failed",
|
||||
"自定义片段": "Custom Clips",
|
||||
"设置需要生成的短视频片段数量": "Set the number of short video clips to generate",
|
||||
"上传字幕文件": "Upload Subtitle File",
|
||||
"上传字幕文件": "Upload SRT",
|
||||
"清除已上传字幕": "Clear Uploaded Subtitle",
|
||||
"无法读取字幕文件,请检查文件编码(支持 UTF-8、UTF-16、GBK、GB2312)": "Unable to read the subtitle file. Please check the file encoding. Supported encodings: UTF-8, UTF-16, GBK, GB2312.",
|
||||
"字幕文件内容似乎为空,请检查文件": "The subtitle file appears to be empty. Please check the file.",
|
||||
@ -289,15 +296,31 @@
|
||||
"Encoding": "Encoding",
|
||||
"Size": "Size",
|
||||
"Characters": "characters",
|
||||
"Ali Bailian Fun-ASR Subtitle Transcription": "Ali Bailian Fun-ASR Subtitle Transcription",
|
||||
"Fun-ASR upload caption": "After uploading a local audio/video file, it will be uploaded to temporary Ali Bailian storage and converted to SRT subtitles with fun-asr.",
|
||||
"Ali Bailian Fun-ASR Subtitle Transcription": "Subtitle Processing",
|
||||
"Subtitle Processing Method": "Subtitle Processing Method",
|
||||
"Fun-ASR Backend": "Fun-ASR Backend",
|
||||
"Local FunASR-Pack API": "Local",
|
||||
"Ali Bailian Online Fun-ASR": "Online",
|
||||
"Local Fun-ASR upload caption": "The current video above will be converted to SRT subtitles through the locally running FunASR-Pack API.",
|
||||
"Fun-ASR upload caption": "The current video above will be uploaded to temporary Ali Bailian storage and converted to SRT subtitles with fun-asr.",
|
||||
"Local FunASR-Pack API URL": "Local FunASR-Pack API URL",
|
||||
"Local FunASR-Pack API URL Help": "For example, http://127.0.0.1:7860. A full /asr endpoint URL is also supported.",
|
||||
"Fun-ASR Hotword": "Hotword",
|
||||
"Fun-ASR Hotword Help": "Optional hotwords passed to the local FunASR-Pack API.",
|
||||
"Enable speaker diarization": "Enable speaker diarization",
|
||||
"Enable speaker diarization Help": "Requires the local FunASR-Pack service to enable and load the spk model.",
|
||||
"API Key URL": "API Key URL",
|
||||
"Ali Bailian API Key": "Ali Bailian API Key",
|
||||
"Ali Bailian API Key Help": "Enter your Ali Bailian API Key. After saving, it will be written to the local config.toml file.",
|
||||
"Upload media to transcribe": "Upload audio/video to transcribe",
|
||||
"Using selected video for subtitle transcription": "Using current video for subtitle transcription: {file}",
|
||||
"Please select or upload a video first": "Please select or upload a video file above first",
|
||||
"Selected video file does not exist": "The selected video file does not exist. Please select or upload it again",
|
||||
"Transcribe subtitles": "Transcribe Subtitles",
|
||||
"Please enter Ali Bailian API Key": "Please enter the Ali Bailian API Key first",
|
||||
"Please enter local FunASR-Pack API URL": "Please enter the local FunASR-Pack API URL first",
|
||||
"Please upload media to transcribe": "Please upload the audio or video file to transcribe first",
|
||||
"Transcribing with local FunASR-Pack...": "Transcribing subtitles with local FunASR-Pack, please wait...",
|
||||
"Transcribing with Fun-ASR...": "Transcribing subtitles with Ali Bailian Fun-ASR, please wait...",
|
||||
"Fun-ASR failed without subtitle file": "Fun-ASR transcription failed: no subtitle file was generated",
|
||||
"Subtitle transcription succeeded": "Subtitle transcription succeeded: {file}",
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
"Video Theme": "视频主题",
|
||||
"Generation Prompt": "自定义提示词",
|
||||
"Save Script": "保存脚本",
|
||||
"Video File": "视频文件(:blue[1️⃣支持上传视频文件(限制2G) 2️⃣大文件建议直接导入 ./resource/videos 目录])",
|
||||
"Video File": "视频文件",
|
||||
"Plot Description": "剧情描述 (:blue[可从 https://www.tvmao.com/ 获取])",
|
||||
"Generate Video Keywords": "点击使用AI根据**文案**生成【视频关键】",
|
||||
"Please Enter the Video Subject": "请先填写视频文案",
|
||||
@ -80,6 +80,13 @@
|
||||
"Synthesizing Voice": "语音合成中,请稍候...",
|
||||
"TTS Provider": "语音合成提供商",
|
||||
"Hide Log": "隐藏日志",
|
||||
"Select from resource directory": "从资源目录选择",
|
||||
"Select a video from resource videos directory": "选择 ./resource/videos 目录中的视频",
|
||||
"Upload a new video file up to 2GB": "上传一个新的视频文件,限制 2GB",
|
||||
"Select Video": "选择视频",
|
||||
"Choose a video file": "选择一个视频文件",
|
||||
"Upload Video": "上传视频",
|
||||
"No video files found in resource videos directory": "未在 ./resource/videos 目录中找到视频文件",
|
||||
"Upload Local Files": "上传本地文件",
|
||||
"File Uploaded Successfully": "文件上传成功",
|
||||
"timestamp": "时间戳",
|
||||
@ -156,14 +163,14 @@
|
||||
"Generate Short Video Script": "AI生成短剧混剪脚本",
|
||||
"Adjust the volume of the original audio": "调整原始音频的音量",
|
||||
"Original Volume": "视频音量",
|
||||
"Auto Generate": "逐帧解说",
|
||||
"Auto Generate": "逐帧分析",
|
||||
"Frame Interval (seconds)": "帧间隔 (秒)",
|
||||
"Frame Interval (seconds) (More keyframes consume more tokens)": "帧间隔 (秒) (更多关键帧消耗更多令牌)",
|
||||
"Batch Size": "批处理大小",
|
||||
"Batch Size (More keyframes consume more tokens)": "批处理大小, 每批处理越少消耗 token 越多",
|
||||
"Short Drama Summary": "短剧解说",
|
||||
"Video Type": "视频类型",
|
||||
"Select/Upload Script": "选择/上传脚本",
|
||||
"Video Type": "创作类型",
|
||||
"Select/Upload Script": "自定义脚本",
|
||||
"Script loaded successfully": "脚本加载成功",
|
||||
"Failed to load script": "加载脚本失败",
|
||||
"Failed to save script": "保存脚本失败",
|
||||
@ -271,15 +278,31 @@
|
||||
"Encoding": "编码",
|
||||
"Size": "大小",
|
||||
"Characters": "字符",
|
||||
"Ali Bailian Fun-ASR Subtitle Transcription": "阿里百炼 Fun-ASR 字幕转录",
|
||||
"Fun-ASR upload caption": "上传本地音频/视频后,将自动上传到阿里百炼临时存储并通过 fun-asr 生成 SRT 字幕。",
|
||||
"Ali Bailian Fun-ASR Subtitle Transcription": "字幕处理",
|
||||
"Subtitle Processing Method": "字幕处理方式",
|
||||
"Fun-ASR Backend": "Fun-ASR 后端",
|
||||
"Local FunASR-Pack API": "本地转写",
|
||||
"Ali Bailian Online Fun-ASR": "在线转写",
|
||||
"Local Fun-ASR upload caption": "将使用上方当前视频,通过本机运行的 FunASR-Pack API 生成 SRT 字幕。",
|
||||
"Fun-ASR upload caption": "将使用上方当前视频,自动上传到阿里百炼临时存储并通过 fun-asr 生成 SRT 字幕。",
|
||||
"Local FunASR-Pack API URL": "本地 FunASR-Pack API 地址",
|
||||
"Local FunASR-Pack API URL Help": "例如 http://127.0.0.1:7860;也可以直接填到 /asr 的完整地址。",
|
||||
"Fun-ASR Hotword": "热词",
|
||||
"Fun-ASR Hotword Help": "可选,传给本地 FunASR-Pack 的热词参数。",
|
||||
"Enable speaker diarization": "启用说话人分段",
|
||||
"Enable speaker diarization Help": "需要本地 FunASR-Pack 已启用并加载 spk 模型。",
|
||||
"API Key URL": "API Key 获取地址",
|
||||
"Ali Bailian API Key": "阿里百炼 API Key",
|
||||
"Ali Bailian API Key Help": "请输入你自己的阿里百炼 API Key;保存配置后会写入本地 config.toml",
|
||||
"Upload media to transcribe": "上传需要转录的音频/视频",
|
||||
"Using selected video for subtitle transcription": "将使用当前视频生成字幕: {file}",
|
||||
"Please select or upload a video first": "请先在上方选择或上传视频文件",
|
||||
"Selected video file does not exist": "当前视频文件不存在,请重新选择或上传",
|
||||
"Transcribe subtitles": "转写生成字幕",
|
||||
"Please enter Ali Bailian API Key": "请先输入阿里百炼 API Key",
|
||||
"Please enter local FunASR-Pack API URL": "请先输入本地 FunASR-Pack API 地址",
|
||||
"Please upload media to transcribe": "请先上传需要转录的音频或视频文件",
|
||||
"Transcribing with local FunASR-Pack...": "正在使用本地 FunASR-Pack 转写字幕,请稍候...",
|
||||
"Transcribing with Fun-ASR...": "正在使用阿里百炼 Fun-ASR 转写字幕,请稍候...",
|
||||
"Fun-ASR failed without subtitle file": "Fun-ASR 转写失败:未生成字幕文件",
|
||||
"Subtitle transcription succeeded": "字幕转写成功: {file}",
|
||||
@ -357,7 +380,7 @@
|
||||
"Voice synthesis successful": "✅ 语音合成成功!",
|
||||
"Voice synthesis failed": "❌ 语音合成失败,请检查配置",
|
||||
"SoulVoice pitch not supported": "ℹ️ SoulVoice 引擎不支持音调调节",
|
||||
"上传字幕文件": "上传字幕文件",
|
||||
"上传字幕文件": "上传字幕",
|
||||
"清除已上传字幕": "清除已上传字幕",
|
||||
"无法读取字幕文件,请检查文件编码(支持 UTF-8、UTF-16、GBK、GB2312)": "无法读取字幕文件,请检查文件编码(支持 UTF-8、UTF-16、GBK、GB2312)",
|
||||
"字幕文件内容似乎为空,请检查文件": "字幕文件内容似乎为空,请检查文件",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user