From ddb4e5b8a638266387f72b2836bf37f3d0201d3c Mon Sep 17 00:00:00 2001 From: linyq Date: Tue, 16 Sep 2025 21:16:21 +0800 Subject: [PATCH 1/6] =?UTF-8?q?=E5=BF=BD=E7=95=A5=E5=BC=80=E5=8F=91?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f3c7489..3e055d7 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,7 @@ app/models/faster-whisper-large-v3/* app/models/bert/* bug清单.md -task.md \ No newline at end of file +task.md +.claude/* +.serena/* +CLAUDE.md \ No newline at end of file From d0f80270240c3d3c586e0cee442f511f8f3e1d2e Mon Sep 17 00:00:00 2001 From: linyq Date: Tue, 16 Sep 2025 22:45:19 +0800 Subject: [PATCH 2/6] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20gemini=20=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E8=AF=B7=E6=B1=82=E5=8F=82=E6=95=B0=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/services/SDE/short_drama_explanation.py | 8 +- app/services/llm/providers/gemini_provider.py | 8 +- app/utils/gemini_analyzer.py | 4 +- app/utils/script_generator.py | 4 +- webui/components/basic_settings.py | 97 ++++--------------- 5 files changed, 31 insertions(+), 90 deletions(-) diff --git a/app/services/SDE/short_drama_explanation.py b/app/services/SDE/short_drama_explanation.py index 1d17ae1..3044983 100644 --- a/app/services/SDE/short_drama_explanation.py +++ b/app/services/SDE/short_drama_explanation.py @@ -152,13 +152,13 @@ class SubtitleAnalyzer: } # 构建请求URL - url = f"{self.base_url}/models/{self.model}:generateContent?key={self.api_key}" + url = f"{self.base_url}/models/{self.model}:generateContent" # 发送请求 response = requests.post( url, json=payload, - headers={"Content-Type": "application/json", "User-Agent": "NarratoAI/1.0"}, + headers={"Content-Type": "application/json", "x-goog-api-key": self.api_key}, timeout=120 ) @@ -440,13 +440,13 @@ class SubtitleAnalyzer: } # 构建请求URL - url = f"{self.base_url}/models/{self.model}:generateContent?key={self.api_key}" + url = f"{self.base_url}/models/{self.model}:generateContent" # 发送请求 response = requests.post( url, json=payload, - headers={"Content-Type": "application/json", "User-Agent": "NarratoAI/1.0"}, + headers={"Content-Type": "application/json", "x-goog-api-key": self.api_key}, timeout=120 ) diff --git a/app/services/llm/providers/gemini_provider.py b/app/services/llm/providers/gemini_provider.py index 949df21..e9225c3 100644 --- a/app/services/llm/providers/gemini_provider.py +++ b/app/services/llm/providers/gemini_provider.py @@ -140,7 +140,7 @@ class GeminiVisionProvider(VisionModelProvider): """执行原生Gemini API调用,包含重试机制""" from app.config import config - url = f"{self.base_url}/models/{self.model_name}:generateContent?key={self.api_key}" + url = f"{self.base_url}/models/{self.model_name}:generateContent" max_retries = config.app.get('llm_max_retries', 3) base_timeout = config.app.get('llm_vision_timeout', 120) @@ -157,7 +157,7 @@ class GeminiVisionProvider(VisionModelProvider): json=payload, headers={ "Content-Type": "application/json", - "User-Agent": "NarratoAI/1.0" + "x-goog-api-key": self.api_key }, timeout=timeout ) @@ -330,7 +330,7 @@ class GeminiTextProvider(TextModelProvider): """执行原生Gemini API调用,包含重试机制""" from app.config import config - url = f"{self.base_url}/models/{self.model_name}:generateContent?key={self.api_key}" + url = f"{self.base_url}/models/{self.model_name}:generateContent" max_retries = config.app.get('llm_max_retries', 3) base_timeout = config.app.get('llm_text_timeout', 180) # 文本生成任务使用更长的基础超时时间 @@ -347,7 +347,7 @@ class GeminiTextProvider(TextModelProvider): json=payload, headers={ "Content-Type": "application/json", - "User-Agent": "NarratoAI/1.0" + "x-goog-api-key": self.api_key }, timeout=timeout ) diff --git a/app/utils/gemini_analyzer.py b/app/utils/gemini_analyzer.py index c3685ab..2de4086 100644 --- a/app/utils/gemini_analyzer.py +++ b/app/utils/gemini_analyzer.py @@ -107,7 +107,7 @@ class VisionAnalyzer: } # 构建请求URL - url = f"{self.base_url}/models/{self.model_name}:generateContent?key={self.api_key}" + url = f"{self.base_url}/models/{self.model_name}:generateContent" # 发送请求 response = await asyncio.to_thread( @@ -116,7 +116,7 @@ class VisionAnalyzer: json=request_data, headers={ "Content-Type": "application/json", - "User-Agent": "NarratoAI/1.0" + "x-goog-api-key": self.api_key }, timeout=120 # 增加超时时间 ) diff --git a/app/utils/script_generator.py b/app/utils/script_generator.py index e6d7cea..18cc618 100644 --- a/app/utils/script_generator.py +++ b/app/utils/script_generator.py @@ -230,7 +230,7 @@ class GeminiOpenAIGenerator(BaseGenerator): } # 构建请求URL - url = f"{self.base_url}/models/{self.model_name}:generateContent?key={self.api_key}" + url = f"{self.base_url}/models/{self.model_name}:generateContent" # 发送请求 response = requests.post( @@ -238,7 +238,7 @@ class GeminiOpenAIGenerator(BaseGenerator): json=request_data, headers={ "Content-Type": "application/json", - "User-Agent": "NarratoAI/1.0" + "x-goog-api-key": self.api_key }, timeout=120 ) diff --git a/webui/components/basic_settings.py b/webui/components/basic_settings.py index a887246..a4aca65 100644 --- a/webui/components/basic_settings.py +++ b/webui/components/basic_settings.py @@ -138,6 +138,7 @@ def test_vision_model_connection(api_key, base_url, model_name, provider, tr): str: 测试结果消息 """ import requests + logger.debug(f"大模型连通性测试: {base_url} 模型: {model_name} apikey: {api_key}") if provider.lower() == 'gemini': # 原生Gemini API测试 try: @@ -145,43 +146,21 @@ def test_vision_model_connection(api_key, base_url, model_name, provider, tr): request_data = { "contents": [{ "parts": [{"text": "直接回复我文本'当前网络可用'"}] - }], - "generationConfig": { - "temperature": 1.0, - "topK": 40, - "topP": 0.95, - "maxOutputTokens": 100, - }, - "safetySettings": [ - { - "category": "HARM_CATEGORY_HARASSMENT", - "threshold": "BLOCK_NONE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "threshold": "BLOCK_NONE" - }, - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "threshold": "BLOCK_NONE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "threshold": "BLOCK_NONE" - } - ] + }] } # 构建请求URL - api_base_url = base_url or "https://generativelanguage.googleapis.com/v1beta" - url = f"{api_base_url}/models/{model_name}:generateContent?key={api_key}" - + api_base_url = base_url + url = f"{api_base_url}/models/{model_name}:generateContent" # 发送请求 response = requests.post( url, json=request_data, - headers={"Content-Type": "application/json"}, - timeout=30 + headers={ + "x-goog-api-key": api_key, + "Content-Type": "application/json" + }, + timeout=10 ) if response.status_code == 200: @@ -190,7 +169,6 @@ def test_vision_model_connection(api_key, base_url, model_name, provider, tr): return False, f"{tr('原生Gemini模型连接失败')}: HTTP {response.status_code}" except Exception as e: return False, f"{tr('原生Gemini模型连接失败')}: {str(e)}" - elif provider.lower() == 'gemini(openai)': # OpenAI兼容的Gemini代理测试 try: @@ -215,23 +193,6 @@ def test_vision_model_connection(api_key, base_url, model_name, provider, tr): return False, f"{tr('OpenAI兼容Gemini代理连接失败')}: HTTP {response.status_code}" except Exception as e: return False, f"{tr('OpenAI兼容Gemini代理连接失败')}: {str(e)}" - elif provider.lower() == 'narratoapi': - try: - # 构建测试请求 - headers = { - "Authorization": f"Bearer {api_key}" - } - - test_url = f"{base_url.rstrip('/')}/health" - response = requests.get(test_url, headers=headers, timeout=10) - - if response.status_code == 200: - return True, tr("NarratoAPI is available") - else: - return False, f"{tr('NarratoAPI is not available')}: HTTP {response.status_code}" - except Exception as e: - return False, f"{tr('NarratoAPI is not available')}: {str(e)}" - else: from openai import OpenAI try: @@ -441,7 +402,8 @@ def test_text_model_connection(api_key, base_url, model_name, provider, tr): str: 测试结果消息 """ import requests - + logger.debug(f"大模型连通性测试: {base_url} 模型: {model_name} apikey: {api_key}") + try: # 构建统一的测试请求(遵循OpenAI格式) headers = { @@ -457,43 +419,22 @@ def test_text_model_connection(api_key, base_url, model_name, provider, tr): request_data = { "contents": [{ "parts": [{"text": "直接回复我文本'当前网络可用'"}] - }], - "generationConfig": { - "temperature": 1.0, - "topK": 40, - "topP": 0.95, - "maxOutputTokens": 100, - }, - "safetySettings": [ - { - "category": "HARM_CATEGORY_HARASSMENT", - "threshold": "BLOCK_NONE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "threshold": "BLOCK_NONE" - }, - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "threshold": "BLOCK_NONE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "threshold": "BLOCK_NONE" - } - ] + }] } # 构建请求URL - api_base_url = base_url or "https://generativelanguage.googleapis.com/v1beta" - url = f"{api_base_url}/models/{model_name}:generateContent?key={api_key}" + api_base_url = base_url + url = f"{api_base_url}/models/{model_name}:generateContent" # 发送请求 response = requests.post( url, json=request_data, - headers={"Content-Type": "application/json"}, - timeout=30 + headers={ + "x-goog-api-key": api_key, + "Content-Type": "application/json" + }, + timeout=10 ) if response.status_code == 200: From 30f7b6d7d90d4f228a7531cb01c794355e6a8358 Mon Sep 17 00:00:00 2001 From: linyq Date: Tue, 16 Sep 2025 22:57:16 +0800 Subject: [PATCH 3/6] =?UTF-8?q?=E4=BC=98=E5=8C=96=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index dc45976..667e38e 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ NarratoAI 是一个自动化影视解说工具,基于LLM实现文案撰写、 本项目仅供学习和研究使用,不得商用。如需商业授权,请联系作者。 ## 最新资讯 +- 2025.08.18 发布新版本 0.7.1,支持 **语音克隆** 和 最新大模型 - 2025.05.11 发布新版本 0.6.0,支持 **短剧解说** 和 优化剪辑流程 - 2025.03.06 发布新版本 0.5.2,支持 DeepSeek R1 和 DeepSeek V3 模型进行短剧混剪 - 2024.12.16 发布新版本 0.3.9,支持阿里 Qwen2-VL 模型理解视频;支持短剧混剪 @@ -44,7 +45,7 @@ NarratoAI 是一个自动化影视解说工具,基于LLM实现文案撰写、 - 2024.11.10 发布新版本 v0.3.5;优化视频剪辑流程, ## 重磅福利 🎉 -即日起全面支持DeepSeek模型!注册即享2000万免费Token(价值14元平台配额),剪辑10分钟视频仅需0.1元! +即日起全面支持国产模型!注册即享2000万免费Token(价值14元平台配额),剪辑10分钟视频仅需0.1元! 🔥 快速领福利: 1️⃣ 点击链接注册:https://cloud.siliconflow.cn/i/pyOKqFCV @@ -57,25 +58,21 @@ NarratoAI 是一个自动化影视解说工具,基于LLM实现文案撰写、 立即行动,用「pyOKqFCV」解锁你的AI生产力! -😊 更新步骤: -整合包:点击 update.bat 一键更新脚本 -代码构建:使用 git pull 拉去最新代码 ## ⚠️谨防被骗 📢 -_**1. NarratoAI 是一款完全免费的软件,近期在社交媒体(抖音等)上发现,有人将 NarratoAI 改名后售卖,下面是部分截图,切忌不要上当受骗!!!**_ +_**1. NarratoAI 是一款完全免费的软件,近期在社交媒体(抖音,B站等)上发现,有人将 NarratoAI 改名后售卖,下面是部分截图,请大家务必提高警惕,切勿上当受骗**_ -Screenshot_20250109_114131_Samsung Internet -Screenshot_20250109_114131_Samsung Internet -Screenshot_20250109_114131_Samsung Internet -Screenshot_20250109_114131_Samsung Internet +--- -_**2. 近期在 x (推特) 上发现有人冒充作者在 pump.fun 平台上发行代币! 这是骗子!!! 不要被割了韭菜 -!!!目前 NarratoAI 没有在 x(推特) 上做任何官方宣传,注意甄别**_ +
+ 诈骗截图 1 + 诈骗截图 2 + 诈骗截图 3 + 诈骗截图 4 +
-下面是此人 x(推特) 首页截图 - -Screenshot_20250109_114131_Samsung Internet +--- ## 未来计划 🥳 - [x] windows 整合包发布 From da27d8d8a19981ae6ec54a774de3756c22ddc6c3 Mon Sep 17 00:00:00 2001 From: linyq Date: Tue, 16 Sep 2025 23:23:58 +0800 Subject: [PATCH 4/6] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 75 +++++++-- config.example.toml | 2 +- project_version | 2 +- webui.txt | 376 -------------------------------------------- 4 files changed, 62 insertions(+), 393 deletions(-) delete mode 100644 webui.txt diff --git a/README.md b/README.md index 667e38e..b89bdca 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,6 @@ NarratoAI 是一个自动化影视解说工具,基于LLM实现文案撰写、 ![](docs/index-zh.png) -

视频审查界面

- -![](docs/check-zh.png) - ## 许可证 @@ -45,18 +41,29 @@ NarratoAI 是一个自动化影视解说工具,基于LLM实现文案撰写、 - 2024.11.10 发布新版本 v0.3.5;优化视频剪辑流程, ## 重磅福利 🎉 -即日起全面支持国产模型!注册即享2000万免费Token(价值14元平台配额),剪辑10分钟视频仅需0.1元! +> 1️⃣ +> **开发者专属福利:一站式AI平台,注册即送体验金!** +> +> 还在为接入各种AI模型烦恼吗?向您推荐 302.ai,一个企业级的AI资源中心。一次接入,即可调用上百种AI模型,涵盖语言、图像、音视频等,按量付费,极大降低开发成本。 +> +> 通过下方我的专属链接注册,**立获1美元免费体验金**,助您轻松开启AI开发之旅。 +> +> **立即注册领取:** [https://share.302.ai/I9P6mP](https://share.302.ai/I9P6mP) -🔥 快速领福利: -1️⃣ 点击链接注册:https://cloud.siliconflow.cn/i/pyOKqFCV -2️⃣ 使用手机号登录,**务必填写邀请码:pyOKqFCV** -3️⃣ 领取14元配额,极速体验高性价比AI剪辑 - -💡 小成本大创作: -硅基流动API Key一键接入,智能剪辑效率翻倍! -(注:邀请码为福利领取唯一凭证,注册后自动到账) - -立即行动,用「pyOKqFCV」解锁你的AI生产力! +--- +> 2️⃣ +> 即日起全面支持硅基流动!注册即享2000万免费Token(价值14元平台配额),剪辑10分钟视频仅需0.1元! +> +> 🔥 快速领福利: +> 1️⃣ 点击链接注册:https://cloud.siliconflow.cn/i/pyOKqFCV +> 2️⃣ 使用手机号登录,**务必填写邀请码:pyOKqFCV** +> 3️⃣ 领取14元配额,极速体验高性价比AI剪辑 +> +> 💡 小成本大创作: +> 硅基流动API Key一键接入,智能剪辑效率翻倍! +> (注:邀请码为福利领取唯一凭证,注册后自动到账) +> +> 立即行动,用「pyOKqFCV」解锁你的AI生产力! ## ⚠️谨防被骗 📢 @@ -90,6 +97,44 @@ _**1. NarratoAI 是一款完全免费的软件,近期在社交媒体(抖音,B - [ ] 支持更多 TTS 引擎 - [ ] ... +## 快速启动 🚀 + +### 方式一:macos Docker 部署(macos 推荐) +```bash +# 1. 克隆项目 +git clone https://github.com/linyqh/NarratoAI.git +cd NarratoAI + +# 2. 一键部署 +docker compose up -d + +# 3. 访问应用 +# 浏览器打开 http://localhost:8501 +``` +### 方式二:整合包(Windows 推荐) +> *关注微信公众号 **NarratoAI 助手** 右下角菜单栏获取下载链接* + +### 方式三:本地运行 +```bash +# 1. 克隆项目 +git clone https://github.com/linyqh/NarratoAI.git +cd NarratoAI + +# 2. 安装依赖 +pip install -r requirements.txt + +# 3. 复制配置文件 +cp config.example.toml config.toml + +# 4. 编辑 config.toml,配置你的 API 密钥 + +# 5. 启动应用 +streamlit run webui.py --server.maxUploadSize=2048 + +# 6. 访问应用 +# 浏览器打开 http://localhost:8501 +``` + ## 配置要求 📦 - 建议最低 CPU 4核或以上,内存 8G 或以上,显卡非必须 diff --git a/config.example.toml b/config.example.toml index feaa4ee..ba185fa 100644 --- a/config.example.toml +++ b/config.example.toml @@ -1,5 +1,5 @@ [app] - project_version="0.7.1" + project_version="0.7.2" # 模型验证模式配置 # true: 严格模式,只允许使用预定义支持列表中的模型(默认) diff --git a/project_version b/project_version index 7deb86f..d5cc44d 100644 --- a/project_version +++ b/project_version @@ -1 +1 @@ -0.7.1 \ No newline at end of file +0.7.2 \ No newline at end of file diff --git a/webui.txt b/webui.txt deleted file mode 100644 index b26a723..0000000 --- a/webui.txt +++ /dev/null @@ -1,376 +0,0 @@ -@echo off -set CURRENT_DIR=%CD% -echo ***** Current directory: %CURRENT_DIR% ***** -set PYTHONPATH=%CURRENT_DIR% - -set "vpn_proxy_url=%http://127.0.0.1:7890%" - -:: 使用VPN代理进行一些操作,例如通过代理下载文件 -set "http_proxy=%vpn_proxy_url%" -set "https_proxy=%vpn_proxy_url%" - -@echo off -setlocal enabledelayedexpansion - -rem 创建链接和路径的数组 -set "urls_paths[0]=https://zenodo.org/records/13293144/files/MicrosoftYaHeiBold.ttc|.\resource\fonts" -set "urls_paths[1]=https://zenodo.org/records/13293144/files/MicrosoftYaHeiNormal.ttc|.\resource\fonts" -set "urls_paths[2]=https://zenodo.org/records/13293144/files/STHeitiLight.ttc|.\resource\fonts" -set "urls_paths[3]=https://zenodo.org/records/13293144/files/STHeitiMedium.ttc|.\resource\fonts" -set "urls_paths[4]=https://zenodo.org/records/13293144/files/UTM%20Kabel%20KT.ttf|.\resource\fonts" -set "urls_paths[5]=https://zenodo.org/records/14167125/files/test.mp4|.\resource\videos" -set "urls_paths[6]=https://zenodo.org/records/13293150/files/output000.mp3|.\resource\songs" -set "urls_paths[7]=https://zenodo.org/records/13293150/files/output001.mp3|.\resource\songs" -set "urls_paths[8]=https://zenodo.org/records/13293150/files/output002.mp3|.\resource\songs" -set "urls_paths[9]=https://zenodo.org/records/13293150/files/output003.mp3|.\resource\songs" -set "urls_paths[10]=https://zenodo.org/records/13293150/files/output004.mp3|.\resource\songs" -set "urls_paths[11]=https://zenodo.org/records/13293150/files/output005.mp3|.\resource\songs" -set "urls_paths[12]=https://zenodo.org/records/13293150/files/output006.mp3|.\resource\songs" -set "urls_paths[13]=https://zenodo.org/records/13293150/files/output007.mp3|.\resource\songs" -set "urls_paths[14]=https://zenodo.org/records/13293150/files/output008.mp3|.\resource\songs" -set "urls_paths[15]=https://zenodo.org/records/13293150/files/output009.mp3|.\resource\songs" -set "urls_paths[16]=https://zenodo.org/records/13293150/files/output010.mp3|.\resource\songs" - -rem 循环下载所有文件并保存到指定路径 -for /L %%i in (0,1,16) do ( - for /f "tokens=1,2 delims=|" %%a in ("!urls_paths[%%i]!") do ( - if not exist "%%b" mkdir "%%b" - echo 正在下载 %%a 到 %%b - curl -o "%%b\%%~nxa" %%a - ) -) - -echo 所有文件已成功下载到指定目录 -endlocal -pause - - -rem set HF_ENDPOINT=https://hf-mirror.com -streamlit run webui.py --browser.serverAddress="127.0.0.1" --server.enableCORS=True --server.maxUploadSize=2048 --browser.gatherUsageStats=False - -streamlit run webui.py --server.maxUploadSize=2048 - -请求0: -curl -X 'POST' \ - 'http://127.0.0.1:8080/api/v2/youtube/download' \ - -H 'accept: application/json' \ - -H 'Content-Type: application/json' \ - -d '{ - "url": "https://www.youtube.com/watch?v=Kenm35gdqtk", - "resolution": "1080p", - "output_format": "mp4", - "rename": "2024-11-19" -}' -{ - "url": "https://www.youtube.com/watch?v=Kenm35gdqtk", - "resolution": "1080p", - "output_format": "mp4", - "rename": "2024-11-19" -} - -请求1: -curl -X 'POST' \ - 'http://127.0.0.1:8080/api/v2/scripts/generate' \ - -H 'accept: application/json' \ - -H 'Content-Type: application/json' \ - -d '{ - "video_path": "E:\\projects\\NarratoAI\\resource\\videos\\test.mp4", - "skip_seconds": 0, - "threshold": 30, - "vision_batch_size": 10, - "vision_llm_provider": "gemini" -}' -{ - "video_path": "E:\\projects\\NarratoAI\\resource\\videos\\test.mp4", - "skip_seconds": 0, - "threshold": 30, - "vision_batch_size": 10, - "vision_llm_provider": "gemini" -} - -请求2: -curl -X 'POST' \ - 'http://127.0.0.1:8080/api/v2/scripts/crop' \ - -H 'accept: application/json' \ - -H 'Content-Type: application/json' \ - -d '{ - "video_origin_path": "E:\\projects\\NarratoAI\\resource\\videos\\test.mp4", - "video_script": [ - { - "timestamp": "00:10-01:01", - "picture": "好的,以下是视频画面的客观描述:\n\n视频展现一名留着胡须的男子在森林里挖掘。\n\n画面首先展现男子从后方视角,背着军绿色背包,穿着卡其色长裤和深色T恤,走向一个泥土斜坡。背包上似乎有一个镐头。\n\n下一个镜头特写展现了该背包,一个镐头从背包里伸出来,包里还有一些其他工具。\n\n然后,视频显示该男子用镐头挖掘泥土斜坡。\n\n接下来是一些近景镜头,展现男子的靴子在泥土中行走,以及男子用手清理泥土。\n\n其他镜头从不同角度展现该男子在挖掘,包括从侧面和上方。\n\n可以看到他用工具挖掘,清理泥土,并检查挖出的土壤。\n\n最后,一个镜头展现了挖出的土壤的质地和颜色。", - "narration": "好的,接下来就是我们这位“胡须大侠”的精彩冒险了!只见他背着军绿色的背包,迈着比我上班还不情愿的步伐走向那泥土斜坡。哎呀,这个背包可真是个宝贝,里面藏着一把镐头和一些工具,简直像是个随身携带的“建筑工具箱”! \n\n看他挥舞着镐头,挖掘泥土的姿势,仿佛在进行一场“挖土大赛”,结果却比我做饭还要糟糕。泥土飞扬中,他的靴子也成了“泥巴艺术家”。最后,那堆色泽各异的土壤就像他心情的写照——五彩斑斓又略显混乱!真是一次让人捧腹的建造之旅!", - "OST": 2, - "new_timestamp": "00:00-00:51" - }, - { - "timestamp": "01:07-01:53", - "picture": "好的,以下是视频画面的客观描述:\n\n视频以一系列森林环境的镜头开头。\n\n第一个镜头是一个特写镜头,镜头中显示的是一些带有水滴的绿色叶子。\n\n第二个镜头显示一个留着胡须的男子在森林中挖掘一个洞。 他跪在地上,用工具挖土。\n\n第三个镜头是一个中等镜头,显示同一个人坐在他挖好的洞边休息。\n\n第四个镜头显示该洞的内部结构,该洞在树根和地面之间。\n\n第五个镜头显示该男子用斧头砍树枝。\n\n第六个镜头显示一堆树枝横跨一个泥泞的小水坑。\n\n第七个镜头显示更多茂盛的树叶和树枝在阳光下。\n\n第八个镜头显示更多茂盛的树叶和树枝。\n\n\n", - "narration": "接下来,我们的“挖土大师”又开始了他的森林探险。看这镜头,水滴在叶子上闪烁,仿佛在说:“快来,快来,这里有故事!”他一边挖洞,一边像个新手厨师试图切洋葱——每一下都小心翼翼,生怕自己不小心挖出个“历史遗址”。坐下休息的时候,脸上的表情就像发现新大陆一样!然后,他拿起斧头砍树枝,简直是现代版的“神雕侠侣”,只不过对象是树木。最后,那堆树枝架过泥泞的小水坑,仿佛在说:“我就是不怕湿脚的勇士!”这就是我们的建造之旅!", - "OST": 2, - "new_timestamp": "00:51-01:37" - } - ] -}' -{ - "video_origin_path": "E:\\projects\\NarratoAI\\resource\\videos\\test.mp4", - "video_script": [ - { - "timestamp": "00:10-01:01", - "picture": "好的,以下是视频画面的客观描述:\n\n视频展现一名留着胡须的男子在森林里挖掘。\n\n画面首先展现男子从后方视角,背着军绿色背包,穿着卡其色长裤和深色T恤,走向一个泥土斜坡。背包上似乎有一个镐头。\n\n下一个镜头特写展现了该背包,一个镐头从背包里伸出来,包里还有一些其他工具。\n\n然后,视频显示该男子用镐头挖掘泥土斜坡。\n\n接下来是一些近景镜头,展现男子的靴子在泥土中行走,以及男子用手清理泥土。\n\n其他镜头从不同角度展现该男子在挖掘,包括从侧面和上方。\n\n可以看到他用工具挖掘,清理泥土,并检查挖出的土壤。\n\n最后,一个镜头展现了挖出的土壤的质地和颜色。", - "narration": "好的,接下来就是我们这位“胡须大侠”的精彩冒险了!只见他背着军绿色的背包,迈着比我上班还不情愿的步伐走向那泥土斜坡。哎呀,这个背包可真是个宝贝,里面藏着一把镐头和一些工具,简直像是个随身携带的“建筑工具箱”! \n\n看他挥舞着镐头,挖掘泥土的姿势,仿佛在进行一场“挖土大赛”,结果却比我做饭还要糟糕。泥土飞扬中,他的靴子也成了“泥巴艺术家”。最后,那堆色泽各异的土壤就像他心情的写照——五彩斑斓又略显混乱!真是一次让人捧腹的建造之旅!", - "OST": 2, - "new_timestamp": "00:00-00:51" - }, - { - "timestamp": "01:07-01:53", - "picture": "好的,以下是视频画面的客观描述:\n\n视频以一系列森林环境的镜头开头。\n\n第一个镜头是一个特写镜头,镜头中显示的是一些带有水滴的绿色叶子。\n\n第二个镜头显示一个留着胡须的男子在森林中挖掘一个洞。 他跪在地上,用工具挖土。\n\n第三个镜头是一个中等镜头,显示同一个人坐在他挖好的洞边休息。\n\n第四个镜头显示该洞的内部结构,该洞在树根和地面之间。\n\n第五个镜头显示该男子用斧头砍树枝。\n\n第六个镜头显示一堆树枝横跨一个泥泞的小水坑。\n\n第七个镜头显示更多茂盛的树叶和树枝在阳光下。\n\n第八个镜头显示更多茂盛的树叶和树枝。\n\n\n", - "narration": "接下来,我们的“挖土大师”又开始了他的森林探险。看这镜头,水滴在叶子上闪烁,仿佛在说:“快来,快来,这里有故事!”他一边挖洞,一边像个新手厨师试图切洋葱——每一下都小心翼翼,生怕自己不小心挖出个“历史遗址”。坐下休息的时候,脸上的表情就像发现新大陆一样!然后,他拿起斧头砍树枝,简直是现代版的“神雕侠侣”,只不过对象是树木。最后,那堆树枝架过泥泞的小水坑,仿佛在说:“我就是不怕湿脚的勇士!”这就是我们的建造之旅!", - "OST": 2, - "new_timestamp": "00:51-01:37" - } - ] -} - -请求3: -curl -X 'POST' \ - 'http://127.0.0.1:8080/api/v2/scripts/start-subclip?task_id=12121' \ - -H 'accept: application/json' \ - -H 'Content-Type: application/json' \ - -d '{ - "request": { - "video_clip_json": [ - { - "timestamp": "00:10-01:01", - "picture": "好的,以下是视频画面的客观描述:\n\n视频展现一名留着胡须的男子在森林里挖掘。\n\n画面首先展现男子从后方视角,背着军绿色背包,穿着卡其色长裤和深色T恤,走向一个泥土斜坡。背包上似乎有一个镐头。\n\n下一个镜头特写展现了该背包,一个镐头从背包里伸出来,包里还有一些其他工具。\n\n然后,视频显示该男子用镐头挖掘泥土斜坡。\n\n接下来是一些近景镜头,展现男子的靴子在泥土中行走,以及男子用手清理泥土。\n\n其他镜头从不同角度展现该男子在挖掘,包括从侧面和上方。\n\n可以看到他用工具挖掘,清理泥土,并检查挖出的土壤。\n\n最后,一个镜头展现了挖出的土壤的质地和颜色。", - "narration": "好的,接下来就是我们这位“胡须大侠”的精彩冒险了!只见他背着军绿色的背包,迈着比我上班还不情愿的步伐走向那泥土斜坡。哎呀,这个背包可真是个宝贝,里面藏着一把镐头和一些工具,简直像是个随身携带的“建筑工具箱”! \n\n看他挥舞着镐头,挖掘泥土的姿势,仿佛在进行一场“挖土大赛”,结果却比我做饭还要糟糕。泥土飞扬中,他的靴子也成了“泥巴艺术家”。最后,那堆色泽各异的土壤就像他心情的写照——五彩斑斓又略显混乱!真是一次让人捧腹的建造之旅!", - "OST": 2, - "new_timestamp": "00:00-00:51" - }, - { - "timestamp": "01:07-01:53", - "picture": "好的,以下是视频画面的客观描述:\n\n视频以一系列森林环境的镜头开头。\n\n第一个镜头是一个特写镜头,镜头中显示的是一些带有水滴的绿色叶子。\n\n第二个镜头显示一个留着胡须的男子在森林中挖掘一个洞。 他跪在地上,用工具挖土。\n\n第三个镜头是一个中等镜头,显示同一个人坐在他挖好的洞边休息。\n\n第四个镜头显示该洞的内部结构,该洞在树根和地面之间。\n\n第五个镜头显示该男子用斧头砍树枝。\n\n第六个镜头显示一堆树枝横跨一个泥泞的小水坑。\n\n第七个镜头显示更多茂盛的树叶和树枝在阳光下。\n\n第八个镜头显示更多茂盛的树叶和树枝。\n\n\n", - "narration": "接下来,我们的“挖土大师”又开始了他的森林探险。看这镜头,水滴在叶子上闪烁,仿佛在说:“快来,快来,这里有故事!”他一边挖洞,一边像个新手厨师试图切洋葱——每一下都小心翼翼,生怕自己不小心挖出个“历史遗址”。坐下休息的时候,脸上的表情就像发现新大陆一样!然后,他拿起斧头砍树枝,简直是现代版的“神雕侠侣”,只不过对象是树木。最后,那堆树枝架过泥泞的小水坑,仿佛在说:“我就是不怕湿脚的勇士!”这就是我们的建造之旅!", - "OST": 2, - "new_timestamp": "00:51-01:37" - } - ], - "video_clip_json_path": "E:\\projects\\NarratoAI\\resource\\scripts\\2024-1118-230421.json", - "video_origin_path": "E:\\projects\\NarratoAI\\resource\\videos\\test.mp4", - "video_aspect": "16:9", - "video_language": "zh-CN", - "voice_name": "zh-CN-YunjianNeural", - "voice_volume": 1, - "voice_rate": 1.2, - "voice_pitch": 1, - "bgm_name": "random", - "bgm_type": "random", - "bgm_file": "", - "bgm_volume": 0.3, - "subtitle_enabled": true, - "subtitle_position": "bottom", - "font_name": "STHeitiMedium.ttc", - "text_fore_color": "#FFFFFF", - "text_background_color": "transparent", - "font_size": 75, - "stroke_color": "#000000", - "stroke_width": 1.5, - "custom_position": 70, - "n_threads": 8 - }, - "subclip_videos": { - "00:10-01:01": "E:\\projects\\NarratoAI\\storage\\cache_videos/vid-00_10-01_01.mp4", - "01:07-01:53": "E:\\projects\\NarratoAI\\storage\\cache_videos/vid-01_07-01_53.mp4" - } -}' -{ - "request": { - "video_clip_json": [ - { - "timestamp": "00:10-01:01", - "picture": "好的,以下是视频画面的客观描述:\n\n视频展现一名留着胡须的男子在森林里挖掘。\n\n画面首先展现男子从后方视角,背着军绿色背包,穿着卡其色长裤和深色T恤,走向一个泥土斜坡。背包上似乎有一个镐头。\n\n下一个镜头特写展现了该背包,一个镐头从背包里伸出来,包里还有一些其他工具。\n\n然后,视频显示该男子用镐头挖掘泥土斜坡。\n\n接下来是一些近景镜头,展现男子的靴子在泥土中行走,以及男子用手清理泥土。\n\n其他镜头从不同角度展现该男子在挖掘,包括从侧面和上方。\n\n可以看到他用工具挖掘,清理泥土,并检查挖出的土壤。\n\n最后,一个镜头展现了挖出的土壤的质地和颜色。", - "narration": "好的,接下来就是我们这位“胡须大侠”的精彩冒险了!只见他背着军绿色的背包,迈着比我上班还不情愿的步伐走向那泥土斜坡。哎呀,这个背包可真是个宝贝,里面藏着一把镐头和一些工具,简直像是个随身携带的“建筑工具箱”! \n\n看他挥舞着镐头,挖掘泥土的姿势,仿佛在进行一场“挖土大赛”,结果却比我做饭还要糟糕。泥土飞扬中,他的靴子也成了“泥巴艺术家”。最后,那堆色泽各异的土壤就像他心情的写照——五彩斑斓又略显混乱!真是一次让人捧腹的建造之旅!", - "OST": 2, - "new_timestamp": "00:00-00:51" - }, - { - "timestamp": "01:07-01:53", - "picture": "好的,以下是视频画面的客观描述:\n\n视频以一系列森林环境的镜头开头。\n\n第一个镜头是一个特写镜头,镜头中显示的是一些带有水滴的绿色叶子。\n\n第二个镜头显示一个留着胡须的男子在森林中挖掘一个洞。 他跪在地上,用工具挖土。\n\n第三个镜头是一个中等镜头,显示同一个人坐在他挖好的洞边休息。\n\n第四个镜头显示该洞的内部结构,该洞在树根和地面之间。\n\n第五个镜头显示该男子用斧头砍树枝。\n\n第六个镜头显示一堆树枝横跨一个泥泞的小水坑。\n\n第七个镜头显示更多茂盛的树叶和树枝在阳光下。\n\n第八个镜头显示更多茂盛的树叶和树枝。\n\n\n", - "narration": "接下来,我们的“挖土大师”又开始了他的森林探险。看这镜头,水滴在叶子上闪烁,仿佛在说:“快来,快来,这里有故事!”他一边挖洞,一边像个新手厨师试图切洋葱——每一下都小心翼翼,生怕自己不小心挖出个“历史遗址”。坐下休息的时候,脸上的表情就像发现新大陆一样!然后,他拿起斧头砍树枝,简直是现代版的“神雕侠侣”,只不过对象是树木。最后,那堆树枝架过泥泞的小水坑,仿佛在说:“我就是不怕湿脚的勇士!”这就是我们的建造之旅!", - "OST": 2, - "new_timestamp": "00:51-01:37" - } - ], - "video_clip_json_path": "E:\\projects\\NarratoAI\\resource\\scripts\\2024-1118-230421.json", - "video_origin_path": "E:\\projects\\NarratoAI\\resource\\videos\\test.mp4", - "video_aspect": "16:9", - "video_language": "zh-CN", - "voice_name": "zh-CN-YunjianNeural", - "voice_volume": 1, - "voice_rate": 1.2, - "voice_pitch": 1, - "bgm_name": "random", - "bgm_type": "random", - "bgm_file": "", - "bgm_volume": 0.3, - "subtitle_enabled": true, - "subtitle_position": "bottom", - "font_name": "STHeitiMedium.ttc", - "text_fore_color": "#FFFFFF", - "text_background_color": "transparent", - "font_size": 75, - "stroke_color": "#000000", - "stroke_width": 1.5, - "custom_position": 70, - "n_threads": 8 - }, - "subclip_videos": { - "00:10-01:01": "E:\\projects\\NarratoAI\\storage\\cache_videos/vid-00_10-01_01.mp4", - "01:07-01:53": "E:\\projects\\NarratoAI\\storage\\cache_videos/vid-01_07-01_53.mp4" - } -} - - -请在最外层新建一个pipeline 工作流执行逻辑的代码; -他会按照下面的顺序请求接口 -1.下载视频 -curl -X 'POST' \ - 'http://127.0.0.1:8080/api/v2/youtube/download' \ - -H 'accept: application/json' \ - -H 'Content-Type: application/json' \ - -d '{ - "url": "https://www.youtube.com/watch?v=Kenm35gdqtk", - "resolution": "1080p", - "output_format": "mp4", - "rename": "2024-11-19" -}' -2.生成脚本 -curl -X 'POST' \ - 'http://127.0.0.1:8080/api/v2/scripts/generate' \ - -H 'accept: application/json' \ - -H 'Content-Type: application/json' \ - -d '{ - "video_path": "E:\\projects\\NarratoAI\\resource\\videos\\test.mp4", - "skip_seconds": 0, - "threshold": 30, - "vision_batch_size": 10, - "vision_llm_provider": "gemini" -}' -3. 剪辑视频 -curl -X 'POST' \ - 'http://127.0.0.1:8080/api/v2/scripts/crop' \ - -H 'accept: application/json' \ - -H 'Content-Type: application/json' \ - -d '{ - "video_origin_path": "E:\\projects\\NarratoAI\\resource\\videos\\test.mp4", - "video_script": [ - { - "timestamp": "00:10-01:01", - "picture": "好的,以下是视频画面的客观描述:\n\n视频展现一名留着胡须的男子在森林里挖掘。\n\n画面首先展现男子从后方视角,背着军绿色背包,穿着卡其色长裤和深色T恤,走向一个泥土斜坡。背包上似乎有一个镐头。\n\n下一个镜头特写展现了该背包,一个镐头从背包里伸出来,包里还有一些其他工具。\n\n然后,视频显示该男子用镐头挖掘泥土斜坡。\n\n接下来是一些近景镜头,展现男子的靴子在泥土中行走,以及男子用手清理泥土。\n\n其他镜头从不同角度展现该男子在挖掘,包括从侧面和上方。\n\n可以看到他用工具挖掘,清理泥土,并检查挖出的土壤。\n\n最后,一个镜头展现了挖出的土壤的质地和颜色。", - "narration": "好的,接下来就是我们这位“胡须大侠”的精彩冒险了!只见他背着军绿色的背包,迈着比我上班还不情愿的步伐走向那泥土斜坡。哎呀,这个背包可真是个宝贝,里面藏着一把镐头和一些工具,简直像是个随身携带的“建筑工具箱”! \n\n看他挥舞着镐头,挖掘泥土的姿势,仿佛在进行一场“挖土大赛”,结果却比我做饭还要糟糕。泥土飞扬中,他的靴子也成了“泥巴艺术家”。最后,那堆色泽各异的土壤就像他心情的写照——五彩斑斓又略显混乱!真是一次让人捧腹的建造之旅!", - "OST": 2, - "new_timestamp": "00:00-00:51" - }, - { - "timestamp": "01:07-01:53", - "picture": "好的,以下是视频画面的客观描述:\n\n视频以一系列森林环境的镜头开头。\n\n第一个镜头是一个特写镜头,镜头中显示的是一些带有水滴的绿色叶子。\n\n第二个镜头显示一个留着胡须的男子在森林中挖掘一个洞。 他跪在地上,用工具挖土。\n\n第三个镜头是一个中等镜头,显示同一个人坐在他挖好的洞边休息。\n\n第四个镜头显示该洞的内部结构,该洞在树根和地面之间。\n\n第五个镜头显示该男子用斧头砍树枝。\n\n第六个镜头显示一堆树枝横跨一个泥泞的小水坑。\n\n第七个镜头显示更多茂盛的树叶和树枝在阳光下。\n\n第八个镜头显示更多茂盛的树叶和树枝。\n\n\n", - "narration": "接下来,我们的“挖土大师”又开始了他的森林探险。看这镜头,水滴在叶子上闪烁,仿佛在说:“快来,快来,这里有故事!”他一边挖洞,一边像个新手厨师试图切洋葱——每一下都小心翼翼,生怕自己不小心挖出个“历史遗址”。坐下休息的时候,脸上的表情就像发现新大陆一样!然后,他拿起斧头砍树枝,简直是现代版的“神雕侠侣”,只不过对象是树木。最后,那堆树枝架过泥泞的小水坑,仿佛在说:“我就是不怕湿脚的勇士!”这就是我们的建造之旅!", - "OST": 2, - "new_timestamp": "00:51-01:37" - } - ] -}' -4.生成视频 -curl -X 'POST' \ - 'http://127.0.0.1:8080/api/v2/scripts/start-subclip?task_id=12121' \ - -H 'accept: application/json' \ - -H 'Content-Type: application/json' \ - -d '{ - "request": { - "video_clip_json": [ - { - "timestamp": "00:10-01:01", - "picture": "好的,以下是视频画面的客观描述:\n\n视频展现一名留着胡须的男子在森林里挖掘。\n\n画面首先展现男子从后方视角,背着军绿色背包,穿着卡其色长裤和深色T恤,走向一个泥土斜坡。背包上似乎有一个镐头。\n\n下一个镜头特写展现了该背包,一个镐头从背包里伸出来,包里还有一些其他工具。\n\n然后,视频显示该男子用镐头挖掘泥土斜坡。\n\n接下来是一些近景镜头,展现男子的靴子在泥土中行走,以及男子用手清理泥土。\n\n其他镜头从不同角度展现该男子在挖掘,包括从侧面和上方。\n\n可以看到他用工具挖掘,清理泥土,并检查挖出的土壤。\n\n最后,一个镜头展现了挖出的土壤的质地和颜色。", - "narration": "好的,接下来就是我们这位“胡须大侠”的精彩冒险了!只见他背着军绿色的背包,迈着比我上班还不情愿的步伐走向那泥土斜坡。哎呀,这个背包可真是个宝贝,里面藏着一把镐头和一些工具,简直像是个随身携带的“建筑工具箱”! \n\n看他挥舞着镐头,挖掘泥土的姿势,仿佛在进行一场“挖土大赛”,结果却比我做饭还要糟糕。泥土飞扬中,他的靴子也成了“泥巴艺术家”。最后,那堆色泽各异的土壤就像他心情的写照——五彩斑斓又略显混乱!真是一次让人捧腹的建造之旅!", - "OST": 2, - "new_timestamp": "00:00-00:51" - }, - { - "timestamp": "01:07-01:53", - "picture": "好的,以下是视频画面的客观描述:\n\n视频以一系列森林环境的镜头开头。\n\n第一个镜头是一个特写镜头,镜头中显示的是一些带有水滴的绿色叶子。\n\n第二个镜头显示一个留着胡须的男子在森林中挖掘一个洞。 他跪在地上,用工具挖土。\n\n第三个镜头是一个中等镜头,显示同一个人坐在他挖好的洞边休息。\n\n第四个镜头显示该洞的内部结构,该洞在树根和地面之间。\n\n第五个镜头显示该男子用斧头砍树枝。\n\n第六个镜头显示一堆树枝横跨一个泥泞的小水坑。\n\n第七个镜头显示更多茂盛的树叶和树枝在阳光下。\n\n第八个镜头显示更多茂盛的树叶和树枝。\n\n\n", - "narration": "接下来,我们的“挖土大师”又开始了他的森林探险。看这镜头,水滴在叶子上闪烁,仿佛在说:“快来,快来,这里有故事!”他一边挖洞,一边像个新手厨师试图切洋葱——每一下都小心翼翼,生怕自己不小心挖出个“历史遗址”。坐下休息的时候,脸上的表情就像发现新大陆一样!然后,他拿起斧头砍树枝,简直是现代版的“神雕侠侣”,只不过对象是树木。最后,那堆树枝架过泥泞的小水坑,仿佛在说:“我就是不怕湿脚的勇士!”这就是我们的建造之旅!", - "OST": 2, - "new_timestamp": "00:51-01:37" - } - ], - "video_clip_json_path": "E:\\projects\\NarratoAI\\resource\\scripts\\2024-1118-230421.json", - "video_origin_path": "E:\\projects\\NarratoAI\\resource\\videos\\test.mp4", - "video_aspect": "16:9", - "video_language": "zh-CN", - "voice_name": "zh-CN-YunjianNeural", - "voice_volume": 1, - "voice_rate": 1.2, - "voice_pitch": 1, - "bgm_name": "random", - "bgm_type": "random", - "bgm_file": "", - "bgm_volume": 0.3, - "subtitle_enabled": true, - "subtitle_position": "bottom", - "font_name": "STHeitiMedium.ttc", - "text_fore_color": "#FFFFFF", - "text_background_color": "transparent", - "font_size": 75, - "stroke_color": "#000000", - "stroke_width": 1.5, - "custom_position": 70, - "n_threads": 8 - }, - "subclip_videos": { - "00:10-01:01": "E:\\projects\\NarratoAI\\storage\\cache_videos/vid-00_10-01_01.mp4", - "01:07-01:53": "E:\\projects\\NarratoAI\\storage\\cache_videos/vid-01_07-01_53.mp4" - } -}' - -请求1,返回的参数是: -{ - "task_id": "4e9b575f-68c0-4ae1-b218-db42b67993d0", - "output_path": "E:\\projects\\NarratoAI\\resource\\videos\\2024-11-19.mp4", - "resolution": "1080p", - "format": "mp4", - "filename": "2024-11-19.mp4" -} -output_path需要传递给请求2 -请求2,返回数据为: -{ - "task_id": "04497017-953c-44b4-bf1d-9d8ed3ebbbce", - "script": [ - { - "timestamp": "00:10-01:01", - "picture": "好的,以下是對影片畫面的客觀描述:\n\n影片顯示一名留著鬍鬚的男子在一處樹林茂密的斜坡上挖掘。\n\n畫面一:男子從後方出現,背著一個軍綠色的背包,背包裡似乎裝有工具。他穿著卡其色的長褲和深色的登山鞋。\n\n畫面二:特寫鏡頭顯示男子的背包,一個舊的鎬頭從包裡露出來,包裡還有其他工具,包括一個鏟子。\n\n畫面三:男子用鎬頭在斜坡上挖土,背包放在他旁邊。\n\n畫面四:特寫鏡頭顯示男子的登山鞋在泥土中。\n\n畫面五:男子坐在斜坡上,用手清理樹根和泥土。\n\n畫面六:地上有一些鬆動的泥土和落葉。\n\n畫面七:男子的背包近景鏡頭,他正在挖掘。\n\n畫面八:男子在斜坡上挖掘,揚起一陣塵土。\n\n畫面九:特寫鏡頭顯示男子用手清理泥土。\n\n畫面十:特寫鏡頭顯示挖出的泥土剖面,可以看到土壤的層次。", - "narration": "上一个画面是我在绝美的自然中,准备开启我的“土豪”挖掘之旅。现在,你们看到这位留着胡子的“大哥”,他背着个军绿色的包,里面装的可不仅仅是工具,还有我对生活的无限热爱(以及一丝不安)。看!这把旧镐头就像我的前任——用起来费劲,但又舍不得扔掉。\n\n他在斜坡上挖土,泥土飞扬,仿佛在跟大地进行一场“泥巴大战”。每一铲下去,都能听到大地微微的呻吟:哎呀,我这颗小树根可比我当年的情感纠葛还难处理呢!别担心,这些泥土层次分明,简直可以开个“泥土博物馆”。所以,朋友们,跟着我一起享受这场泥泞中的乐趣吧!", - "OST": 2, - "new_timestamp": "00:00-00:51" - }, - { - "timestamp": "01:07-01:53", - "picture": "好的,以下是對影片畫面內容的客觀描述:\n\n影片以一系列森林環境的鏡頭開始。第一個鏡頭展示了綠葉植物的特寫鏡頭,葉子上有一些水珠。接下來的鏡頭是一個男人在森林裡挖掘一個小坑,他跪在地上,用鏟子挖土。\n\n接下來的鏡頭是同一個男人坐在他挖的坑旁邊,望著前方。然後,鏡頭顯示該坑的廣角鏡頭,顯示其結構和大小。\n\n之後的鏡頭,同一個男人在樹林裡劈柴。鏡頭最後呈現出一潭渾濁的水,周圍環繞著樹枝。然後鏡頭又回到了森林裡生長茂盛的植物特寫鏡頭。", - "narration": "好嘞,朋友们,我们已经在泥土博物馆里捣鼓了一阵子,现在是时候跟大自然亲密接触了!看看这片森林,绿叶上水珠闪闪发光,就像我曾经的爱情,虽然短暂,却美得让人心碎。\n\n现在,我在这里挖个小坑,感觉自己就像是一位新晋“挖土大王”,不过说实话,这手艺真不敢恭维,连铲子都快对我崩溃了。再说劈柴,这动作简直比我前任的情绪波动还要激烈!最后这一潭浑浊的水,别担心,它只是告诉我:生活就像这水,总有些杂质,但也别忘了,要勇敢面对哦!", - "OST": 2, - "new_timestamp": "00:51-01:37" - } - ] -} -output_path和script参数需要传递给请求3 -请求3返回参数是 -{ - "task_id": "b6f5a98a-b2e0-4e3d-89c5-64fb90db2ec1", - "subclip_videos": { - "00:10-01:01": "E:\\projects\\NarratoAI\\storage\\cache_videos/vid-00_10-01_01.mp4", - "01:07-01:53": "E:\\projects\\NarratoAI\\storage\\cache_videos/vid-01_07-01_53.mp4" - } -} -subclip_videos和 output_path和script参数需要传递给请求4 -最后完成工作流 - -0代表只播放文案音频,禁用视频原声;1代表只播放视频原声,不需要播放文案音频和字幕;2代表即播放文案音频也要播放视频原声; \ No newline at end of file From a1474bed029c4b971c0dd6300aa0fcfa213170a5 Mon Sep 17 00:00:00 2001 From: Emily-LMH <11679731+Emily-LMH@user.noreply.gitee.com> Date: Tue, 16 Sep 2025 14:40:08 +0800 Subject: [PATCH 5/6] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=85=BE=E8=AE=AF?= =?UTF-8?q?=E4=BA=91=20TTS=20=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 38 +++---- app/config/config.py | 2 + app/models/schema.py | 2 +- app/services/clip_video.py | 1 - app/services/llm/providers/__init__.py | 2 +- app/services/task.py | 2 + app/services/voice.py | 149 ++++++++++++++++++++++--- config.example.toml | 10 +- docker-entrypoint.sh | 58 ++++++++++ requirements.txt | 1 + webui/components/audio_settings.py | 127 ++++++++++++++++++++- 11 files changed, 348 insertions(+), 44 deletions(-) diff --git a/Dockerfile b/Dockerfile index ee70617..fc0f316 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,10 +22,9 @@ RUN python -m pip install --upgrade pip setuptools wheel && \ # 激活虚拟环境 ENV PATH="/opt/venv/bin:$PATH" -# 复制 requirements.txt 并安装 Python 依赖 +# 复制 requirements.txt 并使用镜像安装 Python 依赖 COPY requirements.txt . -RUN pip install --no-cache-dir --upgrade pip && \ - pip install --no-cache-dir -r requirements.txt +RUN pip install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt # 运行阶段 FROM python:3.12-slim-bookworm @@ -48,7 +47,7 @@ ENV PATH="/opt/venv/bin:$PATH" \ LANG=C.UTF-8 \ LC_ALL=C.UTF-8 -# 安装运行时系统依赖 +# 一次性安装所有依赖、创建用户、配置系统,减少层级 RUN apt-get update && apt-get install -y --no-install-recommends \ imagemagick \ ffmpeg \ @@ -56,32 +55,25 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ curl \ git-lfs \ ca-certificates \ + dos2unix \ + && sed -i 's/ tuple: """ 解析时间戳字符串,返回开始和结束时间 diff --git a/app/services/llm/providers/__init__.py b/app/services/llm/providers/__init__.py index ea1509d..16b764d 100644 --- a/app/services/llm/providers/__init__.py +++ b/app/services/llm/providers/__init__.py @@ -43,5 +43,5 @@ __all__ = [ 'QwenTextProvider', 'DeepSeekTextProvider', 'SiliconflowVisionProvider', - 'SiliconflowTextProvider' + 'SiliconflowTextProvider', ] diff --git a/app/services/task.py b/app/services/task.py index 3914df5..c3702af 100644 --- a/app/services/task.py +++ b/app/services/task.py @@ -73,6 +73,7 @@ def start_subclip(task_id: str, params: VideoClipParams, subclip_path_videos: di tts_results = voice.tts_multiple( task_id=task_id, list_script=tts_segments, # 只传入需要TTS的片段 + tts_engine=params.tts_engine, voice_name=params.voice_name, voice_rate=params.voice_rate, voice_pitch=params.voice_pitch, @@ -317,6 +318,7 @@ def start_subclip_unified(task_id: str, params: VideoClipParams): tts_results = voice.tts_multiple( task_id=task_id, list_script=tts_segments, # 只传入需要TTS的片段 + tts_engine=params.tts_engine, voice_name=params.voice_name, voice_rate=params.voice_rate, voice_pitch=params.voice_pitch, diff --git a/app/services/voice.py b/app/services/voice.py index 76a7f88..a114534 100644 --- a/app/services/voice.py +++ b/app/services/voice.py @@ -5,6 +5,7 @@ import traceback import edge_tts import asyncio import requests +import uuid from loguru import logger from typing import List, Union, Tuple from datetime import datetime @@ -1080,17 +1081,27 @@ def should_use_azure_speech_services(voice_name: str) -> bool: def tts( - text: str, voice_name: str, voice_rate: float, voice_pitch: float, voice_file: str + text: str, voice_name: str, voice_rate: float, voice_pitch: float, voice_file: str, tts_engine: str = "azure" ) -> Union[SubMaker, None]: - # 检查是否为 SoulVoice 引擎 - if is_soulvoice_voice(voice_name): + logger.info(f"使用 TTS 引擎: '{tts_engine}', 语音: '{voice_name}'") + + if tts_engine == "tencent": + logger.info("分发到腾讯云 TTS") + return tencent_tts(text, voice_name, voice_file, speed=voice_rate) + + if tts_engine == "soulvoice": + logger.info("分发到 SoulVoice TTS") return soulvoice_tts(text, voice_name, voice_file, speed=voice_rate) - # 检查是否应该使用 Azure Speech Services - if should_use_azure_speech_services(voice_name): - return azure_tts_v2(text, voice_name, voice_file) + if tts_engine == "azure": + if should_use_azure_speech_services(voice_name): + logger.info("分发到 Azure Speech Services (V2)") + return azure_tts_v2(text, voice_name, voice_file) + logger.info("分发到 Edge TTS (Azure V1)") + return azure_tts_v1(text, voice_name, voice_rate, voice_pitch, voice_file) - # 默认使用 Edge TTS (Azure V1) + # Fallback for unknown engine - default to azure v1 + logger.warning(f"未知的 TTS 引擎: '{tts_engine}', 将默认使用 Edge TTS (Azure V1)。") return azure_tts_v1(text, voice_name, voice_rate, voice_pitch, voice_file) @@ -1483,7 +1494,7 @@ def get_audio_duration(sub_maker: submaker.SubMaker): return sub_maker.offset[-1][1] / 10000000 -def tts_multiple(task_id: str, list_script: list, voice_name: str, voice_rate: float, voice_pitch: float): +def tts_multiple(task_id: str, list_script: list, voice_name: str, voice_rate: float, voice_pitch: float, tts_engine: str = "azure"): """ 根据JSON文件中的多段文本进行TTS转换 @@ -1491,6 +1502,7 @@ def tts_multiple(task_id: str, list_script: list, voice_name: str, voice_rate: f :param list_script: 脚本列表 :param voice_name: 语音名称 :param voice_rate: 语音速率 + :param tts_engine: TTS 引擎 :return: 生成的音频文件列表 """ voice_name = parse_voice_name(voice_name) @@ -1512,6 +1524,7 @@ def tts_multiple(task_id: str, list_script: list, voice_name: str, voice_rate: f voice_rate=voice_rate, voice_pitch=voice_pitch, voice_file=audio_file, + tts_engine=tts_engine, ) if sub_maker is None: @@ -1581,14 +1594,6 @@ def get_audio_duration_from_file(audio_file: str) -> float: # 如果所有方法都失败,返回一个基于文本长度的估算 return 3.0 # 默认3秒,避免返回0 - -def is_soulvoice_voice(voice_name: str) -> bool: - """ - 检查是否为 SoulVoice 语音 - """ - return voice_name.startswith("soulvoice:") or voice_name.startswith("speech:") - - def parse_soulvoice_voice(voice_name: str) -> str: """ 解析 SoulVoice 语音名称 @@ -1600,6 +1605,118 @@ def parse_soulvoice_voice(voice_name: str) -> str: return voice_name[10:] # 移除 "soulvoice:" 前缀 return voice_name +def parse_tencent_voice(voice_name: str) -> str: + """ + 解析腾讯云 TTS 语音名称 + 支持格式:tencent:101001 + """ + if voice_name.startswith("tencent:"): + return voice_name[8:] # 移除 "tencent:" 前缀 + return voice_name + + +def tencent_tts(text: str, voice_name: str, voice_file: str, speed: float = 1.0) -> Union[SubMaker, None]: + """ + 使用腾讯云 TTS 生成语音 + """ + try: + # 导入腾讯云 SDK + from tencentcloud.common import credential + from tencentcloud.common.profile.client_profile import ClientProfile + from tencentcloud.common.profile.http_profile import HttpProfile + from tencentcloud.tts.v20190823 import tts_client, models + import base64 + except ImportError as e: + logger.error(f"腾讯云 SDK 未安装: {e}") + return None + + # 获取腾讯云配置 + tencent_config = config.tencent + secret_id = tencent_config.get("secret_id") + secret_key = tencent_config.get("secret_key") + region = tencent_config.get("region", "ap-beijing") + + if not secret_id or not secret_key: + logger.error("腾讯云 TTS 配置不完整,请检查 secret_id 和 secret_key") + return None + + # 解析语音名称 + voice_type = parse_tencent_voice(voice_name) + + # 转换速度参数 (腾讯云支持 -2 到 2 的范围) + speed_value = max(-2.0, min(2.0, (speed - 1.0) * 2)) + + for i in range(3): + try: + logger.info(f"第 {i+1} 次使用腾讯云 TTS 生成音频") + + # 创建认证对象 + cred = credential.Credential(secret_id, secret_key) + + # 创建 HTTP 配置 + httpProfile = HttpProfile() + httpProfile.endpoint = "tts.tencentcloudapi.com" + + # 创建客户端配置 + clientProfile = ClientProfile() + clientProfile.httpProfile = httpProfile + + # 创建客户端 + client = tts_client.TtsClient(cred, region, clientProfile) + + req = models.TextToVoiceRequest() + req.Text = text + req.SessionId = str(uuid.uuid4()) + req.VoiceType = int(voice_type) if voice_type.isdigit() else 101001 + req.Speed = speed_value + req.SampleRate = 16000 + req.Codec = "mp3" + req.ProjectId = 0 + req.ModelType = 1 + req.PrimaryLanguage = 1 + req.EnableSubtitle = True + + # 发送请求 + resp = client.TextToVoice(req) + + # 检查响应 + if not resp.Audio: + logger.warning(f"腾讯云 TTS 返回空音频数据") + if i < 2: + time.sleep(1) + continue + + # 解码音频数据 + audio_data = base64.b64decode(resp.Audio) + + # 写入文件 + with open(voice_file, "wb") as f: + f.write(audio_data) + + # 创建字幕对象 + sub_maker = SubMaker() + if resp.Subtitles: + for sub in resp.Subtitles: + start_ms = sub.BeginTime + end_ms = sub.EndTime + text = sub.Text + # 转换为 100ns 单位 + sub_maker.create_sub((start_ms * 10000, end_ms * 10000), text) + else: + # 如果没有字幕返回,则使用估算作为后备方案 + duration_ms = len(text) * 200 + sub_maker.create_sub((0, duration_ms * 10000), text) + + logger.info(f"腾讯云 TTS 生成成功,文件大小: {len(audio_data)} 字节") + return sub_maker + + except Exception as e: + logger.error(f"腾讯云 TTS 生成音频时出错: {str(e)}") + if i < 2: + time.sleep(1) + + return None + def soulvoice_tts(text: str, voice_name: str, voice_file: str, speed: float = 1.0) -> Union[SubMaker, None]: """ diff --git a/config.example.toml b/config.example.toml index ba185fa..6e097fc 100644 --- a/config.example.toml +++ b/config.example.toml @@ -96,6 +96,14 @@ speech_key = "" speech_region = "" +[tencent] + # 腾讯云 TTS 配置 + # 访问 https://console.cloud.tencent.com/cam/capi 获取你的密钥 + secret_id = "" + secret_key = "" + # 地域配置,默认为 ap-beijing + region = "ap-beijing" + [soulvoice] # SoulVoice TTS API 密钥 api_key = "" @@ -107,7 +115,7 @@ model = "FunAudioLLM/CosyVoice2-0.5B" [ui] - # TTS引擎选择 (edge_tts, azure_speech, soulvoice) + # TTS引擎选择 (edge_tts, azure_speech, soulvoice, tencent_tts) tts_engine = "edge_tts" # Edge TTS 配置 diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 87e5ff4..22dc0e8 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -6,6 +6,61 @@ log() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" } +# 函数:安装运行时依赖 +install_runtime_dependencies() { + log "检查并安装运行时依赖..." + + # 检查是否需要安装新的依赖 + local requirements_file="requirements.txt" + local installed_packages_file="/tmp/installed_packages.txt" + + # 如果requirements.txt存在且比已安装包列表新,则重新安装 + if [ -f "$requirements_file" ]; then + if [ ! -f "$installed_packages_file" ] || [ "$requirements_file" -nt "$installed_packages_file" ]; then + log "发现新的依赖需求,开始安装..." + + # 尝试使用sudo安装,如果失败则使用用户级安装 + if command -v sudo >/dev/null 2>&1 && sudo -n true 2>/dev/null; then + log "尝试使用sudo安装依赖..." + sudo pip install --no-cache-dir -r "$requirements_file" 2>&1 | while read line; do + log "pip: $line" + done + INSTALL_RESULT=${PIPESTATUS[0]} + else + INSTALL_RESULT=1 # 设置为失败,触发用户级安装 + fi + + # 如果sudo安装失败,尝试用户级安装 + if [ $INSTALL_RESULT -ne 0 ]; then + log "尝试用户级安装依赖..." + pip install --user --no-cache-dir -r "$requirements_file" 2>&1 | while read line; do + log "pip: $line" + done + + # 确保用户级安装的包在PATH中 + export PATH="$HOME/.local/bin:$PATH" + fi + + # 单独安装腾讯云SDK(确保安装) + log "确保腾讯云SDK已安装..." + if ! pip list | grep -q "tencentcloud-sdk-python"; then + log "安装腾讯云SDK..." + pip install --user tencentcloud-sdk-python>=3.0.1200 + else + log "腾讯云SDK已安装" + fi + + # 记录安装时间 + touch "$installed_packages_file" + log "依赖安装完成" + else + log "依赖已是最新版本,跳过安装" + fi + else + log "未找到 requirements.txt 文件" + fi +} + # 函数:检查必要的文件和目录 check_requirements() { log "检查应用环境..." @@ -27,6 +82,9 @@ check_requirements() { mkdir -p "$dir" fi done + + # 安装运行时依赖 + install_runtime_dependencies log "环境检查完成" } diff --git a/requirements.txt b/requirements.txt index b9bda86..640251e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,6 +14,7 @@ pysrt==1.1.2 openai>=1.77.0 google-generativeai>=0.8.5 azure-cognitiveservices-speech>=1.37.0 +tencentcloud-sdk-python>=3.0.1200 # 图像处理依赖 Pillow>=10.3.0 diff --git a/webui/components/audio_settings.py b/webui/components/audio_settings.py index 100cc44..368ce2e 100644 --- a/webui/components/audio_settings.py +++ b/webui/components/audio_settings.py @@ -24,7 +24,8 @@ def get_tts_engine_options(): return { "edge_tts": "Edge TTS", "azure_speech": "Azure Speech Services", - "soulvoice": "SoulVoice" + "soulvoice": "SoulVoice", + "tencent_tts": "腾讯云 TTS" } @@ -48,6 +49,12 @@ def get_tts_engine_descriptions(): "features": "提供免费额度,支持语音克隆,支持微信购买额度,无需信用卡,性价比极高", "use_case": "个人用户和中小企业,需要语音克隆功能", "registration": "https://soulvoice.scsmtech.cn/" + }, + "tencent_tts": { + "title": "腾讯云 TTS", + "features": "提供免费额度,音质优秀,支持多种音色,国内访问速度快", + "use_case": "个人和企业用户,需要稳定的中文语音合成", + "registration": "https://console.cloud.tencent.com/tts" } } @@ -126,6 +133,8 @@ def render_tts_settings(tr): render_azure_speech_settings(tr) elif selected_engine == "soulvoice": render_soulvoice_engine_settings(tr) + elif selected_engine == "tencent_tts": + render_tencent_tts_settings(tr) # 4. 试听功能 render_voice_preview_new(tr, selected_engine) @@ -357,6 +366,117 @@ def render_azure_speech_settings(tr): st.warning("⚠️ 请配置 API Key") +def render_tencent_tts_settings(tr): + """渲染腾讯云 TTS 引擎设置""" + # Secret ID 输入 + secret_id = st.text_input( + "Secret ID", + value=config.tencent.get("secret_id", ""), + help="请输入您的腾讯云 Secret ID" + ) + + # Secret Key 输入 + secret_key = st.text_input( + "Secret Key", + value=config.tencent.get("secret_key", ""), + type="password", + help="请输入您的腾讯云 Secret Key" + ) + + # 地域选择 + region_options = [ + "ap-beijing", + "ap-shanghai", + "ap-guangzhou", + "ap-chengdu", + "ap-nanjing", + "ap-singapore", + "ap-hongkong" + ] + + saved_region = config.tencent.get("region", "ap-beijing") + if saved_region not in region_options: + region_options.append(saved_region) + + region = st.selectbox( + "服务地域", + options=region_options, + index=region_options.index(saved_region), + help="选择腾讯云 TTS 服务地域" + ) + + # 音色选择 + voice_type_options = { + "101001": "智瑜 - 女声(推荐)", + "101002": "智聆 - 女声", + "101003": "智美 - 女声", + "101004": "智云 - 男声", + "101005": "智莉 - 女声", + "101006": "智言 - 男声", + "101007": "智娜 - 女声", + "101008": "智琪 - 女声", + "101009": "智芸 - 女声", + "101010": "智华 - 男声", + "101011": "智燕 - 女声", + "101012": "智丹 - 女声", + "101013": "智辉 - 男声", + "101014": "智宁 - 女声", + "101015": "智萌 - 女声", + "101016": "智甜 - 女声", + "101017": "智蓉 - 女声", + "101018": "智靖 - 男声" + } + + saved_voice_type = config.ui.get("tencent_voice_type", "101001") + if saved_voice_type not in voice_type_options: + voice_type_options[saved_voice_type] = f"自定义音色 ({saved_voice_type})" + + selected_voice_display = st.selectbox( + "音色选择", + options=list(voice_type_options.values()), + index=list(voice_type_options.keys()).index(saved_voice_type), + help="选择腾讯云 TTS 音色" + ) + + # 获取实际的音色ID + voice_type = list(voice_type_options.keys())[ + list(voice_type_options.values()).index(selected_voice_display) + ] + + # 语速调节 + voice_rate = st.slider( + "语速调节", + min_value=0.5, + max_value=2.0, + value=config.ui.get("tencent_rate", 1.0), + step=0.1, + help="调节语音速度 (0.5-2.0)" + ) + + # 显示音色说明 + with st.expander("💡 腾讯云 TTS 音色说明", expanded=False): + st.write("**女声音色:**") + female_voices = [(k, v) for k, v in voice_type_options.items() if "女声" in v] + for voice_id, voice_desc in female_voices[:6]: # 显示前6个 + st.write(f"• {voice_desc} (ID: {voice_id})") + + st.write("") + st.write("**男声音色:**") + male_voices = [(k, v) for k, v in voice_type_options.items() if "男声" in v] + for voice_id, voice_desc in male_voices: + st.write(f"• {voice_desc} (ID: {voice_id})") + + st.write("") + st.info("💡 更多音色请参考腾讯云官方文档") + + # 保存配置 + config.tencent["secret_id"] = secret_id + config.tencent["secret_key"] = secret_key + config.tencent["region"] = region + config.ui["tencent_voice_type"] = voice_type + config.ui["tencent_rate"] = voice_rate + + def render_soulvoice_engine_settings(tr): """渲染 SoulVoice 引擎设置""" # API Key 输入 @@ -453,6 +573,11 @@ def render_voice_preview_new(tr, selected_engine): voice_name = voice_uri if voice_uri.startswith("soulvoice:") else f"soulvoice:{voice_uri}" voice_rate = 1.0 # SoulVoice 使用默认语速 voice_pitch = 1.0 # SoulVoice 不支持音调调节 + elif selected_engine == "tencent_tts": + voice_type = config.ui.get("tencent_voice_type", "101001") + voice_name = f"tencent:{voice_type}" + voice_rate = config.ui.get("tencent_rate", 1.0) + voice_pitch = 1.0 # 腾讯云 TTS 不支持音调调节 if not voice_name: st.error("请先配置语音设置") From a39c11e0d5d5fca1f7789f1b9ed1eea5d1a0d67a Mon Sep 17 00:00:00 2001 From: linyq Date: Wed, 17 Sep 2025 00:08:01 +0800 Subject: [PATCH 6/6] =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=85=BE=E8=AE=AFtts?= =?UTF-8?q?=E5=BC=95=E6=93=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/schema.py | 2 +- app/services/voice.py | 8 ++++++-- webui/components/audio_settings.py | 17 ++++++++--------- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/app/models/schema.py b/app/models/schema.py index e0447b7..52e9aef 100644 --- a/app/models/schema.py +++ b/app/models/schema.py @@ -176,7 +176,7 @@ class VideoClipParams(BaseModel): voice_volume: Optional[float] = Field(default=AudioVolumeDefaults.VOICE_VOLUME, description="解说语音音量") voice_rate: Optional[float] = Field(default=1.0, description="语速") voice_pitch: Optional[float] = Field(default=1.0, description="语调") - tts_engine: Optional[str] = Field(default="tencent", description="TTS 引擎") + tts_engine: Optional[str] = Field(default="", description="TTS 引擎") bgm_name: Optional[str] = Field(default="random", description="背景音乐名称") bgm_type: Optional[str] = Field(default="random", description="背景音乐类型") bgm_file: Optional[str] = Field(default="", description="背景音乐文件") diff --git a/app/services/voice.py b/app/services/voice.py index a114534..355dfcf 100644 --- a/app/services/voice.py +++ b/app/services/voice.py @@ -1085,7 +1085,7 @@ def tts( ) -> Union[SubMaker, None]: logger.info(f"使用 TTS 引擎: '{tts_engine}', 语音: '{voice_name}'") - if tts_engine == "tencent": + if tts_engine == "tencent_tts": logger.info("分发到腾讯云 TTS") return tencent_tts(text, voice_name, voice_file, speed=voice_rate) @@ -1093,12 +1093,16 @@ def tts( logger.info("分发到 SoulVoice TTS") return soulvoice_tts(text, voice_name, voice_file, speed=voice_rate) - if tts_engine == "azure": + if tts_engine == "azure_speech": if should_use_azure_speech_services(voice_name): logger.info("分发到 Azure Speech Services (V2)") return azure_tts_v2(text, voice_name, voice_file) logger.info("分发到 Edge TTS (Azure V1)") return azure_tts_v1(text, voice_name, voice_rate, voice_pitch, voice_file) + + if tts_engine == "edge_tts": + logger.info("分发到 Edge TTS") + return azure_tts_v1(text, voice_name, voice_rate, voice_pitch, voice_file) # Fallback for unknown engine - default to azure v1 logger.warning(f"未知的 TTS 引擎: '{tts_engine}', 将默认使用 Edge TTS (Azure V1)。") diff --git a/webui/components/audio_settings.py b/webui/components/audio_settings.py index 368ce2e..d83a88d 100644 --- a/webui/components/audio_settings.py +++ b/webui/components/audio_settings.py @@ -112,6 +112,7 @@ def render_tts_settings(tr): # 保存TTS引擎选择 config.ui["tts_engine"] = selected_engine + st.session_state['tts_engine'] = selected_engine # 2. 显示引擎详细说明 if selected_engine in engine_descriptions: @@ -490,17 +491,14 @@ def render_soulvoice_engine_settings(tr): # 音色 URI 输入 voice_uri = st.text_input( "音色URI", - value=config.soulvoice.get("voice_uri", "speech:mcg3fdnx:clzkyf4vy00e5qr6hywum4u84:bzznlkuhcjzpbosexitr"), + value=config.soulvoice.get("voice_uri", "speech:2c2hp73s:clzkyf4vy00e5qr6hywum4u84:itjmezhxyynkyzrhhjav"), help="请输入 SoulVoice 音色标识符", - placeholder="speech:mcg3fdnx:clzkyf4vy00e5qr6hywum4u84:bzznlkuhcjzpbosexitr" + placeholder="speech:2c2hp73s:clzkyf4vy00e5qr6hywum4u84:itjmezhxyynkyzrhhjav" ) # 模型名称选择 model_options = [ - "FunAudioLLM/CosyVoice2-0.5B", - "FunAudioLLM/CosyVoice-300M", - "FunAudioLLM/CosyVoice-300M-SFT", - "FunAudioLLM/CosyVoice-300M-Instruct" + "FunAudioLLM/CosyVoice2-0.5B" ] saved_model = config.soulvoice.get("model", "FunAudioLLM/CosyVoice2-0.5B") @@ -636,7 +634,7 @@ def render_soulvoice_settings(tr): saved_api_key = config.soulvoice.get("api_key", "") saved_api_url = config.soulvoice.get("api_url", "https://tts.scsmtech.cn/tts") saved_model = config.soulvoice.get("model", "FunAudioLLM/CosyVoice2-0.5B") - saved_voice_uri = config.soulvoice.get("voice_uri", "speech:mcg3fdnx:clzkyf4vy00e5qr6hywum4u84:bzznlkuhcjzpbosexitr") + saved_voice_uri = config.soulvoice.get("voice_uri", "speech:2c2hp73s:clzkyf4vy00e5qr6hywum4u84:itjmezhxyynkyzrhhjav") # API Key 输入 api_key = st.text_input( @@ -650,8 +648,8 @@ def render_soulvoice_settings(tr): voice_uri = st.text_input( "音色 URI", value=saved_voice_uri, - help="请输入 SoulVoice 音色标识符,格式如:speech:mcg3fdnx:clzkyf4vy00e5qr6hywum4u84:bzznlkuhcjzpbosexitr", - placeholder="speech:mcg3fdnx:clzkyf4vy00e5qr6hywum4u84:bzznlkuhcjzpbosexitr" + help="请输入 SoulVoice 音色标识符,格式如:speech:2c2hp73s:clzkyf4vy00e5qr6hywum4u84:itjmezhxyynkyzrhhjav", + placeholder="speech:2c2hp73s:clzkyf4vy00e5qr6hywum4u84:itjmezhxyynkyzrhhjav" ) # API URL 输入(可选) @@ -822,4 +820,5 @@ def get_audio_params(): 'bgm_type': st.session_state.get('bgm_type', 'random'), 'bgm_file': st.session_state.get('bgm_file', ''), 'bgm_volume': st.session_state.get('bgm_volume', AudioVolumeDefaults.BGM_VOLUME), + 'tts_engine': st.session_state.get('tts_engine', "edge_tts"), }